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
36 changes: 33 additions & 3 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Product decisions pending sign-off

The implementation resolved four previously-undefined product questions. They
The implementation resolved five previously-undefined product questions. They
are reasonable defaults, but they should be confirmed (or changed) by product
rather than remaining implicit in the code. Each notes where it lives so a
change is a one-line edit.
Expand Down Expand Up @@ -38,9 +38,39 @@ flat history still tolerates normal PSI jitter.
## 4. Collection starts at the workspace's saved local time

The first watched page initializes the workspace to **midnight in that user's
browser timezone**. The Watchlist setting can override both time and IANA
browser timezone**. The Settings screen can override both time and IANA
timezone. Active pages receive stable offsets after the chosen start so the
workspace does not burst every page or PSI sample simultaneously.

- Where: `src/lib/collectionSchedule.ts`, the Watchlist settings panel, and the
- Where: `src/lib/collectionSchedule.ts`, the Settings screen, and the
collector's 15-minute due-page cron.

## 5. Sensitivity is one control with three positions (option 10b)

What a site considers worth reporting is **one setting**, not twelve. The three
positions are Only big moves / Normal / Everything, and each resolves to a
complete threshold set. The limits it resolves to are **displayed beneath the
control, in the strings the digest itself writes**, so the abstraction is never
opaque: a reader who wants to know why a line said "above the 250 ms you set"
can see the 250 ms and see which position put it there.

What the numbers are at each position is the part product should confirm. What
is settled, and should not be reopened without a decision:

- **Twelve per-metric thresholds** were rejected. Every number honest, and
nobody could say what any of them would do to tonight's digest.
- **No thresholds at all** were rejected. The digest's threshold clause is the
reason a reader trusts the line, and it needs a setting behind it to be true.
- **Per-page sensitivity** does not exist anywhere. S3 removed the page-detail
calibration panel and S8 gives it no new home; a site has one answer to "what
is worth telling you" because the digest that asks it is one message per site.
- **No position resolves the savings gate to 0.** At 0 there is no limit the
reader set, so the digest withholds the clause and there is nothing to show
under the control. "Everything" is 1 ms, which is every measurement there is.
- A site whose thresholds were hand-tuned before this landed is **mapped to the
nearest position and told once**, in the digest footer. Discarding somebody's
configuration silently is worse than the configuration was.

- Where: `SENSITIVITY_THRESHOLDS` in `src/lib/sensitivity.ts` is the only place
the numbers appear; `DEFAULT_PERFORMANCE_THRESHOLDS` reads the Normal position
from it. The migration is `normalizeState` in `src/lib/store/normalize.ts`.
4 changes: 2 additions & 2 deletions collector-worker/dataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { buildInitialState, buildSeedCruxEvidence, DEMO_DATA_VERSION } from "../
import { captureAgentReadiness } from "../src/lib/agentScoring";
import { resolveMarkerIndex } from "../src/lib/followups";
import { mediansOf, pageTrend } from "../src/lib/scoring";
import { effectivePerformanceThresholds } from "../src/lib/performanceThresholds";
import { normalizePerformanceThresholds } from "../src/lib/performanceThresholds";
import { normalizeState } from "../src/lib/store/normalize";
import { TENANT, type AppState, type ChangeMarker, type Night } from "../src/lib/types";
import {
Expand Down Expand Up @@ -217,7 +217,7 @@ export class FdeDataStore {
desktop: mediansOf(night.scores.desktop),
};
page.agent = agent ?? [];
page.status = pageTrend(page, "mobile", effectivePerformanceThresholds(draft.performanceThresholds, page));
page.status = pageTrend(page, "mobile", normalizePerformanceThresholds(draft.performanceThresholds));
page.runState = undefined;
page.lastRunAt = night.iso ?? new Date().toISOString();
page.lastCollectionStatus = "trusted";
Expand Down
6 changes: 3 additions & 3 deletions src/app/(app)/pages/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useIssuesView, useStore } from "@/components/store";
import { CATEGORIES } from "@/lib/types";
import type { CategoryKey, CollectionJob, Night, RangeDays, WatchPage } from "@/lib/types";
import { agentReadinessHistoryPoints } from "@/lib/agentHistory";
import { effectivePerformanceThresholds } from "@/lib/performanceThresholds";
import { normalizePerformanceThresholds } from "@/lib/performanceThresholds";
import {
historyForStrategy,
nightHasStrategy,
Expand Down Expand Up @@ -798,7 +798,7 @@ function ReadingsSection({
...run,
startsDateGroup: run.dateKey !== runMetadata[index - 1]?.dateKey,
}));
const thresholds = effectivePerformanceThresholds(store.performanceThresholds, page);
const thresholds = normalizePerformanceThresholds(store.performanceThresholds);
const readinessHistory = agentReadinessHistoryPoints(
agentRangeHistory,
page.agentIgnores,
Expand Down Expand Up @@ -1301,7 +1301,7 @@ export default function PageDetail() {
const collectionBlocked = page.flag === "paused" || (!!page.runState && page.runState !== "failed");
const activeJob = store.jobs?.find((job) => job.runId === page.runId);
const watchedPageHref = /^[a-z][a-z\d+.-]*:\/\//i.test(page.url) ? page.url : `https://${page.url}`;
const thresholds = effectivePerformanceThresholds(store.performanceThresholds, page);
const thresholds = normalizePerformanceThresholds(store.performanceThresholds);
// A development-only comparison of the two trend renderings side by side.
const isStatusPreview = process.env.NODE_ENV === "development" && searchParams.get("statusPreview") === "compare";
const mobileTrend = isStatusPreview ? "regressing" : pageRangeTrend(page, "mobile", rangeDays, thresholds);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(app)/pages/pages-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { SegmentRole } from "@/components/segmented-control";
import { CATEGORIES } from "@/lib/types";
import type { AgentIgnoreSettings, Night, WebflowRemediationLevel } from "@/lib/types";
import { agentReadinessForNight, summarizeAgentChecks } from "@/lib/agentScoring";
import { effectivePerformanceThresholds, normalizePerformanceThresholds } from "@/lib/performanceThresholds";
import { normalizePerformanceThresholds } from "@/lib/performanceThresholds";
import { historyForRange, pageAgentSnapshotForRange, pageRangeLatestNightForStrategy, pageRangeTrend } from "@/lib/scoring";
import { flagChip, savingsValue } from "@/lib/ui";
import { DESTINATION_LABEL, DESTINATION_PATH, QUEUE_LABEL } from "@/lib/vocabulary";
Expand Down Expand Up @@ -294,7 +294,7 @@ function DashboardContent({
const nativeElementRollups = siteNativeElementRollups(activePages);

const rows = pages.map((p, watchlistOrder) => {
const pageThresholds = effectivePerformanceThresholds(thresholds, p);
const pageThresholds = normalizePerformanceThresholds(thresholds);
const mobileTrend = pageRangeTrend(p, "mobile", rangeDays, pageThresholds);
const desktopTrend = pageRangeTrend(p, "desktop", rangeDays, pageThresholds);
const visitorEvidence = evidenceForPage(visitorExperience, p.id, strategy);
Expand Down
Loading
Loading