Skip to content

Commit 7ac48f8

Browse files
committed
fix(stats): preserve dashboard product assets
1 parent 1824968 commit 7ac48f8

8 files changed

Lines changed: 148 additions & 14 deletions

File tree

defaults/public/llms-full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ The default policy rejects risky destinations such as credentialed URLs, non-HTT
5656

5757
Product defaults live in `defaults/`. Instance-owned files live in `custom/` and are not overwritten automatically during updates. The build copies defaults first, overlays `custom/public`, and then generates runtime files.
5858

59-
Use `npm run doctor` to detect stale copied public assets, unsupported copied language folders, stale branding, and missing shared head assets. Use `./scripts/v8s-fix --assets`, `--head-assets`, `--languages`, `--branding`, or `--all` to apply explicit maintenance fixes to copied public files.
59+
Use `npm run doctor` to detect stale copied public assets, stale copied product pages, unsupported copied language folders, stale branding, and missing shared head assets. Use `./scripts/v8s-fix --assets`, `--head-assets`, `--product-pages`, `--languages`, `--branding`, or `--all` to apply explicit maintenance fixes to copied public files.
6060

6161
## Useful Files
6262

scripts/build.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ function applyPublicBranding(siteConfig) {
485485
if (!hasWordmark && !hasSlogan) return;
486486

487487
rewriteHtmlFiles(BUILD_DIR, (html, filePath) => {
488+
if (isStatsDashboardFile(filePath)) return html;
489+
488490
let brandedHtml = html;
489491
const brandLabel = hasWordmark ? `${wordmark.black || ""}${wordmark.green || ""}` : "";
490492

@@ -525,6 +527,10 @@ function applyPublicBranding(siteConfig) {
525527
});
526528
}
527529

530+
function isStatsDashboardFile(filePath) {
531+
return path.relative(BUILD_DIR, filePath).split(path.sep).join("/") === "_stats/index.html";
532+
}
533+
528534
function languageForBuildHtmlFile(filePath) {
529535
const [firstSegment] = path.relative(BUILD_DIR, filePath).split(path.sep);
530536
return LANGUAGE_METADATA[firstSegment] ? firstSegment : "en";

scripts/doctor.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function main() {
3838
}
3939

4040
function printRecommendedFixes(issues) {
41-
const fixes = [...new Set(issues.map((issue) => issue.fix))];
41+
const fixes = [...new Set(issues.map((issue) => issue.fix))].sort(compareFixes);
4242
const fixCounts = fixes.map((fix) => ({
4343
fix,
4444
count: issues.filter((issue) => issue.fix === fix).length
@@ -55,6 +55,11 @@ function printRecommendedFixes(issues) {
5555
}
5656
}
5757

58+
function compareFixes(left, right) {
59+
const order = ["product-pages", "head-assets", "assets", "branding", "languages", "public"];
60+
return order.indexOf(left) - order.indexOf(right);
61+
}
62+
5863
try {
5964
main();
6065
} catch (error) {

scripts/install.mjs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -773,9 +773,10 @@ function customizePublicPages(args) {
773773
fs.rmSync(CUSTOM_PUBLIC_DIR, { recursive: true, force: true });
774774
copyDirectory(DEFAULT_PUBLIC_DIR, CUSTOM_PUBLIC_DIR);
775775
pruneUnsupportedLanguageDirs(CUSTOM_PUBLIC_DIR, args.languages);
776-
rewriteHtmlFiles(CUSTOM_PUBLIC_DIR, (html, filePath) =>
777-
normalizeHtmlHead(applyBranding(html, args, languageForPublicFile(filePath)))
778-
);
776+
rewriteHtmlFiles(CUSTOM_PUBLIC_DIR, (html, filePath) => {
777+
if (isProductPublicFile(filePath)) return html;
778+
return normalizeHtmlHead(applyBranding(html, args, languageForPublicFile(filePath)));
779+
});
779780
formatFiles(CUSTOM_PUBLIC_DIR, [".html"]);
780781
}
781782

@@ -895,6 +896,11 @@ function languageForPublicFile(filePath) {
895896
return DEFAULT_LANGUAGES.includes(language) ? language : "en";
896897
}
897898

899+
function isProductPublicFile(filePath) {
900+
const relative = path.relative(CUSTOM_PUBLIC_DIR, filePath).split(path.sep).join("/");
901+
return relative === "_stats/index.html" || relative === "_tests/index.html";
902+
}
903+
898904
function applyBranding(html, args, language = "en") {
899905
const brandLabel = `${args.wordmarkBlack}${args.wordmarkGreen}`;
900906
const wordmarkSpans = `<span>${escapeHtml(args.wordmarkBlack)}</span><span>${escapeHtml(args.wordmarkGreen)}</span>`;

scripts/install.test.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ function runSetup(cwd, extraArgs) {
157157
const builtIndex = fs.readFileSync(path.join(fixture, "build", "index.html"), "utf8");
158158
assert.match(builtIndex, /<span>v8s\.<\/span><span>link<\/span>/);
159159
assert.match(builtIndex, /The official demo for <a href="https:\/\/example\.com">Example Inc\.<\/a> projects/);
160+
161+
const builtStats = fs.readFileSync(path.join(fixture, "build", "_stats", "index.html"), "utf8");
162+
assert.match(builtStats, /<img src="\/logo\.svg" alt="VanityURLs" \/>/);
163+
assert.doesNotMatch(builtStats, /<a class="brand-mark brand-mark-wordmark"/);
160164
}
161165

162166
console.log("install tests ok");

scripts/lib/custom-public-maintenance.mjs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function diagnoseCustomPublic(context) {
5353

5454
issues.push(...diagnoseHtmlHeadAssets(context));
5555
issues.push(...diagnoseSharedAssets(context));
56+
issues.push(...diagnoseProductPages(context));
5657
issues.push(...diagnoseBranding(context));
5758

5859
const supported = new Set(languages);
@@ -95,9 +96,10 @@ export function reconcileCustomPublic(context, options = {}) {
9596
fs.rmSync(customPublicDir, { recursive: true, force: true });
9697
copyDirectory(defaultPublicDir, customPublicDir);
9798
pruneUnsupportedLanguageDirs(context);
98-
rewriteHtmlFiles(customPublicDir, (html, filePath) =>
99-
normalizeHtmlHead(applyBranding(html, context, languageForPublicFile(context, filePath)))
100-
);
99+
rewriteHtmlFiles(customPublicDir, (html, filePath) => {
100+
if (isProductPublicFile(context, filePath)) return html;
101+
return normalizeHtmlHead(applyBranding(html, context, languageForPublicFile(context, filePath)));
102+
});
101103
actions.push("recreated custom/public from defaults");
102104
} else {
103105
if (options.languages) {
@@ -110,15 +112,24 @@ export function reconcileCustomPublic(context, options = {}) {
110112
actions.push("synced shared CSS, script, logo, icon, font, and badge assets from defaults");
111113
}
112114

115+
if (options.productPages) {
116+
syncProductPages(context);
117+
actions.push("synced product dashboard and QA pages from defaults");
118+
}
119+
113120
if (options.branding) {
114-
rewriteHtmlFiles(customPublicDir, (html, filePath) =>
115-
applyBranding(html, context, languageForPublicFile(context, filePath))
116-
);
121+
rewriteHtmlFiles(customPublicDir, (html, filePath) => {
122+
if (isProductPublicFile(context, filePath)) return html;
123+
return applyBranding(html, context, languageForPublicFile(context, filePath));
124+
});
117125
actions.push("refreshed branding in custom/public HTML");
118126
}
119127

120128
if (options.headAssets) {
121-
rewriteHtmlFiles(customPublicDir, (html) => normalizeHtmlHead(html));
129+
rewriteHtmlFiles(customPublicDir, (html, filePath) => {
130+
if (isProductPublicFile(context, filePath)) return html;
131+
return normalizeHtmlHead(html);
132+
});
122133
actions.push("normalized favicon and theme head assets in custom/public HTML");
123134
}
124135
}
@@ -134,6 +145,7 @@ export function parseReconcileArgs(argv) {
134145
dryRun: false,
135146
headAssets: false,
136147
languages: false,
148+
productPages: false,
137149
public: false
138150
};
139151

@@ -143,6 +155,7 @@ export function parseReconcileArgs(argv) {
143155
options.branding = true;
144156
options.headAssets = true;
145157
options.languages = true;
158+
options.productPages = true;
146159
} else if (arg === "--assets") {
147160
options.assets = true;
148161
} else if (arg === "--branding") {
@@ -153,6 +166,8 @@ export function parseReconcileArgs(argv) {
153166
options.headAssets = true;
154167
} else if (arg === "--languages") {
155168
options.languages = true;
169+
} else if (arg === "--product-pages") {
170+
options.productPages = true;
156171
} else if (arg === "--public") {
157172
options.public = true;
158173
} else {
@@ -165,6 +180,7 @@ export function parseReconcileArgs(argv) {
165180

166181
function diagnoseHtmlHeadAssets(context) {
167182
return listHtmlFiles(context.customPublicDir)
183+
.filter((filePath) => !isProductPublicFile(context, filePath))
168184
.filter((filePath) => normalizeHtmlHead(fs.readFileSync(filePath, "utf8")) !== fs.readFileSync(filePath, "utf8"))
169185
.map((filePath) => ({
170186
code: "html-head-assets-stale",
@@ -193,10 +209,29 @@ function diagnoseSharedAssets(context) {
193209
});
194210
}
195211

212+
function diagnoseProductPages(context) {
213+
return listProductDefaultPages(context)
214+
.filter((defaultPath) => {
215+
const customPath = path.join(context.customPublicDir, path.relative(context.defaultPublicDir, defaultPath));
216+
return fs.existsSync(customPath) && !sameFile(defaultPath, customPath);
217+
})
218+
.map((defaultPath) => {
219+
const customPath = path.join(context.customPublicDir, path.relative(context.defaultPublicDir, defaultPath));
220+
return {
221+
code: "product-page-stale",
222+
severity: "warn",
223+
fix: "product-pages",
224+
path: relativePath(context, customPath),
225+
message: "Product-managed dashboard or QA page differs from defaults."
226+
};
227+
});
228+
}
229+
196230
function diagnoseBranding(context) {
197231
if (!hasBrandingConfig(context.siteConfig)) return [];
198232

199233
return listHtmlFiles(context.customPublicDir)
234+
.filter((filePath) => !isProductPublicFile(context, filePath))
200235
.filter((filePath) => {
201236
const html = fs.readFileSync(filePath, "utf8");
202237
const brandedHtml = applyBranding(html, context, languageForPublicFile(context, filePath));
@@ -242,6 +277,28 @@ function syncSharedAssets(context) {
242277
}
243278
}
244279

280+
function syncProductPages(context) {
281+
for (const defaultPath of listProductDefaultPages(context)) {
282+
const relative = path.relative(context.defaultPublicDir, defaultPath);
283+
const customPath = path.join(context.customPublicDir, relative);
284+
if (!fs.existsSync(customPath)) continue;
285+
fs.mkdirSync(path.dirname(customPath), { recursive: true });
286+
fs.copyFileSync(defaultPath, customPath);
287+
}
288+
}
289+
290+
function listProductDefaultPages(context) {
291+
return [
292+
path.join(context.defaultPublicDir, "_stats", "index.html"),
293+
path.join(context.defaultPublicDir, "_tests", "index.html")
294+
].filter((filePath) => fs.existsSync(filePath));
295+
}
296+
297+
function isProductPublicFile(context, filePath) {
298+
const relative = path.relative(context.customPublicDir, filePath).split(path.sep).join("/");
299+
return relative === "_stats/index.html" || relative === "_tests/index.html";
300+
}
301+
245302
function listSharedDefaultAssets(context) {
246303
const supported = new Set(context.languages);
247304
return listFiles(context.defaultPublicDir).filter((filePath) => {

scripts/maintenance.test.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,52 @@ function runCommand(cwd, args) {
191191
);
192192
}
193193

194+
{
195+
const fixture = makeFixture();
196+
run(fixture, [
197+
"scripts/install.mjs",
198+
"--no-check",
199+
"--domain",
200+
"go.example",
201+
"--worker-name",
202+
"go-example",
203+
"--owner",
204+
"team",
205+
"--languages",
206+
"en,fr",
207+
"--operator-timezone",
208+
"America/Toronto",
209+
"--operator-legal-name",
210+
"Example Inc.",
211+
"--operator-domain",
212+
"example.com",
213+
"--operator-abuse-contact",
214+
"abuse@example.com",
215+
"--operator-security-contact",
216+
"security@example.com",
217+
"--branding-slogan",
218+
"A short-link service for Example Inc.'s projects",
219+
"--customize-public"
220+
]);
221+
222+
const statsPath = path.join(fixture, "custom", "public", "_stats", "index.html");
223+
fs.writeFileSync(statsPath, "<!doctype html><title>Old dashboard</title>\n");
224+
225+
const doctorJson = JSON.parse(run(fixture, ["scripts/doctor.mjs", "--json"]));
226+
assert(
227+
doctorJson.issues.some(
228+
(issue) =>
229+
issue.code === "product-page-stale" &&
230+
issue.fix === "product-pages" &&
231+
issue.path === "custom/public/_stats/index.html"
232+
)
233+
);
234+
235+
runCommand(fixture, ["scripts/v8s-fix", "--product-pages"]);
236+
assert.equal(
237+
fs.readFileSync(statsPath, "utf8"),
238+
fs.readFileSync(path.join(fixture, "defaults", "public", "_stats", "index.html"), "utf8")
239+
);
240+
}
241+
194242
console.log("maintenance tests ok");

scripts/v8s-fix.mjs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ import {
99

1010
function main() {
1111
const options = parseReconcileArgs(process.argv.slice(2));
12-
const selected = options.public || options.languages || options.assets || options.branding || options.headAssets;
12+
const selected =
13+
options.public ||
14+
options.languages ||
15+
options.assets ||
16+
options.branding ||
17+
options.headAssets ||
18+
options.productPages;
1319
if (!selected) {
14-
throw new Error("Choose at least one fix: --head-assets, --assets, --languages, --branding, --public, or --all.");
20+
throw new Error(
21+
"Choose at least one fix: --head-assets, --assets, --product-pages, --languages, --branding, --public, or --all."
22+
);
1523
}
1624

1725
const context = loadMaintenanceContext();

0 commit comments

Comments
 (0)