@stati/core 1.29.2 → 2.0.1
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 +10 -13
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +77 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +39 -29
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +9 -7
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +156 -197
- package/dist/core/eta-plugin.d.ts +60 -0
- package/dist/core/eta-plugin.d.ts.map +1 -0
- package/dist/core/eta-plugin.js +143 -0
- package/dist/core/isg/builder.d.ts +37 -19
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +70 -153
- package/dist/core/isg/deps.d.ts +8 -70
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +10 -401
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +2 -1
- package/dist/core/isg/index.d.ts +2 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -3
- package/dist/core/isg/manifest.d.ts.map +1 -1
- package/dist/core/isg/manifest.js +6 -1
- package/dist/core/markdown.d.ts +1 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +3 -2
- package/dist/core/template-index.d.ts +209 -0
- package/dist/core/template-index.d.ts.map +1 -0
- package/dist/core/template-index.js +484 -0
- package/dist/core/template-render.d.ts +137 -0
- package/dist/core/template-render.d.ts.map +1 -0
- package/dist/core/template-render.js +444 -0
- package/dist/core/templates.d.ts +29 -11
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +115 -196
- package/dist/core/utils/context-validation.utils.d.ts +10 -6
- package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
- package/dist/core/utils/context-validation.utils.js +39 -16
- package/dist/core/utils/date.utils.d.ts +1 -1
- package/dist/core/utils/date.utils.js +1 -1
- package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
- package/dist/core/utils/dev-css-watcher.utils.js +3 -2
- package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
- package/dist/core/utils/error-overlay.utils.js +7 -45
- package/dist/core/utils/front-matter.utils.d.ts +50 -0
- package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
- package/dist/core/utils/front-matter.utils.js +157 -0
- package/dist/core/utils/index.d.ts +4 -5
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +5 -7
- package/dist/core/utils/paths.utils.d.ts +3 -3
- package/dist/core/utils/paths.utils.js +6 -6
- package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +15 -6
- package/dist/core/utils/template-discovery.utils.d.ts +3 -14
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +2 -62
- package/dist/core/utils/template-errors.utils.d.ts +14 -1
- package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
- package/dist/core/utils/template-errors.utils.js +45 -4
- package/dist/core/utils/theme.utils.d.ts +17 -0
- package/dist/core/utils/theme.utils.d.ts.map +1 -0
- package/dist/core/utils/theme.utils.js +59 -0
- package/dist/images/index.d.ts +1 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +0 -1
- package/dist/images/markdown-rule.d.ts +1 -2
- package/dist/images/markdown-rule.d.ts.map +1 -1
- package/dist/images/og.d.ts +2 -15
- package/dist/images/og.d.ts.map +1 -1
- package/dist/images/og.js +4 -47
- package/dist/images/runtime.d.ts +24 -5
- package/dist/images/runtime.d.ts.map +1 -1
- package/dist/images/runtime.js +14 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +4 -1
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +11 -8
- package/dist/metrics/types.d.ts +31 -5
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +2 -1
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.js +1115 -1136
- package/dist/types/config.d.ts +18 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +83 -16
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +48 -2
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/core/utils/callable-partials.utils.d.ts +0 -60
- package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
- package/dist/core/utils/callable-partials.utils.js +0 -127
- package/dist/core/utils/partial-validation.utils.d.ts +0 -9
- package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
- package/dist/core/utils/partial-validation.utils.js +0 -107
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* HTML Metrics Report Generator
|
|
3
3
|
*
|
|
4
4
|
* Generates a self-contained HTML file for visualizing build performance metrics.
|
|
5
|
-
* Handles both standard and detailed (with pageTimings) metrics files.
|
|
5
|
+
* Handles both standard and detailed (with pageTimings) metrics files. The page shares its
|
|
6
|
+
* look with the Stati documentation site and the development error overlay.
|
|
6
7
|
*/
|
|
7
8
|
import { writeFile } from 'node:fs/promises';
|
|
9
|
+
import { STATI_FAVICON_HREF, STATI_MARK_PATH, STATI_PRINT_STYLES, STATI_THEME_STYLES, } from '../../core/utils/theme.utils.js';
|
|
8
10
|
/**
|
|
9
11
|
* Format bytes to human-readable string
|
|
10
12
|
*/
|
|
@@ -14,6 +16,8 @@ function formatBytes(bytes) {
|
|
|
14
16
|
const k = 1024;
|
|
15
17
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
16
18
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
19
|
+
if (i === 0)
|
|
20
|
+
return `${bytes} B`;
|
|
17
21
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
@@ -32,36 +36,57 @@ function formatDuration(ms) {
|
|
|
32
36
|
function formatPercent(value) {
|
|
33
37
|
return `${(value * 100).toFixed(1)}%`;
|
|
34
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Format a count with thousands separators
|
|
41
|
+
*/
|
|
42
|
+
function formatCount(value) {
|
|
43
|
+
return value.toLocaleString('en-US');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Escapes text for HTML content and attribute values.
|
|
47
|
+
*/
|
|
48
|
+
function escapeHtml(text) {
|
|
49
|
+
return text
|
|
50
|
+
.replace(/&/g, '&')
|
|
51
|
+
.replace(/</g, '<')
|
|
52
|
+
.replace(/>/g, '>')
|
|
53
|
+
.replace(/"/g, '"')
|
|
54
|
+
.replace(/'/g, ''');
|
|
55
|
+
}
|
|
35
56
|
/**
|
|
36
57
|
* Get human-readable phase name
|
|
37
58
|
*/
|
|
38
59
|
function getPhaseLabel(key) {
|
|
39
60
|
const labels = {
|
|
40
|
-
configLoadMs: 'Config
|
|
41
|
-
contentDiscoveryMs: 'Content
|
|
42
|
-
navigationBuildMs: 'Navigation
|
|
43
|
-
cacheManifestLoadMs: 'Cache
|
|
44
|
-
typescriptCompileMs: 'TypeScript
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
configLoadMs: 'Config loading',
|
|
62
|
+
contentDiscoveryMs: 'Content discovery',
|
|
63
|
+
navigationBuildMs: 'Navigation build',
|
|
64
|
+
cacheManifestLoadMs: 'Cache manifest load',
|
|
65
|
+
typescriptCompileMs: 'TypeScript compile',
|
|
66
|
+
templateIndexMs: 'Template index',
|
|
67
|
+
pageRenderingMs: 'Page rendering',
|
|
68
|
+
shouldRebuildTotalMs: 'Rebuild checks',
|
|
69
|
+
markdownRenderTotalMs: 'Markdown',
|
|
70
|
+
renderPageTotalMs: 'Render pages',
|
|
71
|
+
postRenderTotalMs: 'After rendering',
|
|
72
|
+
fileWriteTotalMs: 'File writing',
|
|
73
|
+
cacheEntryTotalMs: 'Cache entry updates',
|
|
74
|
+
searchIndexGenerationMs: 'Search index generation',
|
|
75
|
+
searchIndexWriteMs: 'Search index write',
|
|
76
|
+
assetCopyMs: 'Asset copying',
|
|
77
|
+
cacheManifestSaveMs: 'Cache manifest save',
|
|
78
|
+
sitemapGenerationMs: 'Sitemap generation',
|
|
79
|
+
rssGenerationMs: 'RSS generation',
|
|
80
|
+
tailwindInitMs: 'Tailwind init',
|
|
81
|
+
tailwindInventoryMs: 'Tailwind inventory',
|
|
82
|
+
getDirectorySizeMs: 'Directory size',
|
|
83
|
+
lockAcquireMs: 'Lock acquire',
|
|
84
|
+
lockReleaseMs: 'Lock release',
|
|
85
|
+
hookBeforeAllMs: 'beforeAll hook',
|
|
86
|
+
hookAfterAllMs: 'afterAll hook',
|
|
87
|
+
hookBeforeRenderTotalMs: 'beforeRender hooks',
|
|
88
|
+
hookAfterRenderTotalMs: 'afterRender hooks',
|
|
89
|
+
imageSettleMs: 'Image generation',
|
|
65
90
|
};
|
|
66
91
|
return (labels[key] ||
|
|
67
92
|
key
|
|
@@ -79,12 +104,15 @@ function getPhaseDescription(key) {
|
|
|
79
104
|
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
105
|
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
106
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
templateIndexMs: 'Time spent listing every .eta file in the source directory, once per build. Template names resolve against this list, so neither rendering nor the cache checks search folders page by page. A dev server keeps the list between rebuilds.',
|
|
108
|
+
pageRenderingMs: 'Total time spent in the page pipeline: the beforeAll hook, the search index, and for every page the rebuild check, rendering, hooks, file writing and cache update. The steps listed under it are its parts; the per-page ones are summed over all pages. This is typically the largest phase in a build.',
|
|
109
|
+
shouldRebuildTotalMs: 'Aggregate time spent checking if each page needs to be rebuilt. This includes computing content hashes, hashing the templates each page rendered, and checking the template names it looked up against the template index.',
|
|
110
|
+
markdownRenderTotalMs: "Aggregate time spent rendering each rendered page's Markdown to HTML with markdown-it, including its plugins, heading anchors, the table of contents and image sizing. Long pages and heavy plugins increase this time.",
|
|
111
|
+
renderPageTotalMs: 'Aggregate time spent actually rendering pages through the Eta template engine: each layout, the parents it names and the templates it includes. Templates a page does not use are never rendered. Complex templates with many includes or heavy computations will increase this time.',
|
|
112
|
+
postRenderTotalMs: 'Aggregate time spent on each rendered page between its templates and the file write: injecting SEO, search and bundle tags, registering the images the HTML uses, and hashing the data the page read for the cache.',
|
|
85
113
|
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
114
|
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
|
|
115
|
+
searchIndexGenerationMs: 'Time spent generating the search index from page content before the pages render. This includes extracting text and headings and building the index structure.',
|
|
88
116
|
searchIndexWriteMs: 'Time spent writing the search index JSON file to disk. Large indices with many documents will take longer to serialize and write.',
|
|
89
117
|
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
118
|
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.',
|
|
@@ -99,11 +127,12 @@ function getPhaseDescription(key) {
|
|
|
99
127
|
hookAfterAllMs: 'Time spent executing the afterAll hook defined in your config. Post-build processing like compression or uploads will show here.',
|
|
100
128
|
hookBeforeRenderTotalMs: 'Aggregate time spent in beforeRender hooks across all pages. Per-page setup logic accumulates here.',
|
|
101
129
|
hookAfterRenderTotalMs: 'Aggregate time spent in afterRender hooks across all pages. Per-page post-processing accumulates here.',
|
|
130
|
+
imageSettleMs: 'Time spent waiting for the responsive image variants and Open Graph cards the written pages use, and saving the images manifest. Reused files make this fast.',
|
|
102
131
|
};
|
|
103
132
|
return descriptions[key] || 'No description available for this phase.';
|
|
104
133
|
}
|
|
105
134
|
/**
|
|
106
|
-
*
|
|
135
|
+
* Phases in the order a build runs them. The phases measured inside page rendering follow it.
|
|
107
136
|
*/
|
|
108
137
|
const PHASE_EXECUTION_ORDER = [
|
|
109
138
|
'lockAcquireMs',
|
|
@@ -112,17 +141,21 @@ const PHASE_EXECUTION_ORDER = [
|
|
|
112
141
|
'cacheManifestLoadMs',
|
|
113
142
|
'contentDiscoveryMs',
|
|
114
143
|
'navigationBuildMs',
|
|
115
|
-
'hookBeforeAllMs',
|
|
116
144
|
'typescriptCompileMs',
|
|
145
|
+
'templateIndexMs',
|
|
146
|
+
'pageRenderingMs',
|
|
147
|
+
'hookBeforeAllMs',
|
|
148
|
+
'searchIndexGenerationMs',
|
|
149
|
+
'searchIndexWriteMs',
|
|
117
150
|
'shouldRebuildTotalMs',
|
|
118
151
|
'hookBeforeRenderTotalMs',
|
|
152
|
+
'markdownRenderTotalMs',
|
|
119
153
|
'renderPageTotalMs',
|
|
120
|
-
'
|
|
154
|
+
'postRenderTotalMs',
|
|
121
155
|
'fileWriteTotalMs',
|
|
122
156
|
'cacheEntryTotalMs',
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'searchIndexWriteMs',
|
|
157
|
+
'hookAfterRenderTotalMs',
|
|
158
|
+
'imageSettleMs',
|
|
126
159
|
'tailwindInventoryMs',
|
|
127
160
|
'cacheManifestSaveMs',
|
|
128
161
|
'assetCopyMs',
|
|
@@ -133,1396 +166,1342 @@ const PHASE_EXECUTION_ORDER = [
|
|
|
133
166
|
'lockReleaseMs',
|
|
134
167
|
];
|
|
135
168
|
/**
|
|
136
|
-
*
|
|
169
|
+
* Phases measured inside page rendering, so its time includes theirs. The per-page ones are
|
|
170
|
+
* sums over all pages.
|
|
137
171
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
172
|
+
const PAGE_RENDERING_STEPS = new Set([
|
|
173
|
+
'hookBeforeAllMs',
|
|
174
|
+
'searchIndexGenerationMs',
|
|
175
|
+
'searchIndexWriteMs',
|
|
176
|
+
'shouldRebuildTotalMs',
|
|
177
|
+
'hookBeforeRenderTotalMs',
|
|
178
|
+
'markdownRenderTotalMs',
|
|
179
|
+
'renderPageTotalMs',
|
|
180
|
+
'postRenderTotalMs',
|
|
181
|
+
'fileWriteTotalMs',
|
|
182
|
+
'cacheEntryTotalMs',
|
|
183
|
+
'hookAfterRenderTotalMs',
|
|
184
|
+
]);
|
|
185
|
+
function executionIndex(key) {
|
|
186
|
+
const index = PHASE_EXECUTION_ORDER.indexOf(key);
|
|
187
|
+
return index === -1 ? PHASE_EXECUTION_ORDER.length : index;
|
|
148
188
|
}
|
|
149
189
|
/**
|
|
150
|
-
*
|
|
190
|
+
* The recorded phases in the order the build runs them, each with its share of the build and
|
|
191
|
+
* where its bar starts on the timeline: where the phase before it ended. The phases measured
|
|
192
|
+
* inside page rendering are nested under it, so no time is counted twice.
|
|
151
193
|
*/
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
.filter((
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
194
|
+
function generatePhaseRows(phases, durationMs) {
|
|
195
|
+
const recorded = Object.entries(phases)
|
|
196
|
+
.filter((entry) => entry[1] !== undefined && entry[1] > 0)
|
|
197
|
+
.sort(([a], [b]) => executionIndex(a) - executionIndex(b));
|
|
198
|
+
const nested = recorded.some(([key]) => key === 'pageRenderingMs');
|
|
199
|
+
const isStep = (key) => nested && PAGE_RENDERING_STEPS.has(key);
|
|
200
|
+
const topLevel = recorded.filter(([key]) => !isStep(key));
|
|
201
|
+
const scale = Math.max(durationMs, topLevel.reduce((sum, [, value]) => sum + value, 0));
|
|
202
|
+
const toRow = ([key, value], start) => ({
|
|
203
|
+
key,
|
|
158
204
|
label: getPhaseLabel(key),
|
|
159
205
|
description: getPhaseDescription(key),
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
206
|
+
value,
|
|
207
|
+
share: (value / scale) * 100,
|
|
208
|
+
start,
|
|
209
|
+
steps: [],
|
|
210
|
+
});
|
|
211
|
+
let elapsed = 0;
|
|
212
|
+
return topLevel.map((entry) => {
|
|
213
|
+
const row = toRow(entry, (elapsed / scale) * 100);
|
|
214
|
+
elapsed += entry[1];
|
|
215
|
+
if (entry[0] === 'pageRenderingMs') {
|
|
216
|
+
let stepStart = row.start;
|
|
217
|
+
row.steps = recorded
|
|
218
|
+
.filter(([key]) => isStep(key))
|
|
219
|
+
.map((step) => {
|
|
220
|
+
const stepRow = toRow(step, stepStart);
|
|
221
|
+
stepStart += stepRow.share;
|
|
222
|
+
return stepRow;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return row;
|
|
167
226
|
});
|
|
168
227
|
}
|
|
169
228
|
/**
|
|
170
|
-
*
|
|
229
|
+
* The same phases, slowest first, and the steps inside a phase slowest first as well.
|
|
171
230
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
<
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
function sortByDuration(rows) {
|
|
232
|
+
return [...rows]
|
|
233
|
+
.sort((a, b) => b.value - a.value)
|
|
234
|
+
.map((row) => ({ ...row, steps: [...row.steps].sort((a, b) => b.value - a.value) }));
|
|
235
|
+
}
|
|
236
|
+
const CHEVRON_ICON = `<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>`;
|
|
237
|
+
/**
|
|
238
|
+
* A horizontal bar. Thin values stay visible, and a bar never runs past the end of its track.
|
|
239
|
+
*/
|
|
240
|
+
function renderMeter(start, share) {
|
|
241
|
+
const width = Math.min(Math.max(share, 0.4), 100 - start);
|
|
242
|
+
return `<span class="meter" aria-hidden="true"><span class="meter-fill" style="left: ${start.toFixed(2)}%; width: ${width.toFixed(2)}%"></span></span>`;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Phase rows that open to describe what the phase measures. The timeline numbers the phases and
|
|
246
|
+
* starts each bar where the phase began.
|
|
247
|
+
*/
|
|
248
|
+
function renderPhaseRows(rows, view, isStep = false) {
|
|
249
|
+
return rows
|
|
250
|
+
.map((row, index) => {
|
|
251
|
+
const order = view === 'timeline' && !isStep
|
|
252
|
+
? `
|
|
253
|
+
<span class="phase-order">${String(index + 1).padStart(2, '0')}</span>`
|
|
254
|
+
: '';
|
|
255
|
+
const steps = row.steps.length > 0
|
|
256
|
+
? `
|
|
257
|
+
<ol class="phase-steps" aria-label="${escapeHtml(`Parts of ${row.label.toLowerCase()}`)}">${renderPhaseRows(row.steps, view, true)}
|
|
258
|
+
</ol>`
|
|
259
|
+
: '';
|
|
260
|
+
return `
|
|
261
|
+
<li class="${isStep ? 'phase phase-step' : 'phase'}">
|
|
262
|
+
<details>
|
|
263
|
+
<summary class="phase-row" title="${escapeHtml(row.key)}">${order}
|
|
264
|
+
<span class="phase-name">${escapeHtml(row.label)}</span>
|
|
265
|
+
${renderMeter(view === 'timeline' ? row.start : 0, row.share)}
|
|
266
|
+
<span class="phase-time">${formatDuration(row.value)}</span>
|
|
267
|
+
<span class="phase-share">${row.share.toFixed(1)}%</span>
|
|
268
|
+
${CHEVRON_ICON}
|
|
269
|
+
</summary>
|
|
270
|
+
<p class="phase-description">${escapeHtml(row.description)}</p>
|
|
271
|
+
</details>${steps}
|
|
272
|
+
</li>`;
|
|
273
|
+
})
|
|
274
|
+
.join('');
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The phases, slowest first or in the order the build runs them.
|
|
278
|
+
*/
|
|
279
|
+
function renderPhasesSection(rows) {
|
|
280
|
+
const count = rows.reduce((sum, row) => sum + 1 + row.steps.length, 0);
|
|
281
|
+
const body = rows.length === 0
|
|
282
|
+
? `
|
|
283
|
+
<p class="empty">No phases were recorded for this build.</p>`
|
|
284
|
+
: `
|
|
285
|
+
<div class="tabs" role="tablist" aria-label="Phase order">
|
|
286
|
+
<button type="button" class="tab" role="tab" id="phases-tab-duration" aria-controls="phases-duration" aria-selected="true">By duration</button>
|
|
287
|
+
<button type="button" class="tab" role="tab" id="phases-tab-timeline" aria-controls="phases-timeline" aria-selected="false" tabindex="-1">Timeline</button>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div class="tab-panel" role="tabpanel" id="phases-duration" aria-labelledby="phases-tab-duration">
|
|
291
|
+
<p class="view-intro">Slowest first, as a share of the build. Open a phase to see what it measures.</p>
|
|
292
|
+
<ol class="phases" data-view="duration">${renderPhaseRows(sortByDuration(rows), 'duration')}
|
|
293
|
+
</ol>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="tab-panel" role="tabpanel" id="phases-timeline" aria-labelledby="phases-tab-timeline" hidden>
|
|
297
|
+
<p class="view-intro">In the order the build runs them. Steps inside page rendering are totals over all pages, so their bars show how long they took, not when.</p>
|
|
298
|
+
<ol class="phases" data-view="timeline">${renderPhaseRows(rows, 'timeline')}
|
|
299
|
+
</ol>
|
|
300
|
+
</div>`;
|
|
301
|
+
return `
|
|
302
|
+
<section class="section" id="phases" aria-labelledby="phases-title">
|
|
303
|
+
<div class="section-head">
|
|
304
|
+
<h2 class="section-title" id="phases-title">Phase breakdown</h2>${count > 0
|
|
305
|
+
? `
|
|
306
|
+
<span class="section-note">${count} ${count === 1 ? 'phase' : 'phases'}</span>`
|
|
307
|
+
: ''}
|
|
308
|
+
</div>${body}
|
|
309
|
+
</section>`;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* One group of counts, headed by a rule, with a value per row.
|
|
313
|
+
*/
|
|
314
|
+
function renderCountGroup(title, items, status) {
|
|
315
|
+
const rows = items
|
|
316
|
+
.filter((item) => item !== null)
|
|
317
|
+
.map(([label, value]) => `
|
|
318
|
+
<div class="count"><dt>${label}</dt><dd>${value}</dd></div>`)
|
|
319
|
+
.join('');
|
|
320
|
+
return `
|
|
321
|
+
<div class="group">
|
|
322
|
+
<h3 class="group-title">${title}${status ? `<span class="group-status">${status}</span>` : ''}</h3>
|
|
323
|
+
<dl class="counts">${rows}
|
|
324
|
+
</dl>
|
|
325
|
+
</div>`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Every count of the build, grouped.
|
|
329
|
+
*/
|
|
330
|
+
function renderCountsSection(metrics) {
|
|
331
|
+
const { counts, isg } = metrics;
|
|
332
|
+
const optional = (label, value) => value === undefined ? null : [label, formatCount(value)];
|
|
333
|
+
const perPage = counts.renderedPages > 0 ? (counts.templatesLoaded / counts.renderedPages).toFixed(1) : '0';
|
|
334
|
+
return `
|
|
335
|
+
<section class="section" id="counts" aria-labelledby="counts-title">
|
|
336
|
+
<h2 class="section-title" id="counts-title">Counts</h2>
|
|
337
|
+
<div class="groups">${renderCountGroup('Pages', [
|
|
338
|
+
['Total', formatCount(counts.totalPages)],
|
|
339
|
+
['Rendered', formatCount(counts.renderedPages)],
|
|
340
|
+
['From cache', formatCount(counts.cachedPages)],
|
|
341
|
+
['Markdown files', formatCount(counts.markdownFilesProcessed)],
|
|
342
|
+
])}${renderCountGroup('Templates', [
|
|
343
|
+
optional('Indexed', counts.templatesIndexed),
|
|
344
|
+
['Loaded', formatCount(counts.templatesLoaded)],
|
|
345
|
+
['Per rendered page', perPage],
|
|
346
|
+
optional('Renders', counts.templateRenders),
|
|
347
|
+
optional('Cached includes', counts.includeCacheHits),
|
|
348
|
+
])}${renderCountGroup('ISG cache', [
|
|
349
|
+
['Hit rate', formatPercent(isg.cacheHitRate)],
|
|
350
|
+
['Manifest entries', formatCount(isg.manifestEntries)],
|
|
351
|
+
['Invalidated', formatCount(isg.invalidatedEntries)],
|
|
352
|
+
], isg.enabled ? 'Enabled' : 'Disabled')}${renderCountGroup('Assets', [
|
|
353
|
+
['Copied', formatCount(counts.assetsCopied)],
|
|
354
|
+
optional('Images generated', counts.imagesGenerated),
|
|
355
|
+
optional('Images reused', counts.imagesCached),
|
|
356
|
+
optional('Images failed', counts.imagesFailed),
|
|
357
|
+
])}
|
|
358
|
+
</div>
|
|
359
|
+
</section>`;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* What triggered a dev server rebuild and what it did.
|
|
363
|
+
*/
|
|
364
|
+
function renderIncrementalSection(incremental) {
|
|
365
|
+
return `
|
|
366
|
+
<section class="section" id="rebuild" aria-labelledby="rebuild-title">
|
|
367
|
+
<h2 class="section-title" id="rebuild-title">Incremental rebuild</h2>
|
|
368
|
+
<dl class="facts">
|
|
369
|
+
<div class="fact">
|
|
370
|
+
<dt>Changed file</dt>
|
|
371
|
+
<dd>${escapeHtml(incremental.triggerFile)}</dd>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="fact">
|
|
374
|
+
<dt>Type</dt>
|
|
375
|
+
<dd>${escapeHtml(incremental.triggerType)}</dd>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="fact">
|
|
378
|
+
<dt>Duration</dt>
|
|
379
|
+
<dd>${formatDuration(incremental.durationMs)}</dd>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="fact">
|
|
382
|
+
<dt>Pages</dt>
|
|
383
|
+
<dd>${formatCount(incremental.renderedPages)} rendered, ${formatCount(incremental.cachedPages)} cached</dd>
|
|
384
|
+
</div>
|
|
385
|
+
</dl>
|
|
386
|
+
</section>`;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Every page, slowest first: rendered (solid square) or served from cache (hollow circle),
|
|
390
|
+
* with its duration against the slowest page and the templates it rendered.
|
|
391
|
+
*/
|
|
392
|
+
function renderPagesSection(pageTimings) {
|
|
393
|
+
const pages = [...pageTimings].sort((a, b) => b.durationMs - a.durationMs);
|
|
394
|
+
const slowest = pages.reduce((max, page) => Math.max(max, page.durationMs), 0);
|
|
395
|
+
const rendered = pages.filter((page) => !page.cached).length;
|
|
396
|
+
const rows = pages
|
|
397
|
+
.map((page) => {
|
|
398
|
+
const status = page.cached ? 'cached' : 'rendered';
|
|
399
|
+
const meter = slowest > 0 && page.durationMs > 0
|
|
400
|
+
? renderMeter(0, (page.durationMs / slowest) * 100)
|
|
401
|
+
: '<span class="meter meter-empty" aria-hidden="true"></span>';
|
|
402
|
+
// A page served from cache did not render, so it has no render time
|
|
403
|
+
const duration = page.cached && page.durationMs === 0 ? '–' : formatDuration(page.durationMs);
|
|
404
|
+
return `
|
|
405
|
+
<tr>
|
|
406
|
+
<td class="page-url"><span title="${escapeHtml(page.url)}">${escapeHtml(page.url)}</span></td>
|
|
407
|
+
<td><span class="status status-${status}"><span class="marker" aria-hidden="true"></span>${status}</span></td>
|
|
408
|
+
<td class="page-duration"><span class="duration">${meter}<span class="num">${duration}</span></span></td>
|
|
409
|
+
<td class="num">${page.templatesLoaded ?? '–'}</td>
|
|
410
|
+
<td class="num">${page.templateRenders ?? '–'}</td>
|
|
411
|
+
</tr>`;
|
|
412
|
+
})
|
|
413
|
+
.join('');
|
|
414
|
+
return `
|
|
415
|
+
<section class="section" id="pages" aria-labelledby="pages-title">
|
|
416
|
+
<h2 class="section-title" id="pages-title">Page timings</h2>
|
|
417
|
+
<figure class="panel">
|
|
418
|
+
<figcaption class="panel-bar">
|
|
419
|
+
<span>${formatCount(pages.length)} ${pages.length === 1 ? 'page' : 'pages'}, slowest first</span>
|
|
420
|
+
<span class="panel-summary">${formatCount(rendered)} rendered · ${formatCount(pages.length - rendered)} cached</span>
|
|
421
|
+
</figcaption>
|
|
422
|
+
<div class="table-scroll" tabindex="0" role="region" aria-labelledby="pages-title">
|
|
423
|
+
<table class="pages">
|
|
424
|
+
<thead>
|
|
425
|
+
<tr>
|
|
426
|
+
<th scope="col">Page</th>
|
|
427
|
+
<th scope="col">Result</th>
|
|
428
|
+
<th scope="col" class="page-duration">Duration</th>
|
|
429
|
+
<th scope="col" class="num">Templates</th>
|
|
430
|
+
<th scope="col" class="num">Renders</th>
|
|
431
|
+
</tr>
|
|
432
|
+
</thead>
|
|
433
|
+
<tbody>${rows}
|
|
434
|
+
</tbody>
|
|
435
|
+
</table>
|
|
436
|
+
</div>
|
|
437
|
+
</figure>
|
|
438
|
+
</section>`;
|
|
439
|
+
}
|
|
440
|
+
const REPORT_STYLES = `
|
|
441
|
+
*,
|
|
442
|
+
*::before,
|
|
443
|
+
*::after {
|
|
228
444
|
box-sizing: border-box;
|
|
229
|
-
|
|
230
|
-
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
html {
|
|
448
|
+
background-color: rgb(var(--canvas));
|
|
449
|
+
scrollbar-color: rgb(var(--line-strong)) transparent;
|
|
450
|
+
scroll-padding-top: 1.5rem;
|
|
231
451
|
}
|
|
232
452
|
|
|
233
453
|
body {
|
|
234
|
-
|
|
235
|
-
background: var(--bg-primary);
|
|
236
|
-
color: var(--text-primary);
|
|
237
|
-
line-height: 1.5;
|
|
454
|
+
margin: 0;
|
|
238
455
|
min-height: 100vh;
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
background-color: rgb(var(--canvas));
|
|
459
|
+
color: rgb(var(--ink));
|
|
460
|
+
font-family: var(--font-sans);
|
|
461
|
+
font-size: 16px;
|
|
462
|
+
line-height: 1.6;
|
|
463
|
+
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1, 'cv11' 1;
|
|
464
|
+
-webkit-font-smoothing: antialiased;
|
|
465
|
+
-moz-osx-font-smoothing: grayscale;
|
|
239
466
|
}
|
|
240
467
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
margin: 0 auto;
|
|
244
|
-
padding: 24px;
|
|
468
|
+
::selection {
|
|
469
|
+
background-color: rgb(var(--accent) / 0.25);
|
|
245
470
|
}
|
|
246
471
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
border: 1px solid var(--border-color);
|
|
251
|
-
border-radius: var(--radius);
|
|
252
|
-
padding: 24px;
|
|
253
|
-
margin-bottom: 24px;
|
|
254
|
-
box-shadow: var(--shadow);
|
|
472
|
+
:focus-visible {
|
|
473
|
+
outline: 2px solid rgb(var(--accent));
|
|
474
|
+
outline-offset: 2px;
|
|
255
475
|
}
|
|
256
476
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
477
|
+
h1,
|
|
478
|
+
h2,
|
|
479
|
+
h3,
|
|
480
|
+
p,
|
|
481
|
+
pre,
|
|
482
|
+
dl,
|
|
483
|
+
dd,
|
|
484
|
+
ol,
|
|
485
|
+
figure {
|
|
486
|
+
margin: 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
ol {
|
|
490
|
+
padding: 0;
|
|
491
|
+
list-style: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sr-only {
|
|
495
|
+
position: absolute;
|
|
496
|
+
width: 1px;
|
|
497
|
+
height: 1px;
|
|
498
|
+
padding: 0;
|
|
499
|
+
margin: -1px;
|
|
500
|
+
overflow: hidden;
|
|
501
|
+
clip: rect(0, 0, 0, 0);
|
|
502
|
+
white-space: nowrap;
|
|
503
|
+
border: 0;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.meta-time,
|
|
507
|
+
.fact dd,
|
|
508
|
+
.stat-value,
|
|
509
|
+
.section-note,
|
|
510
|
+
.phase-order,
|
|
511
|
+
.phase-time,
|
|
512
|
+
.phase-share,
|
|
513
|
+
.counts dd,
|
|
514
|
+
.panel-bar,
|
|
515
|
+
.page-url,
|
|
516
|
+
.num,
|
|
517
|
+
.code {
|
|
518
|
+
font-family: var(--font-mono);
|
|
519
|
+
font-feature-settings: normal;
|
|
520
|
+
font-variant-ligatures: none;
|
|
521
|
+
font-variant-numeric: tabular-nums;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.shell {
|
|
525
|
+
width: 100%;
|
|
526
|
+
max-width: 72rem;
|
|
527
|
+
margin: 0 auto;
|
|
528
|
+
padding-left: 1rem;
|
|
529
|
+
padding-right: 1rem;
|
|
264
530
|
}
|
|
265
531
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
532
|
+
/* Header bar */
|
|
533
|
+
|
|
534
|
+
.bar {
|
|
535
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
270
536
|
}
|
|
271
537
|
|
|
272
|
-
.
|
|
538
|
+
.bar .shell {
|
|
539
|
+
min-height: 3.5rem;
|
|
273
540
|
display: flex;
|
|
274
541
|
flex-wrap: wrap;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
542
|
+
align-items: center;
|
|
543
|
+
justify-content: space-between;
|
|
544
|
+
gap: 0.5rem 1.5rem;
|
|
545
|
+
padding-top: 0.625rem;
|
|
546
|
+
padding-bottom: 0.625rem;
|
|
279
547
|
}
|
|
280
548
|
|
|
281
|
-
.
|
|
549
|
+
.brand {
|
|
282
550
|
display: flex;
|
|
283
551
|
align-items: center;
|
|
284
|
-
gap:
|
|
552
|
+
gap: 0.625rem;
|
|
553
|
+
min-width: 0;
|
|
285
554
|
}
|
|
286
555
|
|
|
287
|
-
.
|
|
288
|
-
width:
|
|
289
|
-
height:
|
|
290
|
-
|
|
556
|
+
.brand-mark {
|
|
557
|
+
width: 1.25rem;
|
|
558
|
+
height: 1.25rem;
|
|
559
|
+
flex: none;
|
|
560
|
+
color: rgb(var(--accent));
|
|
291
561
|
}
|
|
292
562
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
563
|
+
.brand-name {
|
|
564
|
+
font-family: var(--font-display);
|
|
565
|
+
font-size: 1.125rem;
|
|
566
|
+
font-weight: 800;
|
|
567
|
+
font-stretch: 112%;
|
|
568
|
+
letter-spacing: -0.02em;
|
|
569
|
+
line-height: 1;
|
|
299
570
|
}
|
|
300
571
|
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
padding: 20px;
|
|
306
|
-
box-shadow: var(--shadow);
|
|
572
|
+
.brand-rule {
|
|
573
|
+
width: 1px;
|
|
574
|
+
height: 1rem;
|
|
575
|
+
background-color: rgb(var(--line-strong));
|
|
307
576
|
}
|
|
308
577
|
|
|
309
|
-
.
|
|
310
|
-
font-size:
|
|
578
|
+
.brand-context {
|
|
579
|
+
font-size: 0.875rem;
|
|
311
580
|
font-weight: 500;
|
|
312
|
-
color: var(--
|
|
313
|
-
|
|
314
|
-
letter-spacing: 0.5px;
|
|
315
|
-
margin-bottom: 8px;
|
|
581
|
+
color: rgb(var(--muted));
|
|
582
|
+
white-space: nowrap;
|
|
316
583
|
}
|
|
317
584
|
|
|
318
|
-
.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
585
|
+
.bar-nav {
|
|
586
|
+
display: flex;
|
|
587
|
+
flex-wrap: wrap;
|
|
588
|
+
gap: 0.25rem;
|
|
589
|
+
margin: 0 -0.5rem;
|
|
322
590
|
}
|
|
323
591
|
|
|
324
|
-
.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
592
|
+
.bar-nav a {
|
|
593
|
+
padding: 0.375rem 0.5rem;
|
|
594
|
+
border-radius: 6px;
|
|
595
|
+
font-size: 0.875rem;
|
|
596
|
+
font-weight: 500;
|
|
597
|
+
line-height: 1.25;
|
|
598
|
+
color: rgb(var(--muted));
|
|
599
|
+
text-decoration: none;
|
|
600
|
+
transition: color 150ms, background-color 150ms;
|
|
328
601
|
}
|
|
329
602
|
|
|
330
|
-
.
|
|
331
|
-
|
|
332
|
-
|
|
603
|
+
.bar-nav a:hover {
|
|
604
|
+
background-color: rgb(var(--raised));
|
|
605
|
+
color: rgb(var(--ink));
|
|
333
606
|
}
|
|
334
607
|
|
|
335
|
-
|
|
336
|
-
color: var(--accent-green);
|
|
337
|
-
}
|
|
608
|
+
/* Summary */
|
|
338
609
|
|
|
339
|
-
.
|
|
340
|
-
|
|
610
|
+
.main {
|
|
611
|
+
flex: 1;
|
|
612
|
+
padding-top: 2.5rem;
|
|
613
|
+
padding-bottom: 4rem;
|
|
341
614
|
}
|
|
342
615
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
616
|
+
.headline {
|
|
617
|
+
font-family: var(--font-display);
|
|
618
|
+
font-size: 1.875rem;
|
|
619
|
+
font-weight: 800;
|
|
620
|
+
font-stretch: 108%;
|
|
621
|
+
letter-spacing: -0.015em;
|
|
622
|
+
line-height: 1.1;
|
|
623
|
+
text-wrap: balance;
|
|
351
624
|
}
|
|
352
625
|
|
|
353
|
-
.
|
|
626
|
+
.meta {
|
|
354
627
|
display: flex;
|
|
628
|
+
flex-wrap: wrap;
|
|
355
629
|
align-items: center;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
630
|
+
gap: 0.5rem 0.75rem;
|
|
631
|
+
margin-top: 0.875rem;
|
|
632
|
+
font-size: 0.875rem;
|
|
633
|
+
color: rgb(var(--muted));
|
|
360
634
|
}
|
|
361
635
|
|
|
362
|
-
.
|
|
363
|
-
|
|
364
|
-
font-weight: 600;
|
|
365
|
-
display: flex;
|
|
636
|
+
.type-badge {
|
|
637
|
+
display: inline-flex;
|
|
366
638
|
align-items: center;
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
height:
|
|
373
|
-
color: var(--
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
.section-content {
|
|
377
|
-
padding: 20px;
|
|
639
|
+
padding: 0.125rem 0.5rem;
|
|
640
|
+
border: 1px solid rgb(var(--line-strong));
|
|
641
|
+
border-radius: 4px;
|
|
642
|
+
font-size: 0.75rem;
|
|
643
|
+
font-weight: 600;
|
|
644
|
+
line-height: 1.5;
|
|
645
|
+
color: rgb(var(--ink));
|
|
646
|
+
text-transform: capitalize;
|
|
378
647
|
}
|
|
379
648
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
flex-direction: column;
|
|
384
|
-
gap: 8px;
|
|
649
|
+
.meta-time {
|
|
650
|
+
font-size: 0.75rem;
|
|
651
|
+
color: rgb(var(--faint));
|
|
385
652
|
}
|
|
386
653
|
|
|
387
|
-
.
|
|
654
|
+
.stats {
|
|
388
655
|
display: grid;
|
|
389
|
-
grid-template-columns:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
656
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
657
|
+
gap: 1px;
|
|
658
|
+
margin-top: 1.75rem;
|
|
659
|
+
overflow: hidden;
|
|
660
|
+
border: 1px solid rgb(var(--line));
|
|
393
661
|
border-radius: 8px;
|
|
394
|
-
background: var(--
|
|
395
|
-
cursor: pointer;
|
|
396
|
-
transition: all 0.2s ease;
|
|
662
|
+
background-color: rgb(var(--line));
|
|
397
663
|
}
|
|
398
664
|
|
|
399
|
-
.
|
|
400
|
-
|
|
401
|
-
|
|
665
|
+
.stat {
|
|
666
|
+
min-width: 0;
|
|
667
|
+
padding: 1rem 1.25rem;
|
|
668
|
+
background-color: rgb(var(--canvas));
|
|
402
669
|
}
|
|
403
670
|
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
671
|
+
.stat dt {
|
|
672
|
+
font-size: 0.8125rem;
|
|
673
|
+
color: rgb(var(--muted));
|
|
407
674
|
}
|
|
408
675
|
|
|
409
|
-
.
|
|
410
|
-
|
|
676
|
+
.stat-value {
|
|
677
|
+
margin-top: 0.25rem;
|
|
678
|
+
font-size: 1.5rem;
|
|
411
679
|
font-weight: 500;
|
|
412
|
-
|
|
680
|
+
line-height: 1.25;
|
|
681
|
+
letter-spacing: -0.02em;
|
|
682
|
+
color: rgb(var(--ink));
|
|
683
|
+
overflow-wrap: anywhere;
|
|
413
684
|
}
|
|
414
685
|
|
|
415
|
-
.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
position: relative;
|
|
686
|
+
.stat-note {
|
|
687
|
+
margin-top: 0.25rem;
|
|
688
|
+
font-size: 0.8125rem;
|
|
689
|
+
line-height: 1.5;
|
|
690
|
+
color: rgb(var(--faint));
|
|
421
691
|
}
|
|
422
692
|
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
min-width: 2px;
|
|
693
|
+
.facts {
|
|
694
|
+
display: flex;
|
|
695
|
+
flex-wrap: wrap;
|
|
696
|
+
gap: 0.75rem 2.5rem;
|
|
697
|
+
margin-top: 1.25rem;
|
|
429
698
|
}
|
|
430
699
|
|
|
431
|
-
.
|
|
432
|
-
|
|
433
|
-
font-weight: 600;
|
|
434
|
-
color: var(--text-primary);
|
|
435
|
-
text-align: right;
|
|
436
|
-
font-variant-numeric: tabular-nums;
|
|
700
|
+
.fact {
|
|
701
|
+
min-width: 0;
|
|
437
702
|
}
|
|
438
703
|
|
|
439
|
-
.
|
|
440
|
-
font-size:
|
|
441
|
-
color: var(--
|
|
442
|
-
text-align: right;
|
|
443
|
-
font-variant-numeric: tabular-nums;
|
|
704
|
+
.fact dt {
|
|
705
|
+
font-size: 0.8125rem;
|
|
706
|
+
color: rgb(var(--faint));
|
|
444
707
|
}
|
|
445
708
|
|
|
446
|
-
.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
709
|
+
.fact dd {
|
|
710
|
+
margin-top: 0.125rem;
|
|
711
|
+
font-size: 0.8125rem;
|
|
712
|
+
overflow-wrap: anywhere;
|
|
713
|
+
color: rgb(var(--ink));
|
|
451
714
|
}
|
|
452
715
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
716
|
+
/* Sections */
|
|
717
|
+
|
|
718
|
+
.section {
|
|
719
|
+
margin-top: 2.5rem;
|
|
720
|
+
padding-top: 1.75rem;
|
|
721
|
+
border-top: 1px solid rgb(var(--line));
|
|
456
722
|
}
|
|
457
723
|
|
|
458
|
-
.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
font-size: 13px;
|
|
465
|
-
line-height: 1.6;
|
|
466
|
-
color: var(--text-secondary);
|
|
467
|
-
display: none;
|
|
724
|
+
.section-head {
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-wrap: wrap;
|
|
727
|
+
align-items: baseline;
|
|
728
|
+
justify-content: space-between;
|
|
729
|
+
gap: 0.25rem 1rem;
|
|
468
730
|
}
|
|
469
731
|
|
|
470
|
-
.
|
|
471
|
-
|
|
732
|
+
.section-title {
|
|
733
|
+
font-family: var(--font-display);
|
|
734
|
+
font-size: 1.125rem;
|
|
735
|
+
font-weight: 700;
|
|
736
|
+
font-stretch: 108%;
|
|
737
|
+
letter-spacing: -0.01em;
|
|
738
|
+
line-height: 1.25;
|
|
472
739
|
}
|
|
473
740
|
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
border-radius: 4px;
|
|
478
|
-
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
479
|
-
font-size: 12px;
|
|
480
|
-
color: var(--accent-cyan);
|
|
741
|
+
.section-note {
|
|
742
|
+
font-size: 0.75rem;
|
|
743
|
+
color: rgb(var(--faint));
|
|
481
744
|
}
|
|
482
745
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
position: relative;
|
|
486
|
-
padding: 20px 0;
|
|
746
|
+
.section > .section-title + * {
|
|
747
|
+
margin-top: 1rem;
|
|
487
748
|
}
|
|
488
749
|
|
|
489
|
-
.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
position: relative;
|
|
494
|
-
overflow: hidden;
|
|
495
|
-
margin-bottom: 16px;
|
|
750
|
+
.empty {
|
|
751
|
+
margin-top: 1rem;
|
|
752
|
+
font-size: 0.875rem;
|
|
753
|
+
color: rgb(var(--muted));
|
|
496
754
|
}
|
|
497
755
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
756
|
+
/* Tabs */
|
|
757
|
+
|
|
758
|
+
.tabs {
|
|
501
759
|
display: flex;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
760
|
+
gap: 1.5rem;
|
|
761
|
+
margin-top: 1rem;
|
|
762
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.tab {
|
|
766
|
+
margin-bottom: -1px;
|
|
767
|
+
padding: 0.5rem 0;
|
|
768
|
+
border: 0;
|
|
769
|
+
border-radius: 0;
|
|
770
|
+
background: none;
|
|
771
|
+
font: inherit;
|
|
772
|
+
font-size: 0.875rem;
|
|
505
773
|
font-weight: 500;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
overflow: hidden;
|
|
774
|
+
line-height: 1.5;
|
|
775
|
+
color: rgb(var(--muted));
|
|
509
776
|
cursor: pointer;
|
|
510
|
-
transition:
|
|
511
|
-
min-width: 2px;
|
|
777
|
+
transition: color 150ms;
|
|
512
778
|
}
|
|
513
779
|
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
z-index: 10;
|
|
780
|
+
.tab:hover {
|
|
781
|
+
color: rgb(var(--ink));
|
|
517
782
|
}
|
|
518
783
|
|
|
519
|
-
.
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
text-overflow: ellipsis;
|
|
523
|
-
padding: 0 4px;
|
|
784
|
+
.tab[aria-selected='true'] {
|
|
785
|
+
color: rgb(var(--ink));
|
|
786
|
+
box-shadow: inset 0 -2px 0 rgb(var(--accent));
|
|
524
787
|
}
|
|
525
788
|
|
|
526
|
-
.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
789
|
+
.view-intro {
|
|
790
|
+
max-width: 40rem;
|
|
791
|
+
margin-top: 1rem;
|
|
792
|
+
font-size: 0.875rem;
|
|
793
|
+
color: rgb(var(--muted));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* Phases */
|
|
797
|
+
|
|
798
|
+
.phases {
|
|
799
|
+
--order: 0rem;
|
|
800
|
+
margin-top: 1rem;
|
|
801
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
530
802
|
}
|
|
531
803
|
|
|
532
|
-
.timeline
|
|
804
|
+
.phases[data-view='timeline'] {
|
|
805
|
+
--order: 2.75rem;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.phase-row {
|
|
533
809
|
display: grid;
|
|
534
|
-
grid-template-columns:
|
|
810
|
+
grid-template-columns: minmax(0, 13rem) minmax(0, 1fr) 5.5rem 3.25rem 1rem;
|
|
811
|
+
grid-template-areas: 'name meter time share icon';
|
|
535
812
|
align-items: center;
|
|
536
|
-
gap:
|
|
537
|
-
padding:
|
|
538
|
-
border-
|
|
539
|
-
background: var(--bg-tertiary);
|
|
813
|
+
column-gap: 1rem;
|
|
814
|
+
padding: 0.625rem 0;
|
|
815
|
+
border-top: 1px solid rgb(var(--line));
|
|
540
816
|
cursor: pointer;
|
|
541
|
-
|
|
817
|
+
list-style: none;
|
|
542
818
|
}
|
|
543
819
|
|
|
544
|
-
.
|
|
545
|
-
|
|
820
|
+
.phase-row::-webkit-details-marker {
|
|
821
|
+
display: none;
|
|
546
822
|
}
|
|
547
823
|
|
|
548
|
-
.
|
|
549
|
-
|
|
550
|
-
box-shadow: 0 0 0 1px var(--accent-blue);
|
|
824
|
+
.phases > .phase:first-child > details > .phase-row {
|
|
825
|
+
border-top-color: rgb(var(--ink));
|
|
551
826
|
}
|
|
552
827
|
|
|
553
|
-
.timeline-
|
|
554
|
-
|
|
555
|
-
|
|
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);
|
|
828
|
+
.phases[data-view='timeline'] .phase-row {
|
|
829
|
+
grid-template-columns: 1.75rem minmax(0, 13rem) minmax(0, 1fr) 5.5rem 3.25rem 1rem;
|
|
830
|
+
grid-template-areas: 'order name meter time share icon';
|
|
564
831
|
}
|
|
565
832
|
|
|
566
|
-
.
|
|
567
|
-
|
|
568
|
-
|
|
833
|
+
.phase-order {
|
|
834
|
+
grid-area: order;
|
|
835
|
+
font-size: 0.75rem;
|
|
836
|
+
color: rgb(var(--faint));
|
|
837
|
+
}
|
|
569
838
|
|
|
570
|
-
.
|
|
571
|
-
|
|
839
|
+
.phase-name {
|
|
840
|
+
grid-area: name;
|
|
841
|
+
min-width: 0;
|
|
842
|
+
font-size: 0.875rem;
|
|
572
843
|
font-weight: 500;
|
|
573
|
-
|
|
844
|
+
line-height: 1.4;
|
|
845
|
+
overflow-wrap: anywhere;
|
|
846
|
+
transition: color 150ms;
|
|
574
847
|
}
|
|
575
848
|
|
|
576
|
-
.
|
|
577
|
-
|
|
578
|
-
background: rgba(0,0,0,0.3);
|
|
579
|
-
border-radius: 4px;
|
|
580
|
-
overflow: hidden;
|
|
581
|
-
position: relative;
|
|
849
|
+
.phase-row:hover .phase-name {
|
|
850
|
+
color: rgb(var(--accent-ink));
|
|
582
851
|
}
|
|
583
852
|
|
|
584
|
-
.
|
|
585
|
-
|
|
586
|
-
border-radius: 4px;
|
|
587
|
-
min-width: 2px;
|
|
853
|
+
.phase-row .meter {
|
|
854
|
+
grid-area: meter;
|
|
588
855
|
}
|
|
589
856
|
|
|
590
|
-
.
|
|
591
|
-
|
|
592
|
-
font-
|
|
593
|
-
color: var(--text-primary);
|
|
857
|
+
.phase-time {
|
|
858
|
+
grid-area: time;
|
|
859
|
+
font-size: 0.8125rem;
|
|
594
860
|
text-align: right;
|
|
595
|
-
|
|
861
|
+
white-space: nowrap;
|
|
596
862
|
}
|
|
597
863
|
|
|
598
|
-
.
|
|
599
|
-
grid-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
border-radius: 6px;
|
|
604
|
-
font-size: 12px;
|
|
605
|
-
line-height: 1.6;
|
|
606
|
-
color: var(--text-secondary);
|
|
607
|
-
display: none;
|
|
864
|
+
.phase-share {
|
|
865
|
+
grid-area: share;
|
|
866
|
+
font-size: 0.75rem;
|
|
867
|
+
text-align: right;
|
|
868
|
+
color: rgb(var(--faint));
|
|
608
869
|
}
|
|
609
870
|
|
|
610
|
-
.
|
|
611
|
-
|
|
871
|
+
.chevron {
|
|
872
|
+
grid-area: icon;
|
|
873
|
+
width: 1rem;
|
|
874
|
+
height: 1rem;
|
|
875
|
+
color: rgb(var(--faint));
|
|
876
|
+
transition: transform 150ms, color 150ms;
|
|
612
877
|
}
|
|
613
878
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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); }
|
|
879
|
+
.phase-row:hover .chevron {
|
|
880
|
+
color: rgb(var(--ink));
|
|
881
|
+
}
|
|
623
882
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
overflow-x: auto;
|
|
627
|
-
margin-top: 16px;
|
|
883
|
+
details[open] > .phase-row .chevron {
|
|
884
|
+
transform: rotate(90deg);
|
|
628
885
|
}
|
|
629
886
|
|
|
630
|
-
.
|
|
631
|
-
|
|
887
|
+
.phase-description {
|
|
888
|
+
max-width: 42rem;
|
|
889
|
+
padding: 0 0 1rem var(--order);
|
|
890
|
+
font-size: 0.875rem;
|
|
891
|
+
line-height: 1.65;
|
|
892
|
+
color: rgb(var(--muted));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.phase-step {
|
|
632
896
|
position: relative;
|
|
633
897
|
}
|
|
634
898
|
|
|
635
|
-
.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
899
|
+
.phase-step::before {
|
|
900
|
+
content: '';
|
|
901
|
+
position: absolute;
|
|
902
|
+
top: 0;
|
|
903
|
+
bottom: 0;
|
|
904
|
+
left: var(--order);
|
|
905
|
+
width: 1px;
|
|
906
|
+
background-color: rgb(var(--line-strong));
|
|
641
907
|
}
|
|
642
908
|
|
|
643
|
-
.
|
|
644
|
-
border-
|
|
909
|
+
.phase-step .phase-row {
|
|
910
|
+
border-top: 0;
|
|
645
911
|
}
|
|
646
912
|
|
|
647
|
-
.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
color: var(--text-secondary);
|
|
652
|
-
overflow: hidden;
|
|
653
|
-
text-overflow: ellipsis;
|
|
654
|
-
white-space: nowrap;
|
|
913
|
+
.phase-step .phase-name {
|
|
914
|
+
padding-left: 0.875rem;
|
|
915
|
+
font-weight: 400;
|
|
916
|
+
color: rgb(var(--muted));
|
|
655
917
|
}
|
|
656
918
|
|
|
657
|
-
.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
919
|
+
.phase-step .phase-description {
|
|
920
|
+
padding-left: calc(var(--order) + 0.875rem);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Bars */
|
|
924
|
+
|
|
925
|
+
.meter {
|
|
662
926
|
position: relative;
|
|
927
|
+
display: block;
|
|
928
|
+
height: 0.375rem;
|
|
929
|
+
overflow: hidden;
|
|
930
|
+
border-radius: 2px;
|
|
931
|
+
background-color: rgb(var(--line));
|
|
663
932
|
}
|
|
664
933
|
|
|
665
|
-
.
|
|
934
|
+
.meter-fill {
|
|
666
935
|
position: absolute;
|
|
667
|
-
|
|
936
|
+
top: 0;
|
|
937
|
+
bottom: 0;
|
|
938
|
+
min-width: 2px;
|
|
668
939
|
border-radius: 2px;
|
|
669
|
-
|
|
940
|
+
background-color: rgb(var(--ink));
|
|
670
941
|
}
|
|
671
942
|
|
|
672
|
-
.
|
|
673
|
-
background: var(--
|
|
674
|
-
opacity: 0.6;
|
|
943
|
+
.phase-step .meter-fill {
|
|
944
|
+
background-color: rgb(var(--muted));
|
|
675
945
|
}
|
|
676
946
|
|
|
677
|
-
|
|
678
|
-
background: var(--accent-blue);
|
|
679
|
-
}
|
|
947
|
+
/* Counts */
|
|
680
948
|
|
|
681
|
-
.
|
|
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 {
|
|
949
|
+
.groups {
|
|
692
950
|
display: grid;
|
|
693
|
-
grid-template-columns:
|
|
694
|
-
gap:
|
|
951
|
+
grid-template-columns: minmax(0, 1fr);
|
|
952
|
+
gap: 2rem 2.5rem;
|
|
953
|
+
margin-top: 1.25rem;
|
|
695
954
|
}
|
|
696
955
|
|
|
697
|
-
.
|
|
956
|
+
.group-title {
|
|
698
957
|
display: flex;
|
|
699
|
-
|
|
700
|
-
gap:
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
font-size: 12px;
|
|
705
|
-
color: var(--text-secondary);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
.stat-value {
|
|
709
|
-
font-size: 18px;
|
|
958
|
+
justify-content: space-between;
|
|
959
|
+
gap: 1rem;
|
|
960
|
+
padding-bottom: 0.5rem;
|
|
961
|
+
border-bottom: 1px solid rgb(var(--ink));
|
|
962
|
+
font-size: 0.875rem;
|
|
710
963
|
font-weight: 600;
|
|
711
|
-
|
|
712
|
-
font-variant-numeric: tabular-nums;
|
|
964
|
+
line-height: 1.25;
|
|
713
965
|
}
|
|
714
966
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
719
|
-
gap: 20px;
|
|
967
|
+
.group-status {
|
|
968
|
+
font-weight: 400;
|
|
969
|
+
color: rgb(var(--muted));
|
|
720
970
|
}
|
|
721
971
|
|
|
722
|
-
.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
972
|
+
.count {
|
|
973
|
+
display: flex;
|
|
974
|
+
align-items: baseline;
|
|
975
|
+
justify-content: space-between;
|
|
976
|
+
gap: 1rem;
|
|
977
|
+
padding: 0.5rem 0;
|
|
978
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
979
|
+
font-size: 0.875rem;
|
|
727
980
|
}
|
|
728
981
|
|
|
729
|
-
.
|
|
730
|
-
|
|
731
|
-
font-weight: 700;
|
|
982
|
+
.count dt {
|
|
983
|
+
color: rgb(var(--muted));
|
|
732
984
|
}
|
|
733
985
|
|
|
734
|
-
.
|
|
735
|
-
font-size:
|
|
736
|
-
color: var(--
|
|
737
|
-
margin-top: 4px;
|
|
986
|
+
.counts dd {
|
|
987
|
+
font-size: 0.8125rem;
|
|
988
|
+
color: rgb(var(--ink));
|
|
738
989
|
}
|
|
739
990
|
|
|
740
|
-
|
|
741
|
-
color: var(--accent-green);
|
|
742
|
-
}
|
|
991
|
+
/* Panels */
|
|
743
992
|
|
|
744
|
-
.
|
|
745
|
-
|
|
993
|
+
.panel {
|
|
994
|
+
overflow: hidden;
|
|
995
|
+
border: 1px solid rgb(var(--line));
|
|
996
|
+
border-radius: 8px;
|
|
997
|
+
background-color: rgb(var(--sunken));
|
|
746
998
|
}
|
|
747
999
|
|
|
748
|
-
|
|
749
|
-
.cache-ring-container {
|
|
1000
|
+
.panel-bar {
|
|
750
1001
|
display: flex;
|
|
751
|
-
|
|
1002
|
+
flex-wrap: wrap;
|
|
752
1003
|
align-items: center;
|
|
753
|
-
|
|
1004
|
+
justify-content: space-between;
|
|
1005
|
+
gap: 0.25rem 1rem;
|
|
1006
|
+
min-height: 2.5rem;
|
|
1007
|
+
padding: 0.375rem 1rem;
|
|
1008
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
1009
|
+
background-color: rgb(var(--canvas));
|
|
1010
|
+
font-size: 0.75rem;
|
|
1011
|
+
line-height: 1.5;
|
|
1012
|
+
color: rgb(var(--faint));
|
|
754
1013
|
}
|
|
755
1014
|
|
|
756
|
-
.
|
|
757
|
-
|
|
758
|
-
width: 150px;
|
|
759
|
-
height: 150px;
|
|
1015
|
+
.panel-summary {
|
|
1016
|
+
font-family: var(--font-sans);
|
|
760
1017
|
}
|
|
761
1018
|
|
|
762
|
-
.
|
|
763
|
-
|
|
1019
|
+
.table-scroll {
|
|
1020
|
+
max-height: 32rem;
|
|
1021
|
+
overflow: auto;
|
|
764
1022
|
}
|
|
765
1023
|
|
|
766
|
-
.
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1024
|
+
.pages {
|
|
1025
|
+
width: 100%;
|
|
1026
|
+
border-collapse: collapse;
|
|
1027
|
+
font-size: 0.8125rem;
|
|
770
1028
|
}
|
|
771
1029
|
|
|
772
|
-
.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1030
|
+
.pages th {
|
|
1031
|
+
position: sticky;
|
|
1032
|
+
top: 0;
|
|
1033
|
+
z-index: 1;
|
|
1034
|
+
padding: 0.5rem 1rem;
|
|
1035
|
+
border-bottom: 1px solid rgb(var(--line));
|
|
1036
|
+
background-color: rgb(var(--sunken));
|
|
1037
|
+
font-size: 0.75rem;
|
|
1038
|
+
font-weight: 600;
|
|
1039
|
+
text-align: left;
|
|
1040
|
+
white-space: nowrap;
|
|
1041
|
+
color: rgb(var(--muted));
|
|
778
1042
|
}
|
|
779
1043
|
|
|
780
|
-
.
|
|
781
|
-
|
|
782
|
-
top:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
text-align: center;
|
|
1044
|
+
.pages td {
|
|
1045
|
+
padding: 0.5rem 1rem;
|
|
1046
|
+
border-top: 1px solid rgb(var(--line));
|
|
1047
|
+
vertical-align: middle;
|
|
1048
|
+
white-space: nowrap;
|
|
786
1049
|
}
|
|
787
1050
|
|
|
788
|
-
.
|
|
789
|
-
|
|
790
|
-
font-weight: 700;
|
|
791
|
-
color: var(--accent-green);
|
|
1051
|
+
.pages tbody tr:first-child td {
|
|
1052
|
+
border-top: 0;
|
|
792
1053
|
}
|
|
793
1054
|
|
|
794
|
-
.
|
|
795
|
-
|
|
796
|
-
color: var(--text-secondary);
|
|
1055
|
+
.pages .num {
|
|
1056
|
+
text-align: right;
|
|
797
1057
|
}
|
|
798
1058
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
width: 100%;
|
|
802
|
-
border-collapse: collapse;
|
|
803
|
-
font-size: 13px;
|
|
1059
|
+
.page-url {
|
|
1060
|
+
color: rgb(var(--ink));
|
|
804
1061
|
}
|
|
805
1062
|
|
|
806
|
-
.page-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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);
|
|
1063
|
+
.page-url span {
|
|
1064
|
+
display: block;
|
|
1065
|
+
max-width: 26rem;
|
|
1066
|
+
overflow: hidden;
|
|
1067
|
+
text-overflow: ellipsis;
|
|
816
1068
|
}
|
|
817
1069
|
|
|
818
|
-
.
|
|
819
|
-
|
|
820
|
-
|
|
1070
|
+
.status {
|
|
1071
|
+
display: inline-flex;
|
|
1072
|
+
align-items: center;
|
|
1073
|
+
gap: 0.5rem;
|
|
821
1074
|
vertical-align: middle;
|
|
822
1075
|
}
|
|
823
1076
|
|
|
824
|
-
.
|
|
825
|
-
|
|
1077
|
+
.status-rendered {
|
|
1078
|
+
color: rgb(var(--ink));
|
|
826
1079
|
}
|
|
827
1080
|
|
|
828
|
-
.
|
|
829
|
-
|
|
830
|
-
font-size: 12px;
|
|
831
|
-
color: var(--accent-blue);
|
|
832
|
-
max-width: 400px;
|
|
833
|
-
overflow: hidden;
|
|
834
|
-
text-overflow: ellipsis;
|
|
835
|
-
white-space: nowrap;
|
|
1081
|
+
.status-cached {
|
|
1082
|
+
color: rgb(var(--faint));
|
|
836
1083
|
}
|
|
837
1084
|
|
|
838
|
-
.
|
|
839
|
-
|
|
840
|
-
|
|
1085
|
+
.marker {
|
|
1086
|
+
width: 0.375rem;
|
|
1087
|
+
height: 0.375rem;
|
|
1088
|
+
flex: none;
|
|
841
1089
|
}
|
|
842
1090
|
|
|
843
|
-
.
|
|
844
|
-
|
|
845
|
-
align-items: center;
|
|
846
|
-
gap: 6px;
|
|
847
|
-
padding: 4px 10px;
|
|
848
|
-
border-radius: 20px;
|
|
849
|
-
font-size: 11px;
|
|
850
|
-
font-weight: 500;
|
|
1091
|
+
.status-rendered .marker {
|
|
1092
|
+
background-color: rgb(var(--ink));
|
|
851
1093
|
}
|
|
852
1094
|
|
|
853
|
-
.
|
|
854
|
-
|
|
855
|
-
|
|
1095
|
+
.status-cached .marker {
|
|
1096
|
+
border: 1px solid currentColor;
|
|
1097
|
+
border-radius: 50%;
|
|
856
1098
|
}
|
|
857
1099
|
|
|
858
|
-
.page-
|
|
859
|
-
|
|
860
|
-
|
|
1100
|
+
.page-duration {
|
|
1101
|
+
width: 40%;
|
|
1102
|
+
text-align: right;
|
|
861
1103
|
}
|
|
862
1104
|
|
|
863
|
-
|
|
864
|
-
.tabs {
|
|
1105
|
+
.duration {
|
|
865
1106
|
display: flex;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
background: var(--bg-tertiary);
|
|
869
|
-
border-radius: var(--radius);
|
|
870
|
-
margin-bottom: 16px;
|
|
1107
|
+
align-items: center;
|
|
1108
|
+
gap: 0.75rem;
|
|
871
1109
|
}
|
|
872
1110
|
|
|
873
|
-
.
|
|
874
|
-
|
|
875
|
-
|
|
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;
|
|
1111
|
+
.duration .meter {
|
|
1112
|
+
flex: 1;
|
|
1113
|
+
min-width: 6rem;
|
|
883
1114
|
}
|
|
884
1115
|
|
|
885
|
-
.
|
|
886
|
-
|
|
887
|
-
|
|
1116
|
+
.duration .num {
|
|
1117
|
+
min-width: 4.75rem;
|
|
1118
|
+
text-align: right;
|
|
888
1119
|
}
|
|
889
1120
|
|
|
890
|
-
.
|
|
891
|
-
color:
|
|
892
|
-
background: var(--bg-secondary);
|
|
893
|
-
box-shadow: var(--shadow);
|
|
1121
|
+
.meter-empty {
|
|
1122
|
+
background-color: transparent;
|
|
894
1123
|
}
|
|
895
1124
|
|
|
896
|
-
|
|
897
|
-
display: none;
|
|
898
|
-
}
|
|
1125
|
+
/* Raw JSON */
|
|
899
1126
|
|
|
900
|
-
.
|
|
901
|
-
display:
|
|
1127
|
+
.disclosure summary {
|
|
1128
|
+
display: flex;
|
|
1129
|
+
align-items: center;
|
|
1130
|
+
gap: 0.75rem;
|
|
1131
|
+
width: fit-content;
|
|
1132
|
+
cursor: pointer;
|
|
1133
|
+
list-style: none;
|
|
1134
|
+
border-radius: 4px;
|
|
902
1135
|
}
|
|
903
1136
|
|
|
904
|
-
.
|
|
1137
|
+
.disclosure summary::-webkit-details-marker {
|
|
905
1138
|
display: none;
|
|
906
1139
|
}
|
|
907
1140
|
|
|
908
|
-
.
|
|
909
|
-
|
|
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;
|
|
1141
|
+
.disclosure summary:hover .section-title {
|
|
1142
|
+
color: rgb(var(--accent-ink));
|
|
926
1143
|
}
|
|
927
1144
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
display: grid;
|
|
931
|
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
932
|
-
gap: 20px;
|
|
1145
|
+
.disclosure[open] summary .chevron {
|
|
1146
|
+
transform: rotate(90deg);
|
|
933
1147
|
}
|
|
934
1148
|
|
|
935
|
-
.
|
|
936
|
-
|
|
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;
|
|
1149
|
+
.disclosure .panel {
|
|
1150
|
+
margin-top: 1rem;
|
|
943
1151
|
}
|
|
944
1152
|
|
|
945
|
-
|
|
946
|
-
.badge {
|
|
1153
|
+
.copy-button {
|
|
947
1154
|
display: inline-flex;
|
|
948
1155
|
align-items: center;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
border
|
|
952
|
-
|
|
1156
|
+
padding: 0.25rem 0.5rem;
|
|
1157
|
+
margin-right: -0.5rem;
|
|
1158
|
+
border: 0;
|
|
1159
|
+
border-radius: 6px;
|
|
1160
|
+
background: none;
|
|
1161
|
+
font: inherit;
|
|
1162
|
+
font-family: var(--font-sans);
|
|
1163
|
+
font-size: 0.75rem;
|
|
953
1164
|
font-weight: 500;
|
|
1165
|
+
color: rgb(var(--muted));
|
|
1166
|
+
cursor: pointer;
|
|
1167
|
+
transition: color 150ms, background-color 150ms;
|
|
954
1168
|
}
|
|
955
1169
|
|
|
956
|
-
.
|
|
957
|
-
background:
|
|
958
|
-
color: var(--
|
|
1170
|
+
.copy-button:hover {
|
|
1171
|
+
background-color: rgb(var(--raised));
|
|
1172
|
+
color: rgb(var(--ink));
|
|
959
1173
|
}
|
|
960
1174
|
|
|
961
|
-
.
|
|
962
|
-
|
|
963
|
-
color: var(--accent-blue);
|
|
1175
|
+
.copy-button[data-copied='true'] {
|
|
1176
|
+
color: rgb(var(--accent-ink));
|
|
964
1177
|
}
|
|
965
1178
|
|
|
966
|
-
.
|
|
967
|
-
|
|
968
|
-
|
|
1179
|
+
.code {
|
|
1180
|
+
max-height: 36rem;
|
|
1181
|
+
overflow: auto;
|
|
1182
|
+
padding: 1rem 1.25rem;
|
|
1183
|
+
font-size: 0.8125rem;
|
|
1184
|
+
line-height: 1.65;
|
|
1185
|
+
tab-size: 2;
|
|
1186
|
+
color: rgb(var(--ink));
|
|
969
1187
|
}
|
|
970
1188
|
|
|
971
|
-
/*
|
|
972
|
-
.flags {
|
|
973
|
-
display: flex;
|
|
974
|
-
gap: 8px;
|
|
975
|
-
flex-wrap: wrap;
|
|
976
|
-
}
|
|
1189
|
+
/* Footer */
|
|
977
1190
|
|
|
978
|
-
.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
padding: 4px 10px;
|
|
983
|
-
background: var(--bg-tertiary);
|
|
984
|
-
border-radius: 4px;
|
|
985
|
-
font-size: 12px;
|
|
986
|
-
color: var(--text-secondary);
|
|
1191
|
+
.footer {
|
|
1192
|
+
border-top: 1px solid rgb(var(--line));
|
|
1193
|
+
font-size: 0.8125rem;
|
|
1194
|
+
color: rgb(var(--faint));
|
|
987
1195
|
}
|
|
988
1196
|
|
|
989
|
-
.
|
|
990
|
-
|
|
1197
|
+
.footer .shell {
|
|
1198
|
+
display: flex;
|
|
1199
|
+
flex-wrap: wrap;
|
|
1200
|
+
gap: 0.25rem 0.75rem;
|
|
1201
|
+
padding-top: 1rem;
|
|
1202
|
+
padding-bottom: 1rem;
|
|
991
1203
|
}
|
|
992
1204
|
|
|
993
|
-
.
|
|
994
|
-
|
|
1205
|
+
.footer-name {
|
|
1206
|
+
font-weight: 600;
|
|
1207
|
+
color: rgb(var(--muted));
|
|
995
1208
|
}
|
|
996
1209
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1210
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1211
|
+
.main {
|
|
1212
|
+
animation: report-in 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
1213
|
+
}
|
|
1002
1214
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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);
|
|
1215
|
+
@keyframes report-in {
|
|
1216
|
+
from {
|
|
1217
|
+
opacity: 0;
|
|
1218
|
+
transform: translateY(6px);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1018
1221
|
}
|
|
1019
1222
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1223
|
+
@media (max-width: 639px) {
|
|
1224
|
+
.phase-row {
|
|
1225
|
+
grid-template-columns: minmax(0, 1fr) auto auto 1rem;
|
|
1226
|
+
grid-template-areas:
|
|
1227
|
+
'name time share icon'
|
|
1228
|
+
'meter meter meter meter';
|
|
1229
|
+
row-gap: 0.5rem;
|
|
1024
1230
|
}
|
|
1025
1231
|
|
|
1026
|
-
.phase-
|
|
1027
|
-
grid-template-columns: 1fr;
|
|
1028
|
-
|
|
1232
|
+
.phases[data-view='timeline'] .phase-row {
|
|
1233
|
+
grid-template-columns: 1.75rem minmax(0, 1fr) auto auto 1rem;
|
|
1234
|
+
grid-template-areas:
|
|
1235
|
+
'order name time share icon'
|
|
1236
|
+
'. meter meter meter meter';
|
|
1029
1237
|
}
|
|
1030
1238
|
|
|
1031
|
-
.phase-
|
|
1032
|
-
|
|
1033
|
-
text-align: left;
|
|
1239
|
+
.phase-step .meter {
|
|
1240
|
+
margin-left: 0.875rem;
|
|
1034
1241
|
}
|
|
1035
1242
|
|
|
1036
|
-
.
|
|
1037
|
-
|
|
1038
|
-
|
|
1243
|
+
.duration .meter {
|
|
1244
|
+
display: none;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.page-duration {
|
|
1248
|
+
width: auto;
|
|
1039
1249
|
}
|
|
1040
1250
|
}
|
|
1041
1251
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
color: black;
|
|
1252
|
+
@media (min-width: 640px) {
|
|
1253
|
+
.shell {
|
|
1254
|
+
padding-left: 1.5rem;
|
|
1255
|
+
padding-right: 1.5rem;
|
|
1047
1256
|
}
|
|
1048
1257
|
|
|
1049
|
-
.
|
|
1050
|
-
|
|
1258
|
+
.main {
|
|
1259
|
+
padding-top: 3.5rem;
|
|
1051
1260
|
}
|
|
1052
1261
|
|
|
1053
|
-
.
|
|
1054
|
-
|
|
1262
|
+
.headline {
|
|
1263
|
+
font-size: 2.25rem;
|
|
1055
1264
|
}
|
|
1056
1265
|
|
|
1057
|
-
.
|
|
1058
|
-
|
|
1266
|
+
.groups {
|
|
1267
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1059
1268
|
}
|
|
1060
1269
|
}
|
|
1061
1270
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1271
|
+
@media (min-width: 1024px) {
|
|
1272
|
+
.shell {
|
|
1273
|
+
padding-left: 3rem;
|
|
1274
|
+
padding-right: 3rem;
|
|
1275
|
+
}
|
|
1067
1276
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1277
|
+
.headline {
|
|
1278
|
+
font-size: 2.75rem;
|
|
1279
|
+
}
|
|
1071
1280
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1281
|
+
.stats,
|
|
1282
|
+
.groups {
|
|
1283
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1284
|
+
}
|
|
1074
1285
|
}
|
|
1075
1286
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1287
|
+
@media print {
|
|
1288
|
+
.bar-nav,
|
|
1289
|
+
.copy-button {
|
|
1290
|
+
display: none;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.table-scroll,
|
|
1294
|
+
.code {
|
|
1295
|
+
max-height: none;
|
|
1296
|
+
overflow: visible;
|
|
1297
|
+
}
|
|
1298
|
+
}`;
|
|
1299
|
+
/**
|
|
1300
|
+
* Tabs that switch the phase views, with the arrow keys moving between them, and copying the
|
|
1301
|
+
* raw JSON.
|
|
1302
|
+
*/
|
|
1303
|
+
const REPORT_SCRIPT = `
|
|
1304
|
+
(function () {
|
|
1305
|
+
document.querySelectorAll('[role="tablist"]').forEach(function (list) {
|
|
1306
|
+
var tabs = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
|
|
1307
|
+
|
|
1308
|
+
function select(tab) {
|
|
1309
|
+
tabs.forEach(function (other) {
|
|
1310
|
+
var selected = other === tab;
|
|
1311
|
+
other.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
1312
|
+
other.tabIndex = selected ? 0 : -1;
|
|
1313
|
+
document.getElementById(other.getAttribute('aria-controls')).hidden = !selected;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
tabs.forEach(function (tab, index) {
|
|
1318
|
+
tab.addEventListener('click', function () {
|
|
1319
|
+
select(tab);
|
|
1320
|
+
});
|
|
1321
|
+
tab.addEventListener('keydown', function (event) {
|
|
1322
|
+
var step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
1323
|
+
if (!step) return;
|
|
1324
|
+
event.preventDefault();
|
|
1325
|
+
var next = tabs[(index + step + tabs.length) % tabs.length];
|
|
1326
|
+
select(next);
|
|
1327
|
+
next.focus();
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// The JSON section starts closed; following a link to it opens it
|
|
1333
|
+
var json = document.getElementById('raw-json-data');
|
|
1334
|
+
document.querySelectorAll('a[href="#raw-json"]').forEach(function (link) {
|
|
1335
|
+
link.addEventListener('click', function () {
|
|
1336
|
+
json.open = true;
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
var copyButton = document.getElementById('copy-json');
|
|
1341
|
+
var copyStatus = document.getElementById('copy-status');
|
|
1342
|
+
if (!navigator.clipboard) {
|
|
1343
|
+
copyButton.hidden = true;
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
copyButton.addEventListener('click', function () {
|
|
1347
|
+
var text = document.getElementById('json-content').textContent;
|
|
1348
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
1349
|
+
copyButton.setAttribute('data-copied', 'true');
|
|
1350
|
+
copyButton.textContent = 'Copied';
|
|
1351
|
+
copyStatus.textContent = 'JSON copied to the clipboard';
|
|
1352
|
+
setTimeout(function () {
|
|
1353
|
+
copyButton.removeAttribute('data-copied');
|
|
1354
|
+
copyButton.textContent = 'Copy';
|
|
1355
|
+
copyStatus.textContent = '';
|
|
1356
|
+
}, 2000);
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
})();`;
|
|
1360
|
+
/**
|
|
1361
|
+
* Generate the HTML report content
|
|
1362
|
+
*/
|
|
1363
|
+
export function generateMetricsHtml(metrics) {
|
|
1364
|
+
const { meta, totals, phases, counts, isg, pageTimings, incremental } = metrics;
|
|
1365
|
+
const phaseRows = generatePhaseRows(phases, totals.durationMs);
|
|
1366
|
+
const hasDetailedTimings = pageTimings !== undefined && pageTimings.length > 0;
|
|
1367
|
+
const enabledFlags = [
|
|
1368
|
+
meta.flags.force ? '--force' : null,
|
|
1369
|
+
meta.flags.clean ? '--clean' : null,
|
|
1370
|
+
meta.flags.includeDrafts ? '--include-drafts' : null,
|
|
1371
|
+
].filter((flag) => flag !== null);
|
|
1372
|
+
const commit = meta.gitCommit ? meta.gitCommit.substring(0, 7) : undefined;
|
|
1373
|
+
const source = meta.gitBranch ? `${meta.gitBranch}${commit ? ` (${commit})` : ''}` : commit;
|
|
1374
|
+
const perPage = counts.renderedPages > 0 ? (counts.templatesLoaded / counts.renderedPages).toFixed(1) : '0';
|
|
1375
|
+
const navigation = [
|
|
1376
|
+
['phases', 'Phases'],
|
|
1377
|
+
['counts', 'Counts'],
|
|
1378
|
+
incremental ? ['rebuild', 'Rebuild'] : null,
|
|
1379
|
+
hasDetailedTimings ? ['pages', 'Pages'] : null,
|
|
1380
|
+
['raw-json', 'JSON'],
|
|
1381
|
+
]
|
|
1382
|
+
.filter((link) => link !== null)
|
|
1383
|
+
.map(([id, label]) => `<a href="#${id}">${label}</a>`)
|
|
1384
|
+
.join('\n ');
|
|
1385
|
+
// Escaped for the <pre>: the JSON stays readable in the page source and copies unchanged
|
|
1386
|
+
const json = JSON.stringify(metrics, null, 2);
|
|
1387
|
+
const jsonData = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1388
|
+
return `<!DOCTYPE html>
|
|
1389
|
+
<html lang="en">
|
|
1390
|
+
<head>
|
|
1391
|
+
<meta charset="UTF-8">
|
|
1392
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1393
|
+
<meta name="color-scheme" content="light dark">
|
|
1394
|
+
<link rel="icon" href="${STATI_FAVICON_HREF}">
|
|
1395
|
+
<title>Stati Build Metrics Report - ${escapeHtml(meta.timestamp)}</title>
|
|
1396
|
+
<style>${STATI_THEME_STYLES}
|
|
1397
|
+
${REPORT_STYLES}
|
|
1398
|
+
${STATI_PRINT_STYLES}
|
|
1081
1399
|
</style>
|
|
1082
1400
|
</head>
|
|
1083
1401
|
<body>
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
<
|
|
1089
|
-
|
|
1090
|
-
</
|
|
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>
|
|
1402
|
+
<header class="bar">
|
|
1403
|
+
<div class="shell">
|
|
1404
|
+
<div class="brand">
|
|
1405
|
+
<svg class="brand-mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${STATI_MARK_PATH}"/></svg>
|
|
1406
|
+
<span class="brand-name">Stati</span>
|
|
1407
|
+
<span class="brand-rule" aria-hidden="true"></span>
|
|
1408
|
+
<span class="brand-context">Build metrics</span>
|
|
1179
1409
|
</div>
|
|
1410
|
+
<nav class="bar-nav" aria-label="Report sections">
|
|
1411
|
+
${navigation}
|
|
1412
|
+
</nav>
|
|
1180
1413
|
</div>
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
</
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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>
|
|
1414
|
+
</header>
|
|
1415
|
+
|
|
1416
|
+
<main class="main shell">
|
|
1417
|
+
<section class="summary" aria-labelledby="report-title">
|
|
1418
|
+
<h1 class="headline" id="report-title">Built ${formatCount(counts.totalPages)} ${counts.totalPages === 1 ? 'page' : 'pages'} in ${formatDuration(totals.durationMs)}</h1>
|
|
1419
|
+
<p class="meta">
|
|
1420
|
+
<span class="type-badge">${escapeHtml(meta.command)}</span>
|
|
1421
|
+
<span class="type-badge">${meta.ci ? 'CI' : 'Local'}</span>
|
|
1422
|
+
${hasDetailedTimings ? '<span class="type-badge">Detailed</span>' : ''}
|
|
1423
|
+
<time class="meta-time" datetime="${escapeHtml(meta.timestamp)}">${escapeHtml(new Date(meta.timestamp).toLocaleString())}</time>
|
|
1424
|
+
</p>
|
|
1425
|
+
|
|
1426
|
+
<dl class="stats">
|
|
1427
|
+
<div class="stat">
|
|
1428
|
+
<dt>Pages rendered</dt>
|
|
1429
|
+
<dd class="stat-value">${formatCount(counts.renderedPages)}</dd>
|
|
1430
|
+
<dd class="stat-note">of ${formatCount(counts.totalPages)}, ${formatCount(counts.cachedPages)} from cache</dd>
|
|
1210
1431
|
</div>
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
<
|
|
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>
|
|
1432
|
+
<div class="stat">
|
|
1433
|
+
<dt>Cache hit rate</dt>
|
|
1434
|
+
<dd class="stat-value">${formatPercent(isg.cacheHitRate)}</dd>
|
|
1435
|
+
<dd class="stat-note">${formatCount(isg.manifestEntries)} manifest entries</dd>
|
|
1234
1436
|
</div>
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
<
|
|
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>
|
|
1437
|
+
<div class="stat">
|
|
1438
|
+
<dt>Templates loaded</dt>
|
|
1439
|
+
<dd class="stat-value">${formatCount(counts.templatesLoaded)}</dd>
|
|
1440
|
+
<dd class="stat-note">${perPage} per rendered page</dd>
|
|
1270
1441
|
</div>
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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>
|
|
1442
|
+
<div class="stat">
|
|
1443
|
+
<dt>Peak memory</dt>
|
|
1444
|
+
<dd class="stat-value">${formatBytes(totals.peakRssBytes)}</dd>
|
|
1445
|
+
<dd class="stat-note">Heap ${formatBytes(totals.heapUsedBytes)}</dd>
|
|
1321
1446
|
</div>
|
|
1322
|
-
</
|
|
1323
|
-
</div>
|
|
1447
|
+
</dl>
|
|
1324
1448
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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>
|
|
1449
|
+
<dl class="facts">
|
|
1450
|
+
<div class="fact">
|
|
1451
|
+
<dt>Flags</dt>
|
|
1452
|
+
<dd>${enabledFlags.length > 0 ? enabledFlags.join(' ') : 'none'}</dd>
|
|
1356
1453
|
</div>
|
|
1357
|
-
|
|
1358
|
-
|
|
1454
|
+
${source
|
|
1455
|
+
? `<div class="fact">
|
|
1456
|
+
<dt>${meta.gitBranch ? 'Branch' : 'Commit'}</dt>
|
|
1457
|
+
<dd>${escapeHtml(source)}</dd>
|
|
1458
|
+
</div>`
|
|
1359
1459
|
: ''}
|
|
1360
|
-
|
|
1361
|
-
|
|
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>
|
|
1460
|
+
<div class="fact">
|
|
1461
|
+
<dt>Stati</dt>
|
|
1462
|
+
<dd>CLI ${escapeHtml(meta.cliVersion)}, core ${escapeHtml(meta.coreVersion)}</dd>
|
|
1381
1463
|
</div>
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
<
|
|
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>
|
|
1464
|
+
<div class="fact">
|
|
1465
|
+
<dt>Node</dt>
|
|
1466
|
+
<dd>${escapeHtml(meta.nodeVersion)}</dd>
|
|
1407
1467
|
</div>
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
<
|
|
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>
|
|
1468
|
+
<div class="fact">
|
|
1469
|
+
<dt>Platform</dt>
|
|
1470
|
+
<dd>${escapeHtml(`${meta.platform} ${meta.arch}`)}, ${meta.cpuCount} ${meta.cpuCount === 1 ? 'CPU' : 'CPUs'}</dd>
|
|
1437
1471
|
</div>
|
|
1438
|
-
</
|
|
1439
|
-
</
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
</
|
|
1449
|
-
|
|
1450
|
-
</
|
|
1451
|
-
<
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
</
|
|
1472
|
+
</dl>
|
|
1473
|
+
</section>
|
|
1474
|
+
${renderPhasesSection(phaseRows)}
|
|
1475
|
+
${renderCountsSection(metrics)}
|
|
1476
|
+
${incremental ? renderIncrementalSection(incremental) : ''}
|
|
1477
|
+
${pageTimings && pageTimings.length > 0 ? renderPagesSection(pageTimings) : ''}
|
|
1478
|
+
<section class="section" id="raw-json" aria-labelledby="raw-json-title">
|
|
1479
|
+
<details class="disclosure" id="raw-json-data">
|
|
1480
|
+
<summary>
|
|
1481
|
+
${CHEVRON_ICON}
|
|
1482
|
+
<h2 class="section-title" id="raw-json-title">Raw JSON data</h2>
|
|
1483
|
+
<span class="section-note">${formatBytes(Buffer.byteLength(json))}</span>
|
|
1484
|
+
</summary>
|
|
1485
|
+
<figure class="panel">
|
|
1486
|
+
<figcaption class="panel-bar">
|
|
1487
|
+
<span>Schema version ${escapeHtml(metrics.schemaVersion)}</span>
|
|
1488
|
+
<button type="button" class="copy-button" id="copy-json">Copy</button>
|
|
1489
|
+
</figcaption>
|
|
1490
|
+
<pre class="code" id="json-content" tabindex="0">${jsonData}</pre>
|
|
1491
|
+
</figure>
|
|
1492
|
+
<span class="sr-only" id="copy-status" role="status"></span>
|
|
1493
|
+
</details>
|
|
1494
|
+
</section>
|
|
1495
|
+
</main>
|
|
1496
|
+
|
|
1497
|
+
<footer class="footer">
|
|
1498
|
+
<div class="shell">
|
|
1499
|
+
<span class="footer-name">Stati build metrics</span>
|
|
1500
|
+
<span>Generated ${escapeHtml(new Date().toISOString())}</span>
|
|
1460
1501
|
</div>
|
|
1502
|
+
</footer>
|
|
1461
1503
|
|
|
1462
|
-
|
|
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
|
-
}
|
|
1504
|
+
<script>${REPORT_SCRIPT}
|
|
1526
1505
|
</script>
|
|
1527
1506
|
</body>
|
|
1528
1507
|
</html>`;
|