Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/app/(app)/guide/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";

/**
* Retired destination. The reference material moves into Settings; this route
* only exists to keep old links from 404ing.
* Retired destination. The glossary is gone: a term a reader has to look up is
* a term the copy should have explained where they met it, and most of this
* product's copy is read outside the app — in digests, tickets and screenshots,
* where no glossary is reachable.
*
* Note: the `#reference` section of Settings is not built yet, so this lands on
* Settings without scrolling anywhere. That section arrives with the Settings
* work, not with the app chrome.
* This lands on the issues list, alongside the other retired destinations,
* rather than on the Settings section the glossary's replacement was once
* expected to become. There is no `#reference` section and there will not be
* one: the operational lines this route used to explain now sit beside the
* Settings controls they describe, so there is no single anchor to send an old
* link to, and the issues list is where every other retired route goes.
*/
export default function RedirectToReference() {
redirect(withBasePath(normalizeBasePath(getEnv("BASE_URL")), `${DESTINATION_PATH.settings}#reference`));
export default function RedirectToIssues() {
redirect(withBasePath(normalizeBasePath(getEnv("BASE_URL")), DESTINATION_PATH.issues));
}
75 changes: 43 additions & 32 deletions src/app/(app)/pages/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
CONFIDENCE_LABEL,
DESTINATION_LABEL,
DESTINATION_PATH,
EVIDENCE_SOURCE_LABEL,
applicabilityActionLabel,
type ExclusionReason,
} from "@/lib/vocabulary";
Expand Down Expand Up @@ -260,31 +261,31 @@ function CollectionState({ page, job }: { page: WatchPage; job?: CollectionJob }
const failed = page.runState === "failed";
const retained = job?.completedStrategies ?? [];
const retainedTests = [
...retained.map((strategy) => `${strategy[0].toUpperCase()}${strategy.slice(1)} PSI`),
...(job?.cruxCompletedAt ? ["CrUX"] : []),
...(job?.agentCompletedAt ? ["Agent readiness"] : []),
...retained.map((strategy) => `${strategy[0].toUpperCase()}${strategy.slice(1)} score`),
...(job?.cruxCompletedAt ? ["Visitor figures"] : []),
...(job?.agentCompletedAt ? ["Agent checks"] : []),
];
const retryingTests = [
...(["mobile", "desktop"] as const)
.filter((strategy) => !retained.includes(strategy))
.map((strategy) => `${strategy[0].toUpperCase()}${strategy.slice(1)} PSI`),
...(!job?.cruxCompletedAt ? ["CrUX"] : []),
...(!job?.agentCompletedAt ? ["Agent readiness"] : []),
.map((strategy) => `${strategy[0].toUpperCase()}${strategy.slice(1)} score`),
...(!job?.cruxCompletedAt ? ["Visitor figures"] : []),
...(!job?.agentCompletedAt ? ["Agent checks"] : []),
];
const waitDetail = retainedTests.length > 0
? `${retainedTests.join(" and ")} retained. `
+ `Retrying ${retryingTests.join(" and ")}`
+ `${job?.nextRetryAt ? ` around ${formatSuccessfulRunAt(job.nextRetryAt)}` : " in the next evidence cycle"}.`
: `No independent test has completed yet. Retrying Mobile PSI, Desktop PSI, CrUX, and Agent readiness`
: `No test has finished yet. Retrying the mobile score, the desktop score, the visitor figures, and the agent checks`
+ `${job?.nextRetryAt ? ` around ${formatSuccessfulRunAt(job.nextRetryAt)}` : " in the next evidence cycle"}.`;
const providerDetail = (["mobile", "desktop"] as const).flatMap((strategy) => {
const error = job?.strategyErrors?.[strategy];
return error ? [`${strategy[0].toUpperCase()}${strategy.slice(1)}: ${error}`] : [];
}).join(" · ");
const latestDetail = [
providerDetail,
job?.cruxError ? `CrUX: ${job.cruxError}` : "",
job?.agentError ? `Agent: ${job.agentError}` : "",
job?.cruxError ? `Visitor figures: ${job.cruxError}` : "",
job?.agentError ? `Agent checks: ${job.agentError}` : "",
].filter(Boolean).join(" · ");
const title = page.runState === "queued"
? "Collection queued"
Expand Down Expand Up @@ -890,7 +891,7 @@ function ReadingsSection({
function fallbackReport(d: Night): string {
if (!nightHasStrategy(d, strategy)) {
return JSON.stringify({
note: `No ${strategy} PSI measurement completed for this collection. Other independent results are retained.`,
note: `No ${strategy} score was measured this time. The other readings were kept.`,
date: d.date,
strategy,
agentChecksRecorded: d.agent?.length ?? 0,
Expand All @@ -900,7 +901,7 @@ function ReadingsSection({
}
return JSON.stringify(
{
note: "No raw PSI payload is stored for this night (seed / imported data). Showing the stored medians and ranges only.",
note: "The full reply from the test was not stored for this night, so only the saved scores and their spread are shown.",
date: d.date,
strategy,
samples: d.samples ?? d.sampleSize ?? null,
Expand Down Expand Up @@ -975,10 +976,10 @@ function ReadingsSection({
}}
>
<div style={{ fontSize: 12, color: "var(--text-muted)", marginBottom: 18 }}>
Desktop and Mobile are stacked for comparison. Each median line includes its run-to-run range; reference
lines show that device&apos;s original benchmark and, when enough scans exist, the previous {rangeDays}-day
period median.
{excludedHistory.length > 0 && " Shaded bands mark measurements retained for diagnosis but excluded from scores, trends, and recommendations."}
Desktop and Mobile are stacked for comparison. Each line is the middle score of a night&apos;s runs, with the
spread between those runs around it; the reference lines show that device&apos;s starting point and, once
there are enough nights, the middle score of the previous {rangeDays} days.
{excludedHistory.length > 0 && " Shaded bands are readings kept for diagnosis but left out of scores, trends and recommendations."}
</div>
{historyForStrategy(rangeHistory, "desktop").length < 2 && historyForStrategy(rangeHistory, "mobile").length < 2 ? (
<div style={{ padding: "42px 16px", textAlign: "center", color: "var(--text-muted)", fontSize: 13 }}>
Expand Down Expand Up @@ -1079,10 +1080,17 @@ function ReadingsSection({
className="table-scroll"
style={{ background: "var(--surface-card)", border: "1px solid var(--border-hairline)", borderRadius: 11 }}
>
{/*
The caption is where every measurement below it is introduced in
words. The column headings are acronyms because a column is four
characters wide, and that is only honest if the reader has met the
words first — so they are met here, once, and the headings are the
second mention. Nothing below relies on a hover to be understood.
*/}
<div style={{ padding: "13px 22px", borderBottom: "1px solid var(--border-hairline)", fontSize: 12, color: "var(--text-muted)" }}>
Every recorded collection · <span style={{ color: "var(--text-body)", textTransform: "capitalize", fontWeight: 600 }}>{strategy}</span> primary · Lighthouse median with range below
{showVisitorColumns && " · CrUX p75 with weekly change below"}
{excludedHistory.length > 0 && " · PSI anomaly rows are observed measurements excluded from scoring"}
Every measurement ever taken · <span style={{ color: "var(--text-body)", textTransform: "capitalize", fontWeight: 600 }}>{strategy}</span> first · the middle score of each night&apos;s runs, with the spread beneath it
{showVisitorColumns && " · then what real visitors met, at the level three quarters of them did better than (p75): when the main content appeared (LCP), how long the page took to answer a tap (INP), how much it jumped about (CLS), and how long the server took to reply (TTFB), each with its change on the week"}
{excludedHistory.length > 0 && " · rows marked as odd readings were measured and then left out of the scoring"}
{` · Dates in ${collectionSchedule.timeZone}`}
</div>
<div
Expand All @@ -1102,9 +1110,12 @@ function ReadingsSection({
>
<div>Night</div>
<div>Marker</div>
{/* Four columns, four score names. "A11y" was a numeronym nobody
outside the trade reads; the others are the score names
themselves, abbreviated to fit the column. */}
<div style={{ textAlign: "center" }}>Perf</div>
<div style={{ textAlign: "center" }}>A11y</div>
<div style={{ textAlign: "center" }}>BP</div>
<div style={{ textAlign: "center" }}>Access</div>
<div style={{ textAlign: "center" }}>Practices</div>
<div style={{ textAlign: "center" }}>SEO</div>
{showVisitorColumns && VISITOR_METRICS.map((metric) => (
<div
Expand All @@ -1128,16 +1139,16 @@ function ReadingsSection({
? visitorEvidence!.snapshots[visitorSnapshotIndex - 1]
: null;
const completedTests = [
nightHasStrategy(d, "mobile") ? "M PSI" : null,
nightHasStrategy(d, "desktop") ? "D PSI" : null,
visitorSnapshot ? "CrUX" : null,
Array.isArray(d.agent) ? "Agent" : null,
d.kitesurf?.status === "available" ? "Kitesurf" : null,
nightHasStrategy(d, "mobile") ? "Mobile" : null,
nightHasStrategy(d, "desktop") ? "Desktop" : null,
visitorSnapshot ? "Visitors" : null,
Array.isArray(d.agent) ? "Agent checks" : null,
d.kitesurf?.status === "available" ? EVIDENCE_SOURCE_LABEL.kitesurf : null,
].filter((label): label is string => label !== null);
const cell = (k: CategoryKey) => {
if (!nightHasStrategy(d, strategy)) {
return (
<div aria-label={`No ${strategy} PSI measurement`} style={{ textAlign: "center", color: "var(--health-none-text)" }}>
<div aria-label={`No ${strategy} score measured`} style={{ textAlign: "center", color: "var(--health-none-text)" }}>
</div>
);
Expand All @@ -1146,7 +1157,7 @@ function ReadingsSection({
const categoryLabel = CATEGORIES.find((category) => category.key === k)?.label ?? k;
return (
<div
aria-label={`${categoryLabel} ${excludedAnomaly ? "observed" : "median"} ${score.m}, range ${score.lo} to ${score.hi}${excludedAnomaly ? ", excluded PSI anomaly" : ""}`}
aria-label={`${categoryLabel} ${excludedAnomaly ? "observed" : "median"} ${score.m}, range ${score.lo} to ${score.hi}${excludedAnomaly ? ", an odd reading left out of the scoring" : ""}`}
style={{ textAlign: "center" }}
>
<div style={{ fontSize: 14, lineHeight: 1.1, fontWeight: 650, color: scoreMetaVars(score.m).fg }}>{score.m}</div>
Expand Down Expand Up @@ -1174,8 +1185,8 @@ function ReadingsSection({
const label = VISITOR_METRICS.find((metric) => metric.key === key)?.label ?? key;
return (
<div
aria-label={`${label} ${formatVisitorMetric(key, value)}, ${movement === "—" ? "no prior CrUX snapshot" : movement}`}
title={visitorSnapshot ? `Rolling window ending ${visitorSnapshot.collectionEnd} · ${rating ?? "Unavailable"}` : "No CrUX window available for this night"}
aria-label={`${label} ${formatVisitorMetric(key, value)}, ${movement === "—" ? "no earlier reading to compare" : movement}`}
title={visitorSnapshot ? `28 days ending ${visitorSnapshot.collectionEnd} · ${rating ?? "Unavailable"}` : "No visitor figures cover this night"}
style={{ textAlign: "center", borderLeft: key === "lcpP75Ms" ? "1px solid var(--border-hairline)" : undefined }}
>
<div style={{ fontSize: 13, lineHeight: 1.1, fontWeight: 650, color: valueColor }}>
Expand Down Expand Up @@ -1226,10 +1237,10 @@ function ReadingsSection({
{startsDateGroup && timeLabel ? `${timeLabel} · ${runLabel}` : runLabel}
</div>
<div
title={`Completed independently: ${completedTests.join(", ") || "none"}`}
title={`Measured separately: ${completedTests.join(", ") || "nothing"}`}
style={{ marginTop: 4, color: "var(--text-muted)", fontSize: 12, lineHeight: 1.3 }}
>
{completedTests.join(" · ") || "No completed test"}
{completedTests.join(" · ") || "Nothing measured"}
</div>
</div>
<div style={{ fontSize: 12, display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 5 }}>
Expand All @@ -1238,7 +1249,7 @@ function ReadingsSection({
title="Retained for diagnosis; not used in status, trend, or recommendations"
style={{ color: "var(--text-muted)", fontWeight: 600 }}
>
PSI anomaly · excluded
Odd reading · left out
</span>
)}
{!excludedAnomaly && markers.length === 0 ? (
Expand Down
18 changes: 9 additions & 9 deletions src/app/(app)/pages/pages-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { normalizeCollectionSchedule } from "@/lib/collectionSchedule";
import { evidenceForPage, visitorExperienceTrend } from "@/lib/visitorExperience";
import { performanceIssueCounts, siteCulpritRollups, sitePerformanceIssues } from "@/lib/performanceIssues";
import type { PerformanceIssueStatus } from "@/lib/performanceIssues";
import { customerActionabilityFor, recommendationIsCustomerActionable, remediationTone, triageActionLabel, webflowClassificationFor } from "@/lib/webflowPerformance";
import { customerActionabilityFor, metricDisplay, recommendationIsCustomerActionable, remediationTone, triageActionLabel, webflowClassificationFor } from "@/lib/webflowPerformance";
import { siteNativeElementRollups } from "@/lib/nativeElements";
import { compareLabAndField } from "@/lib/labFieldComparison";
import { fieldPriorityRankForRec, recommendationEvidenceSignal } from "@/lib/fieldPrioritization";
Expand Down Expand Up @@ -550,7 +550,7 @@ function DashboardContent({
) : topRibbonRec && topRibbonClassification ? (
topRibbonRec.source === "crux-field-only" ? (
<>
Investigate <strong>{topRibbonRec.pageTitle}</strong> — {topRibbonRec.title} is the clearest next step because exact-URL visitor evidence is outside the good range while Lighthouse did not reproduce or explain it.
Investigate <strong>{topRibbonRec.pageTitle}</strong> — {topRibbonRec.title} is the clearest next step, because real visitors to this exact page are outside the good range and the nightly test did not reproduce or explain it.
</>
) : customerActionabilityFor(topRibbonRec) === "workaround" ? (
<>
Expand Down Expand Up @@ -603,8 +603,8 @@ function DashboardContent({
<span style={{ fontSize: 13, fontWeight: 650 }}>
{measurementIncident.status === "suspected"
? measurementIncident.confirmationAttempts
? "PSI measurement anomaly persists"
: "Possible PSI measurement anomaly"
? "The test environment is still behaving oddly"
: "The test environment may have behaved oddly"
: measurementIncident.status === "confirming"
? "Collecting independent confirmation"
: measurementIncident.status === "recovered"
Expand All @@ -621,13 +621,13 @@ function DashboardContent({
<div style={{ maxWidth: 900, marginTop: 4, color: incidentSurface.body, fontSize: 12, lineHeight: 1.5 }}>
{measurementIncident.status === "suspected"
? measurementIncident.confirmationAttempts
? `Independent confirmation showed the same PSI environment pattern across ${measurementIncident.affectedPages} pages. The suspect measurements remain excluded, no action is recommended, and the next scheduled cohort will continue monitoring.`
: `${measurementIncident.affectedPages} of ${measurementIncident.eligiblePages} pages moved together while the PSI test environment also changed. No action is recommended until the automatic confirmation finishes.`
? `A second check found the same pattern across ${measurementIncident.affectedPages} pages. The suspect readings stay out of the scoring, there is nothing to do, and the next run keeps watching.`
: `${measurementIncident.affectedPages} of ${measurementIncident.eligiblePages} pages moved together at the moment the test environment changed too. Nothing to do until the automatic second check finishes.`
: measurementIncident.status === "confirming"
? "Page Watch is re-testing the affected cohort with independent, staggered samples. Earlier measurements are excluded from regression statuses while confirmation is running."
: measurementIncident.status === "recovered"
? "Follow-up measurements returned to the expected range. The earlier movement was treated as temporary PSI test-environment variability; no action is needed."
: "Independent follow-up measurements confirmed the synchronized slowdown without the earlier PSI environment anomaly. Review the affected pages and shared site dependencies."}
? "Later measurements came back to the expected range. The earlier movement was treated as the test environment being briefly unsteady, so there is nothing to do."
: "A separate follow-up confirmed the slowdown, and this time the test environment was steady. Look at the affected pages and anything they share."}
</div>
</section>
)}
Expand Down Expand Up @@ -780,7 +780,7 @@ function DashboardContent({
<div style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 6, marginTop: 11 }}>
{rollup.metrics.filter((metric) => metric.metric !== "other").map((metric) => (
<span key={`${metric.metric}:${metric.metricWeight}`} style={{ display: "inline-flex", alignItems: "baseline", gap: 5, fontSize: 12, color: "var(--text-muted)", background: "var(--surface-raised)", padding: "2px 7px", borderRadius: 5 }}>
{metric.metric} ·
{metricDisplay(metric.metric)} ·
{/* The unit sits tight against the numeral so it
still reads as a percent sign, not a word. */}
<Magnitude value={metric.metricWeight} unit="%" fontSize={12} style={{ gap: 1 }} />
Expand Down
Loading
Loading