@ainyc/canonry 5.5.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +1 -1
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +10 -0
- package/assets/assets/{AuditHistoryPanel-BMGGxHpw.js → AuditHistoryPanel-DVUbjx1M.js} +1 -1
- package/assets/assets/{BacklinksPage-CS3wJSWe.js → BacklinksPage-D01VTDAp.js} +1 -1
- package/assets/assets/ChartPrimitives-BQC2VH0O.js +1 -0
- package/assets/assets/{HistoryPage-Ba9ezeuP.js → HistoryPage-Bk1gsXe5.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Y9eseEv0.js → MeasurementPropertyPage-D7p5vpY_.js} +1 -1
- package/assets/assets/ProjectPage-Dv3USEIE.js +8 -0
- package/assets/assets/{RunRow-DbUtgNUI.js → RunRow-Dpa0kAip.js} +1 -1
- package/assets/assets/{RunsPage-BBuFIYuS.js → RunsPage-B59z7DQ_.js} +1 -1
- package/assets/assets/{SettingsPage-DNnR33D-.js → SettingsPage-HJDgmQi9.js} +1 -1
- package/assets/assets/{SiteHealthSection-Cf4KHWp0.js → SiteHealthSection-BnJA-zvQ.js} +3 -3
- package/assets/assets/{TrafficPage-DSQjaZM9.js → TrafficPage-C8na59q_.js} +5 -5
- package/assets/assets/{TrafficSourceDetailPage-C-Pg69TW.js → TrafficSourceDetailPage-B2fO8bDc.js} +1 -1
- package/assets/assets/{extract-error-message-DNLmL0ID.js → extract-error-message-670MySRM.js} +1 -1
- package/assets/assets/index-DTrkuKOh.js +86 -0
- package/assets/assets/index-PT4UiPTM.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-DkItaBPY.js → react-sigma_core.esm.min-DOhuYLQj.js} +1 -1
- package/assets/assets/v2-overview-adapter-CQyDNkXa.js +1 -0
- package/assets/assets/{vendor-recharts-D-cUuLZo.js → vendor-recharts-llkUFBGR.js} +1 -1
- package/assets/index.html +3 -3
- package/dist/{chunk-75ECQ77U.js → chunk-C4KBR4H6.js} +6 -6
- package/dist/{chunk-REQEWYNF.js → chunk-D3DKDTXP.js} +1027 -70
- package/dist/{chunk-DX4CZNNU.js → chunk-GHAUT7XJ.js} +2 -2
- package/dist/{chunk-KKDXJWDK.js → chunk-I4AHG7K3.js} +3 -3
- package/dist/{chunk-LQAOGFFK.js → chunk-KZYOJ4F5.js} +2 -2
- package/dist/{chunk-BL3FMPSM.js → chunk-TM2NQMJA.js} +803 -777
- package/dist/cli.js +10 -9
- package/dist/{demo-server-BSH6SSWD.js → demo-server-JEGVGOPQ.js} +7 -3
- package/dist/index.js +5 -5
- package/dist/{intelligence-service-CU7W5AVC.js → intelligence-service-5P62KKG6.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +11 -11
- package/assets/assets/ChartPrimitives-Bxa_vFmq.js +0 -1
- package/assets/assets/ProjectPage-Cl1N9Dcw.js +0 -8
- package/assets/assets/index-BCCrbno5.js +0 -86
- package/assets/assets/index-gQj7x9RX.css +0 -1
- package/assets/assets/v2-overview-adapter-DKBMcYBh.js +0 -1
|
@@ -88,8 +88,11 @@ import {
|
|
|
88
88
|
MeasurementRunScopeError,
|
|
89
89
|
PROJECTS_WRITE_SCOPE,
|
|
90
90
|
READ_ONLY_SCOPE,
|
|
91
|
+
REPORT_HEADER_COPY,
|
|
92
|
+
REPORT_SECTION_COPY,
|
|
91
93
|
REPORT_VISIBILITY_COPY,
|
|
92
94
|
RESEARCH_RUN_SCOPE,
|
|
95
|
+
ReportSectionIds,
|
|
93
96
|
ResearchQueryStatuses,
|
|
94
97
|
ResearchRunStatuses,
|
|
95
98
|
RunKinds,
|
|
@@ -127,6 +130,7 @@ import {
|
|
|
127
130
|
USAGE_TELEMETRY_HEADERS,
|
|
128
131
|
UserRoles,
|
|
129
132
|
VerificationStatuses,
|
|
133
|
+
VisibilityReportScopeErrorReasons,
|
|
130
134
|
WILDCARD_SCOPE,
|
|
131
135
|
WinnabilityClasses,
|
|
132
136
|
__export,
|
|
@@ -270,7 +274,6 @@ import {
|
|
|
270
274
|
createTemplateLinkPairIndex,
|
|
271
275
|
createUserRequestSchema,
|
|
272
276
|
createdApiKeyDtoSchema,
|
|
273
|
-
dedupeReportActions,
|
|
274
277
|
dedupeReportOpportunities,
|
|
275
278
|
deduplicateResearchQueries,
|
|
276
279
|
deliveryFailed,
|
|
@@ -282,6 +285,7 @@ import {
|
|
|
282
285
|
deriveSiteHealthState,
|
|
283
286
|
deriveWinnabilityClass,
|
|
284
287
|
describeError,
|
|
288
|
+
describeLandingPage,
|
|
285
289
|
determineAnswerMentioned,
|
|
286
290
|
diagnosticIdentity,
|
|
287
291
|
discoveryBucketSchema,
|
|
@@ -310,16 +314,15 @@ import {
|
|
|
310
314
|
findDuplicateLocationLabels,
|
|
311
315
|
forbidden,
|
|
312
316
|
formatAiReferralClassSummary,
|
|
313
|
-
formatAverageDelta,
|
|
314
317
|
formatCanonryPluginUpdateHint,
|
|
315
318
|
formatCanonryPluginVersionMismatch,
|
|
316
319
|
formatDate,
|
|
317
|
-
formatDateRange,
|
|
318
320
|
formatDeltaCopy,
|
|
319
321
|
formatIsoDate,
|
|
320
322
|
formatIsoDateInTimeZone,
|
|
321
323
|
formatNumber,
|
|
322
324
|
formatRatio,
|
|
325
|
+
formatWholePercent,
|
|
323
326
|
formatWindowCountDelta,
|
|
324
327
|
ga4AiReferralDailyDtoSchema,
|
|
325
328
|
ga4AiReferralHistoryEntrySchema,
|
|
@@ -573,12 +576,68 @@ import {
|
|
|
573
576
|
registrableDomain,
|
|
574
577
|
relativeChangeRatio,
|
|
575
578
|
reportActionCategoryLabel,
|
|
579
|
+
reportActionConfidenceBadge,
|
|
580
|
+
reportActionHorizonBadge,
|
|
576
581
|
reportActionTone,
|
|
582
|
+
reportAudienceActions,
|
|
583
|
+
reportBarChartLabel,
|
|
584
|
+
reportCitationsTrendBaseline,
|
|
585
|
+
reportCitedUrlCount,
|
|
586
|
+
reportClientCitedSubtitle,
|
|
587
|
+
reportClientClicksNoun,
|
|
588
|
+
reportClientHeroSentence,
|
|
589
|
+
reportClientIndexedPages,
|
|
590
|
+
reportClientIndexingTone,
|
|
591
|
+
reportClientMentionedSubtitle,
|
|
592
|
+
reportClientNotIndexedTail,
|
|
593
|
+
reportClientProvidersSubtitle,
|
|
594
|
+
reportClientQueriesSubtitle,
|
|
595
|
+
reportClientSearchCount,
|
|
596
|
+
reportClientSourceCount,
|
|
597
|
+
reportClientTrendCopy,
|
|
598
|
+
reportCompactList,
|
|
577
599
|
reportComparisonWindowDays,
|
|
578
|
-
|
|
579
|
-
|
|
600
|
+
reportCompetitorMentionCopy,
|
|
601
|
+
reportCrawlerTrustSummary,
|
|
602
|
+
reportDeltaArrow,
|
|
603
|
+
reportDirectionTone,
|
|
604
|
+
reportExecutiveHeadline,
|
|
605
|
+
reportGaIntro,
|
|
606
|
+
reportGscIntro,
|
|
607
|
+
reportHeaderMarketLabel,
|
|
608
|
+
reportHeaderPeriodLabel,
|
|
609
|
+
reportIndexingIntro,
|
|
610
|
+
reportIndexingLegendLabel,
|
|
611
|
+
reportInsightTone,
|
|
612
|
+
reportInstanceCountLabel,
|
|
613
|
+
reportLineChartLabel,
|
|
614
|
+
reportMarketScope,
|
|
615
|
+
reportMissRateLabel,
|
|
616
|
+
reportMoreChipLabel,
|
|
617
|
+
reportMovementChangeCopy,
|
|
618
|
+
reportOpportunityActionLine,
|
|
619
|
+
reportPressureTone,
|
|
620
|
+
reportPriorWindowLabel,
|
|
621
|
+
reportProviderDisplayName,
|
|
622
|
+
reportProviderRateLabel,
|
|
580
623
|
reportQueryClassLabel,
|
|
624
|
+
reportRateDeltaCopy,
|
|
625
|
+
reportReferralRedirectNote,
|
|
626
|
+
reportServerActivityAgencyOperatorHeaders,
|
|
627
|
+
reportServerActivityAgencyTiles,
|
|
628
|
+
reportServerActivityClientOperatorHeaders,
|
|
629
|
+
reportServerActivityCrawledPathsNote,
|
|
630
|
+
reportServerActivityHeading,
|
|
631
|
+
reportServerActivityOperatorDelta,
|
|
632
|
+
reportServerActivityPathHits,
|
|
633
|
+
reportServerActivityTrendTitle,
|
|
581
634
|
reportSeverityLabel,
|
|
635
|
+
reportShareBarShareLabel,
|
|
636
|
+
reportSourceCategoryShareLabel,
|
|
637
|
+
reportSourceCategoryTone,
|
|
638
|
+
reportSourceOriginHeadline,
|
|
639
|
+
reportTrendProviderRates,
|
|
640
|
+
reportTruncatedList,
|
|
582
641
|
reportVisibilityComparison,
|
|
583
642
|
reportVisibilityEvidence,
|
|
584
643
|
reportVisibilityHistoryLabel,
|
|
@@ -612,6 +671,7 @@ import {
|
|
|
612
671
|
runTriggerRequestSchema,
|
|
613
672
|
runTriggerSchema,
|
|
614
673
|
runtimeStateMissing,
|
|
674
|
+
safeLinkHref,
|
|
615
675
|
schedulableRunKindSchema,
|
|
616
676
|
scheduleDtoSchema,
|
|
617
677
|
scheduleExpectedUpdatedAtSchema,
|
|
@@ -709,10 +769,10 @@ import {
|
|
|
709
769
|
wordpressSchemaDeployResultDtoSchema,
|
|
710
770
|
wordpressSchemaStatusResultDtoSchema,
|
|
711
771
|
wordpressStatusDtoSchema
|
|
712
|
-
} from "./chunk-
|
|
772
|
+
} from "./chunk-D3DKDTXP.js";
|
|
713
773
|
|
|
714
774
|
// src/intelligence-service.ts
|
|
715
|
-
import { eq as eq68, desc as desc30, asc as asc12, and as and58, ne as
|
|
775
|
+
import { eq as eq68, desc as desc30, asc as asc12, and as and58, ne as ne10, or as or17, inArray as inArray26, gte as gte18, lte as lte15, isNull as isNull12, sql as sql27, exists } from "drizzle-orm";
|
|
716
776
|
|
|
717
777
|
// ../db/src/client.ts
|
|
718
778
|
import { mkdirSync } from "fs";
|
|
@@ -22786,7 +22846,59 @@ async function measurementPortfolioReadRoutes(app) {
|
|
|
22786
22846
|
|
|
22787
22847
|
// ../api-routes/src/visibility-report.ts
|
|
22788
22848
|
import { createHash as createHash5 } from "crypto";
|
|
22789
|
-
import { and as and19, desc as desc7, eq as eq24, gte as gte4, inArray as inArray9, isNotNull, isNull as isNull4, lte as lte4,
|
|
22849
|
+
import { and as and19, desc as desc7, eq as eq24, gte as gte4, inArray as inArray9, isNotNull, isNull as isNull4, lt as lt4, lte as lte4, or as or4 } from "drizzle-orm";
|
|
22850
|
+
|
|
22851
|
+
// ../api-routes/src/measurement-scope-options.ts
|
|
22852
|
+
function marketLinks(keys, settings) {
|
|
22853
|
+
if (!settings.marketLinks) return {};
|
|
22854
|
+
const marketKeys = [...new Set(keys ?? [])].sort();
|
|
22855
|
+
return marketKeys.length === 0 ? {} : { marketKeys };
|
|
22856
|
+
}
|
|
22857
|
+
function planScopeOptions(plan, settings) {
|
|
22858
|
+
const groupKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
22859
|
+
for (const group of plan.groups) {
|
|
22860
|
+
for (const targetKey of group.targetKeys) groupKeysForTarget.get(targetKey)?.push(group.stableKey);
|
|
22861
|
+
}
|
|
22862
|
+
const marketKeysForGroup = /* @__PURE__ */ new Map();
|
|
22863
|
+
const marketKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
22864
|
+
for (const market of plan.reportingScopes ?? []) {
|
|
22865
|
+
if (market.groupKey !== void 0) {
|
|
22866
|
+
const keys = marketKeysForGroup.get(market.groupKey) ?? [];
|
|
22867
|
+
keys.push(market.stableKey);
|
|
22868
|
+
marketKeysForGroup.set(market.groupKey, keys);
|
|
22869
|
+
}
|
|
22870
|
+
for (const edge of market.usageEdges) marketKeysForTarget.get(edge.targetKey)?.push(market.stableKey);
|
|
22871
|
+
}
|
|
22872
|
+
return [
|
|
22873
|
+
{ id: "project", label: "Project", kind: "project", targetCount: plan.targets.length },
|
|
22874
|
+
...plan.groups.map((group) => ({
|
|
22875
|
+
id: group.stableKey,
|
|
22876
|
+
label: group.label,
|
|
22877
|
+
kind: "group",
|
|
22878
|
+
targetCount: group.targetKeys.length,
|
|
22879
|
+
...group.parentGroupKey === void 0 ? {} : { parentGroupIds: [group.parentGroupKey] },
|
|
22880
|
+
...marketLinks(marketKeysForGroup.get(group.stableKey), settings)
|
|
22881
|
+
})),
|
|
22882
|
+
...(plan.reportingScopes ?? []).map((market) => ({
|
|
22883
|
+
id: market.stableKey,
|
|
22884
|
+
label: market.label,
|
|
22885
|
+
kind: "market",
|
|
22886
|
+
targetCount: new Set(market.usageEdges.map((edge) => edge.targetKey)).size,
|
|
22887
|
+
...market.groupKey === void 0 ? {} : { parentGroupIds: [market.groupKey] }
|
|
22888
|
+
})),
|
|
22889
|
+
...plan.targets.map((target) => ({
|
|
22890
|
+
id: target.stableKey,
|
|
22891
|
+
label: target.label,
|
|
22892
|
+
kind: "property",
|
|
22893
|
+
targetCount: 1,
|
|
22894
|
+
parentGroupIds: [...new Set(groupKeysForTarget.get(target.stableKey) ?? [])].sort(),
|
|
22895
|
+
...marketLinks(marketKeysForTarget.get(target.stableKey), settings)
|
|
22896
|
+
}))
|
|
22897
|
+
];
|
|
22898
|
+
}
|
|
22899
|
+
function simpleScopeOptions() {
|
|
22900
|
+
return [{ id: "project", label: "Project", kind: "project", targetCount: 1 }];
|
|
22901
|
+
}
|
|
22790
22902
|
|
|
22791
22903
|
// ../api-routes/src/visibility-report-reader.ts
|
|
22792
22904
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -22797,9 +22909,12 @@ var VisibilityReportCursorError = class extends Error {
|
|
|
22797
22909
|
}
|
|
22798
22910
|
};
|
|
22799
22911
|
var VisibilityReportScopeError = class extends Error {
|
|
22800
|
-
|
|
22912
|
+
/** Set only when the selected scope or market key is absent from the frozen definition. */
|
|
22913
|
+
details;
|
|
22914
|
+
constructor(message, details) {
|
|
22801
22915
|
super(message);
|
|
22802
22916
|
this.name = "VisibilityReportScopeError";
|
|
22917
|
+
this.details = details;
|
|
22803
22918
|
}
|
|
22804
22919
|
};
|
|
22805
22920
|
var ALL_CLASSES = ["branded", "non-brand", "unknown"];
|
|
@@ -22840,13 +22955,13 @@ function scopeResolution(definition, selection) {
|
|
|
22840
22955
|
const key = selection.scopeKey;
|
|
22841
22956
|
if (!key) throw new VisibilityReportScopeError(`${selection.scope} scope requires a scope key.`);
|
|
22842
22957
|
const scoped = definition.scopeOptions.find((candidate) => candidate.kind === selection.scope && candidate.id === key);
|
|
22843
|
-
if (!scoped) throw new VisibilityReportScopeError(`${selection.scope} scope "${key}" is not in this frozen definition
|
|
22958
|
+
if (!scoped) throw new VisibilityReportScopeError(`${selection.scope} scope "${key}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: selection.scope, key });
|
|
22844
22959
|
option = scoped;
|
|
22845
22960
|
if (selection.scope === "property") {
|
|
22846
22961
|
edgeIds = new Set(definition.edges.filter((edge) => edge.targetKey === key).map((edge) => edge.id));
|
|
22847
22962
|
} else if (selection.scope === "group") {
|
|
22848
22963
|
const group = definition.groups.find((candidate) => candidate.id === key);
|
|
22849
|
-
if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition
|
|
22964
|
+
if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: "group", key });
|
|
22850
22965
|
const targetKeys = new Set(group.targetKeys);
|
|
22851
22966
|
edgeIds = new Set(definition.edges.filter((edge) => targetKeys.has(edge.targetKey)).map((edge) => edge.id));
|
|
22852
22967
|
} else {
|
|
@@ -22859,7 +22974,7 @@ function scopeResolution(definition, selection) {
|
|
|
22859
22974
|
const marketKey = selection.scope === "market" ? selection.scopeKey : selection.marketKey;
|
|
22860
22975
|
if (marketKey === void 0) return { option, edgeIds };
|
|
22861
22976
|
const market = definition.scopeOptions.find((candidate) => candidate.kind === "market" && candidate.id === marketKey);
|
|
22862
|
-
if (!market) throw new VisibilityReportScopeError(`Market "${marketKey}" is not in this frozen definition
|
|
22977
|
+
if (!market) throw new VisibilityReportScopeError(`Market "${marketKey}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-market"], kind: "market", key: marketKey });
|
|
22863
22978
|
return {
|
|
22864
22979
|
option,
|
|
22865
22980
|
market,
|
|
@@ -22873,7 +22988,7 @@ function scopeTargetKeys(definition, selection) {
|
|
|
22873
22988
|
if (selection.scope === "property") return [resolution.option.id];
|
|
22874
22989
|
if (selection.scope === "group") {
|
|
22875
22990
|
const group = definition.groups.find((candidate) => candidate.id === resolution.option.id);
|
|
22876
|
-
if (!group) throw new VisibilityReportScopeError(`Group "${resolution.option.id}" is not in this frozen definition
|
|
22991
|
+
if (!group) throw new VisibilityReportScopeError(`Group "${resolution.option.id}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: "group", key: resolution.option.id });
|
|
22877
22992
|
return [...group.targetKeys].sort(compareText14);
|
|
22878
22993
|
}
|
|
22879
22994
|
return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
|
|
@@ -23254,17 +23369,69 @@ function filterOptions(definition, selection, run) {
|
|
|
23254
23369
|
}
|
|
23255
23370
|
return { providers, models, locations };
|
|
23256
23371
|
}
|
|
23257
|
-
function
|
|
23258
|
-
if (!previous) return { state: "first", comparedRunId: null };
|
|
23372
|
+
function pairContinuity(previous, current) {
|
|
23259
23373
|
const currentLegacy = current.definition.provenance.kind === "legacy-simple";
|
|
23260
23374
|
const previousLegacy = previous.definition.provenance.kind === "legacy-simple";
|
|
23261
|
-
if (currentLegacy || previousLegacy) return
|
|
23375
|
+
if (currentLegacy || previousLegacy) return "legacy-unknown";
|
|
23262
23376
|
const sameDefinition = current.definitionId !== null && previous.definitionId !== null && (current.definitionId === previous.definitionId || current.comparableDefinitionIds.includes(previous.definitionId));
|
|
23263
|
-
if (!sameDefinition) return
|
|
23377
|
+
if (!sameDefinition) return "definition-changed";
|
|
23264
23378
|
if (current.modelFingerprint === null || previous.modelFingerprint === null || current.modelFingerprint !== previous.modelFingerprint) {
|
|
23265
|
-
return
|
|
23379
|
+
return "model-changed";
|
|
23266
23380
|
}
|
|
23267
|
-
return
|
|
23381
|
+
return "comparable";
|
|
23382
|
+
}
|
|
23383
|
+
function continuity(previous, current) {
|
|
23384
|
+
if (!previous) return { state: "first", comparedRunId: null };
|
|
23385
|
+
return { state: pairContinuity(previous, current), comparedRunId: previous.id };
|
|
23386
|
+
}
|
|
23387
|
+
function comparedRun(run) {
|
|
23388
|
+
return { id: run.id, createdAt: run.createdAt, completedAt: run.completedAt };
|
|
23389
|
+
}
|
|
23390
|
+
function previousSummaries(run, selection, classes) {
|
|
23391
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
23392
|
+
for (const queryClass of classes) {
|
|
23393
|
+
try {
|
|
23394
|
+
const { candidates } = candidatesFor(run, selection, queryClass);
|
|
23395
|
+
summaries.set(queryClass, summary(candidates, run.definition, scopeTargetKeys(run.definition, selection)));
|
|
23396
|
+
} catch (error) {
|
|
23397
|
+
if (!(error instanceof VisibilityReportScopeError)) return void 0;
|
|
23398
|
+
summaries.set(queryClass, null);
|
|
23399
|
+
}
|
|
23400
|
+
}
|
|
23401
|
+
return summaries;
|
|
23402
|
+
}
|
|
23403
|
+
function comparisonBasis(selectedRun, previous, selection, classes) {
|
|
23404
|
+
if (previous !== null && "unreadable" in previous) return void 0;
|
|
23405
|
+
if (!selectedRun) return { state: "unavailable", reason: "no-selected-run", previousRun: null };
|
|
23406
|
+
if (selectedRun.scoped) return { state: "unavailable", reason: "scoped-run", previousRun: null };
|
|
23407
|
+
if (previous === null) return { state: "unavailable", reason: "no-previous-run", previousRun: null };
|
|
23408
|
+
if ("incomparable" in previous) return { state: "unavailable", reason: "definition-changed", previousRun: previous.incomparable };
|
|
23409
|
+
const pair = pairContinuity(previous.run, selectedRun);
|
|
23410
|
+
if (pair !== "comparable") return { state: "unavailable", reason: pair, previousRun: comparedRun(previous.run) };
|
|
23411
|
+
if (selectedRun.state === "partial" || previous.run.state === "partial") {
|
|
23412
|
+
return { state: "unavailable", reason: "partial-run", previousRun: comparedRun(previous.run) };
|
|
23413
|
+
}
|
|
23414
|
+
const summaries = previousSummaries(previous.run, selection, classes);
|
|
23415
|
+
return summaries === void 0 ? void 0 : { state: "available", previous: previous.run, summaries };
|
|
23416
|
+
}
|
|
23417
|
+
function rateChange(current, previous) {
|
|
23418
|
+
if (current.rate === null) {
|
|
23419
|
+
return { state: "unavailable", reason: current.reason === "not-applicable" ? "not-applicable" : "current-unavailable" };
|
|
23420
|
+
}
|
|
23421
|
+
if (previous === null || previous.rate === null) return { state: "unavailable", reason: "previous-unavailable" };
|
|
23422
|
+
return { state: "available", previous, delta: current.rate - previous.rate };
|
|
23423
|
+
}
|
|
23424
|
+
function populationComparison(basis, current, selection, queryClass) {
|
|
23425
|
+
if (basis.state === "unavailable") return { state: "unavailable", reason: basis.reason, previousRun: basis.previousRun };
|
|
23426
|
+
const previous = basis.summaries.get(queryClass) ?? null;
|
|
23427
|
+
const currentReach = selection.scope === "property" ? unavailable4("not-applicable") : current.propertyReach;
|
|
23428
|
+
return {
|
|
23429
|
+
state: "available",
|
|
23430
|
+
previousRun: comparedRun(basis.previous),
|
|
23431
|
+
mentionCoverage: rateChange(current.mentionCoverage, previous?.mentionCoverage ?? null),
|
|
23432
|
+
citationCoverage: rateChange(current.citationCoverage, previous?.citationCoverage ?? null),
|
|
23433
|
+
propertyReach: rateChange(currentReach, previous?.propertyReach ?? null)
|
|
23434
|
+
};
|
|
23268
23435
|
}
|
|
23269
23436
|
function inTime(run, selection) {
|
|
23270
23437
|
return (selection.from === void 0 || run.createdAt >= selection.from) && (selection.to === void 0 || run.createdAt <= selection.to);
|
|
@@ -23305,8 +23472,12 @@ function buildVisibilityReport(input) {
|
|
|
23305
23472
|
}
|
|
23306
23473
|
runCandidates.set(run.id, byClass);
|
|
23307
23474
|
}
|
|
23475
|
+
const basis = input.previous === void 0 ? void 0 : comparisonBasis(selectedRun, input.previous, input.selection, classes);
|
|
23308
23476
|
const populations = classes.map((queryClass) => {
|
|
23309
|
-
if (!selectedRun)
|
|
23477
|
+
if (!selectedRun) {
|
|
23478
|
+
const empty = emptyPopulation(queryClass, definition, input);
|
|
23479
|
+
return basis === void 0 ? empty : { ...empty, comparison: populationComparison(basis, empty.summary, input.selection, queryClass) };
|
|
23480
|
+
}
|
|
23310
23481
|
const selected = runCandidates.get(selectedRun.id)?.get(queryClass) ?? [];
|
|
23311
23482
|
const trend = runs2.map((run, index2) => {
|
|
23312
23483
|
const candidates = runCandidates.get(run.id)?.get(queryClass) ?? [];
|
|
@@ -23325,10 +23496,12 @@ function buildVisibilityReport(input) {
|
|
|
23325
23496
|
});
|
|
23326
23497
|
const rows = queryRows(selected, definition, input.selection);
|
|
23327
23498
|
const evidence = evidenceRows(selected, definition, input.selection);
|
|
23499
|
+
const selectedSummary = summary(selected, definition, populationTargetKeys);
|
|
23328
23500
|
return {
|
|
23329
23501
|
queryClass,
|
|
23330
|
-
summary:
|
|
23502
|
+
summary: selectedSummary,
|
|
23331
23503
|
trend,
|
|
23504
|
+
...basis === void 0 ? {} : { comparison: populationComparison(basis, selectedSummary, input.selection, queryClass) },
|
|
23332
23505
|
queries: page2(rows, (row) => [row.queryKey, row.provider, row.model ?? "", row.location ?? ""].join("\0"), "queries", queryClass, input, selectedRun),
|
|
23333
23506
|
evidence: page2(evidence, (row) => [row.answerId, row.runId].join("\0"), "evidence", queryClass, input, selectedRun),
|
|
23334
23507
|
competitorAvailability: definition.competitorAvailability,
|
|
@@ -23524,56 +23697,10 @@ function v2Definition(plan, revision, manifest, run) {
|
|
|
23524
23697
|
location: slot.context?.label ?? null
|
|
23525
23698
|
};
|
|
23526
23699
|
});
|
|
23527
|
-
const groupKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
23528
|
-
for (const group of plan.groups) {
|
|
23529
|
-
for (const targetKey of group.targetKeys) groupKeysForTarget.get(targetKey)?.push(group.stableKey);
|
|
23530
|
-
}
|
|
23531
|
-
const marketKeysForGroup = /* @__PURE__ */ new Map();
|
|
23532
|
-
const marketKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
23533
|
-
for (const market of plan.reportingScopes ?? []) {
|
|
23534
|
-
if (market.groupKey !== void 0) {
|
|
23535
|
-
const keys = marketKeysForGroup.get(market.groupKey) ?? [];
|
|
23536
|
-
keys.push(market.stableKey);
|
|
23537
|
-
marketKeysForGroup.set(market.groupKey, keys);
|
|
23538
|
-
}
|
|
23539
|
-
for (const edge of market.usageEdges) marketKeysForTarget.get(edge.targetKey)?.push(market.stableKey);
|
|
23540
|
-
}
|
|
23541
|
-
const scopeOptions = [
|
|
23542
|
-
{ id: "project", label: "Project", kind: "project", targetCount: plan.targets.length },
|
|
23543
|
-
...plan.groups.map((group) => {
|
|
23544
|
-
const marketKeys = [...new Set(marketKeysForGroup.get(group.stableKey) ?? [])].sort();
|
|
23545
|
-
return {
|
|
23546
|
-
id: group.stableKey,
|
|
23547
|
-
label: group.label,
|
|
23548
|
-
kind: "group",
|
|
23549
|
-
targetCount: group.targetKeys.length,
|
|
23550
|
-
...group.parentGroupKey === void 0 ? {} : { parentGroupIds: [group.parentGroupKey] },
|
|
23551
|
-
...marketKeys.length === 0 ? {} : { marketKeys }
|
|
23552
|
-
};
|
|
23553
|
-
}),
|
|
23554
|
-
...plan.reportingScopes?.map((market) => ({
|
|
23555
|
-
id: market.stableKey,
|
|
23556
|
-
label: market.label,
|
|
23557
|
-
kind: "market",
|
|
23558
|
-
targetCount: new Set(market.usageEdges.map((edge) => edge.targetKey)).size,
|
|
23559
|
-
...market.groupKey === void 0 ? {} : { parentGroupIds: [market.groupKey] }
|
|
23560
|
-
})) ?? [],
|
|
23561
|
-
...plan.targets.map((target) => {
|
|
23562
|
-
const marketKeys = [...new Set(marketKeysForTarget.get(target.stableKey) ?? [])].sort();
|
|
23563
|
-
return {
|
|
23564
|
-
id: target.stableKey,
|
|
23565
|
-
label: target.label,
|
|
23566
|
-
kind: "property",
|
|
23567
|
-
targetCount: 1,
|
|
23568
|
-
parentGroupIds: [...new Set(groupKeysForTarget.get(target.stableKey) ?? [])].sort(),
|
|
23569
|
-
...marketKeys.length === 0 ? {} : { marketKeys }
|
|
23570
|
-
};
|
|
23571
|
-
})
|
|
23572
|
-
];
|
|
23573
23700
|
return {
|
|
23574
23701
|
revision,
|
|
23575
23702
|
provenance: { kind: "frozen-advanced", definitionRevision: revision },
|
|
23576
|
-
scopeOptions,
|
|
23703
|
+
scopeOptions: planScopeOptions(plan, { marketLinks: true }),
|
|
23577
23704
|
targets: plan.targets.map((target) => ({ id: target.stableKey, label: target.label, mentionEligible: !target.mentionNotApplicable })),
|
|
23578
23705
|
groups: plan.groups.map((group) => ({ id: group.stableKey, label: group.label, targetKeys: group.targetKeys })),
|
|
23579
23706
|
competitorAvailability: { state: "available" },
|
|
@@ -23623,6 +23750,7 @@ function advancedRun(run, version, plan, snapshots, comparableDefinitionIds, inc
|
|
|
23623
23750
|
completedAt: run.finishedAt,
|
|
23624
23751
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23625
23752
|
probe: false,
|
|
23753
|
+
scoped: run.measurementScope !== null,
|
|
23626
23754
|
definition: v2Definition(plan, version.revision, populated.manifest, run),
|
|
23627
23755
|
definitionId: version.id,
|
|
23628
23756
|
comparableDefinitionIds,
|
|
@@ -23634,7 +23762,7 @@ function simpleScope(label, mentionEligible) {
|
|
|
23634
23762
|
return {
|
|
23635
23763
|
revision: null,
|
|
23636
23764
|
provenance: { kind: "legacy-simple", definitionRevision: null },
|
|
23637
|
-
scopeOptions:
|
|
23765
|
+
scopeOptions: simpleScopeOptions(),
|
|
23638
23766
|
targets: [{ id: "project", label, mentionEligible }],
|
|
23639
23767
|
groups: [],
|
|
23640
23768
|
competitorAvailability: { state: "unavailable", reason: "frozen-competitor-identity-missing" }
|
|
@@ -23734,6 +23862,7 @@ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
|
|
|
23734
23862
|
completedAt: run.finishedAt,
|
|
23735
23863
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23736
23864
|
probe: false,
|
|
23865
|
+
scoped: run.measurementScope !== null,
|
|
23737
23866
|
definition: {
|
|
23738
23867
|
...simpleScope(definition.identity.displayName, matcher.keys.size > 0),
|
|
23739
23868
|
provenance: { kind: "frozen-simple", definitionRevision: null },
|
|
@@ -23772,6 +23901,7 @@ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
|
|
|
23772
23901
|
completedAt: run.finishedAt,
|
|
23773
23902
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23774
23903
|
probe: false,
|
|
23904
|
+
scoped: run.measurementScope !== null,
|
|
23775
23905
|
definition: {
|
|
23776
23906
|
...simpleScope(project.displayName, true),
|
|
23777
23907
|
slots,
|
|
@@ -23874,7 +24004,7 @@ function completedVisibilityRuns(db, projectId, planless, query) {
|
|
|
23874
24004
|
eq24(runs.projectId, projectId),
|
|
23875
24005
|
eq24(runs.kind, RunKinds["answer-visibility"]),
|
|
23876
24006
|
inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
23877
|
-
|
|
24007
|
+
notProbeRun(),
|
|
23878
24008
|
planless ? isNull4(runs.measurementPlanVersionId) : isNotNull(runs.measurementPlanVersionId),
|
|
23879
24009
|
// A scoped spot check is never a whole-project sweep. It can be inspected
|
|
23880
24010
|
// by its exact id, but must not win the default latest-result selection.
|
|
@@ -23884,6 +24014,27 @@ function completedVisibilityRuns(db, projectId, planless, query) {
|
|
|
23884
24014
|
query.to === void 0 ? void 0 : lte4(runs.createdAt, query.to)
|
|
23885
24015
|
)).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(VISIBILITY_REPORT_MAX_RUNS).all();
|
|
23886
24016
|
}
|
|
24017
|
+
function previousEligibleVisibilityRun(db, projectId, planless, selected) {
|
|
24018
|
+
return db.select().from(runs).where(and19(
|
|
24019
|
+
eq24(runs.projectId, projectId),
|
|
24020
|
+
eq24(runs.kind, RunKinds["answer-visibility"]),
|
|
24021
|
+
inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24022
|
+
notProbeRun(),
|
|
24023
|
+
planless ? isNull4(runs.measurementPlanVersionId) : isNotNull(runs.measurementPlanVersionId),
|
|
24024
|
+
isNull4(runs.measurementScope),
|
|
24025
|
+
or4(
|
|
24026
|
+
lt4(runs.createdAt, selected.createdAt),
|
|
24027
|
+
and19(eq24(runs.createdAt, selected.createdAt), lt4(runs.id, selected.id))
|
|
24028
|
+
)
|
|
24029
|
+
)).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(1).get();
|
|
24030
|
+
}
|
|
24031
|
+
function readPreviousSweep(read) {
|
|
24032
|
+
try {
|
|
24033
|
+
return read();
|
|
24034
|
+
} catch {
|
|
24035
|
+
return { unreadable: true };
|
|
24036
|
+
}
|
|
24037
|
+
}
|
|
23887
24038
|
function parseV2Versions(rows) {
|
|
23888
24039
|
const versions = /* @__PURE__ */ new Map();
|
|
23889
24040
|
for (const row of rows) {
|
|
@@ -23924,7 +24075,7 @@ function pendingAssignments(activePlan, measuredPlan, measuredDefinition) {
|
|
|
23924
24075
|
const measuredSignatures = new Set(measuredPlan.assignments.filter((assignment) => measuredEdges.has([assignment.executionNodeKey, assignment.targetKey, assignment.queryId].join("\0"))).map((assignment) => assignmentSignature(measuredPlan, assignment)));
|
|
23925
24076
|
return activePlan.assignments.filter((assignment) => !measuredSignatures.has(assignmentSignature(activePlan, assignment))).length;
|
|
23926
24077
|
}
|
|
23927
|
-
function advancedReaderInput(db, projectId, active, query) {
|
|
24078
|
+
function advancedReaderInput(db, projectId, active, query, includeComparison) {
|
|
23928
24079
|
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) throw new Error("Expected a v2 active plan");
|
|
23929
24080
|
const allVersionRows = db.select().from(measurementPlanVersions).where(eq24(measurementPlanVersions.projectId, projectId)).all();
|
|
23930
24081
|
const allVersionRowsById = new Map(allVersionRows.map((row) => [row.id, row]));
|
|
@@ -23954,7 +24105,7 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23954
24105
|
const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
|
|
23955
24106
|
const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
|
|
23956
24107
|
const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
|
|
23957
|
-
const
|
|
24108
|
+
const materialize = (candidate, includeEvidence) => {
|
|
23958
24109
|
const usePresentation = presentationComparableIds.has(candidate.source.row.id);
|
|
23959
24110
|
const version = usePresentation ? presentationVersion : candidate.source.row;
|
|
23960
24111
|
const plan = usePresentation ? presentationPlan : candidate.source.plan;
|
|
@@ -23965,10 +24116,25 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23965
24116
|
plan,
|
|
23966
24117
|
snapshots,
|
|
23967
24118
|
usePresentation ? [...presentationComparableIds] : comparableVersionIds(allVersionRowsById, candidate.source.row.id),
|
|
23968
|
-
|
|
24119
|
+
includeEvidence
|
|
23969
24120
|
);
|
|
23970
|
-
}
|
|
24121
|
+
};
|
|
24122
|
+
const candidates = sourceCandidates.map((candidate) => materialize(candidate, candidate.run.id === selectedSource?.run.id));
|
|
23971
24123
|
const selectedDefinition = candidates.find((candidate) => candidate.id === selectedSource?.run.id)?.definition;
|
|
24124
|
+
const previousInput = () => {
|
|
24125
|
+
if (selectedSource === void 0 || selectedSource.run.measurementScope !== null) return null;
|
|
24126
|
+
return readPreviousSweep(() => {
|
|
24127
|
+
const predecessor = previousEligibleVisibilityRun(db, projectId, false, selectedSource.run);
|
|
24128
|
+
if (predecessor === void 0) return null;
|
|
24129
|
+
const loaded = candidates.find((candidate) => candidate.id === predecessor.id);
|
|
24130
|
+
if (loaded !== void 0) return { run: loaded };
|
|
24131
|
+
const source = predecessor.measurementPlanVersionId === null ? void 0 : versions.get(predecessor.measurementPlanVersionId);
|
|
24132
|
+
if (source === void 0) {
|
|
24133
|
+
return { incomparable: { id: predecessor.id, createdAt: predecessor.createdAt, completedAt: predecessor.finishedAt } };
|
|
24134
|
+
}
|
|
24135
|
+
return { run: materialize({ run: predecessor, source }, false) };
|
|
24136
|
+
});
|
|
24137
|
+
};
|
|
23972
24138
|
return {
|
|
23973
24139
|
mode: "advanced",
|
|
23974
24140
|
activeRevision: active.version.revision,
|
|
@@ -23976,35 +24142,52 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23976
24142
|
selection: query,
|
|
23977
24143
|
activeDefinition: activeV2Definition(presentationPlan, presentationVersion.revision),
|
|
23978
24144
|
...preferredSource === void 0 ? {} : { preferredRunId: preferredSource.run.id },
|
|
23979
|
-
runs: candidates
|
|
24145
|
+
runs: candidates,
|
|
24146
|
+
...includeComparison ? { previous: previousInput() } : {}
|
|
23980
24147
|
};
|
|
23981
24148
|
}
|
|
23982
|
-
function
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
eq24(simpleMeasurementDefinitions.projectId,
|
|
23986
|
-
inArray9(simpleMeasurementDefinitions.runId,
|
|
24149
|
+
function frozenSimpleDefinitions(db, projectId, runIds) {
|
|
24150
|
+
if (runIds.length === 0) return /* @__PURE__ */ new Map();
|
|
24151
|
+
return new Map(db.select().from(simpleMeasurementDefinitions).where(and19(
|
|
24152
|
+
eq24(simpleMeasurementDefinitions.projectId, projectId),
|
|
24153
|
+
inArray9(simpleMeasurementDefinitions.runId, [...runIds])
|
|
23987
24154
|
)).all().map((row) => [row.runId, row.definition]));
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
23992
|
-
|
|
23993
|
-
|
|
23994
|
-
|
|
24155
|
+
}
|
|
24156
|
+
function simpleRunInput(db, project, run, definition, includeEvidence) {
|
|
24157
|
+
const snapshots = loadVisibilitySnapshots(db, [run.id]).get(run.id) ?? [];
|
|
24158
|
+
return definition ? frozenSimpleRun(run, definition, snapshots, includeEvidence) : legacySimpleRun(project, run, snapshots, includeEvidence);
|
|
24159
|
+
}
|
|
24160
|
+
function simpleReaderInput(db, project, query, includeComparison) {
|
|
24161
|
+
const sourceRuns = completedVisibilityRuns(db, project.id, true, query);
|
|
24162
|
+
const frozen = frozenSimpleDefinitions(db, project.id, sourceRuns.map((run) => run.id));
|
|
24163
|
+
const selectedRun = sourceRuns.at(0);
|
|
24164
|
+
const candidates = sourceRuns.map((run) => simpleRunInput(db, project, run, frozen.get(run.id), run.id === selectedRun?.id));
|
|
23995
24165
|
if (query.runId !== void 0 && !candidates.some((run) => run.id === query.runId)) {
|
|
23996
24166
|
throw validationError(`Measurement run "${query.runId}" is not an eligible simple result.`);
|
|
23997
24167
|
}
|
|
24168
|
+
const previousInput = () => {
|
|
24169
|
+
if (selectedRun === void 0 || selectedRun.measurementScope !== null) return null;
|
|
24170
|
+
return readPreviousSweep(() => {
|
|
24171
|
+
const predecessor = previousEligibleVisibilityRun(db, project.id, true, selectedRun);
|
|
24172
|
+
if (predecessor === void 0) return null;
|
|
24173
|
+
const loaded = candidates.find((candidate) => candidate.id === predecessor.id);
|
|
24174
|
+
if (loaded !== void 0) return { run: loaded };
|
|
24175
|
+
const definition = frozenSimpleDefinitions(db, project.id, [predecessor.id]).get(predecessor.id);
|
|
24176
|
+
return { run: simpleRunInput(db, project, predecessor, definition, false) };
|
|
24177
|
+
});
|
|
24178
|
+
};
|
|
23998
24179
|
return {
|
|
23999
24180
|
mode: "simple",
|
|
24000
24181
|
activeRevision: null,
|
|
24001
24182
|
pendingAssignmentCount: 0,
|
|
24002
24183
|
selection: query,
|
|
24003
24184
|
activeDefinition: simpleActiveDefinition(project),
|
|
24004
|
-
runs: candidates
|
|
24185
|
+
runs: candidates,
|
|
24186
|
+
...includeComparison ? { previous: previousInput() } : {}
|
|
24005
24187
|
};
|
|
24006
24188
|
}
|
|
24007
|
-
function readVisibilityReport(db, project, rawQuery) {
|
|
24189
|
+
function readVisibilityReport(db, project, rawQuery, options = {}) {
|
|
24190
|
+
const includeComparison = options.includeComparison ?? true;
|
|
24008
24191
|
const query = parseQuery(rawQuery);
|
|
24009
24192
|
const active = activeMeasurementPlan(db, project.id);
|
|
24010
24193
|
const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
|
|
@@ -24012,13 +24195,12 @@ function readVisibilityReport(db, project, rawQuery) {
|
|
|
24012
24195
|
if (mode === "advanced") {
|
|
24013
24196
|
if (active === null) throw validationError("This project has no advanced measurement plan.");
|
|
24014
24197
|
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) return unsupportedAdvancedResponse(query, active);
|
|
24015
|
-
return buildVisibilityReport(advancedReaderInput(db, project.id, active, query));
|
|
24198
|
+
return buildVisibilityReport(advancedReaderInput(db, project.id, active, query, includeComparison));
|
|
24016
24199
|
}
|
|
24017
|
-
return buildVisibilityReport(simpleReaderInput(db, project, query));
|
|
24200
|
+
return buildVisibilityReport(simpleReaderInput(db, project, query, includeComparison));
|
|
24018
24201
|
} catch (error) {
|
|
24019
|
-
if (error instanceof
|
|
24020
|
-
|
|
24021
|
-
}
|
|
24202
|
+
if (error instanceof VisibilityReportScopeError) throw validationError(error.message, error.details);
|
|
24203
|
+
if (error instanceof VisibilityReportCursorError) throw validationError(error.message);
|
|
24022
24204
|
throw error;
|
|
24023
24205
|
}
|
|
24024
24206
|
}
|
|
@@ -24031,7 +24213,7 @@ async function visibilityReportRoutes(app) {
|
|
|
24031
24213
|
|
|
24032
24214
|
// ../api-routes/src/query-tracking.ts
|
|
24033
24215
|
import crypto20 from "crypto";
|
|
24034
|
-
import { and as and20, desc as desc8, eq as eq25, inArray as inArray10, isNotNull as isNotNull2, isNull as isNull5, ne as
|
|
24216
|
+
import { and as and20, desc as desc8, eq as eq25, inArray as inArray10, isNotNull as isNotNull2, isNull as isNull5, ne as ne3 } from "drizzle-orm";
|
|
24035
24217
|
var SOURCE_LIMIT = 100;
|
|
24036
24218
|
var MAX_REVIEW_AGE_MS = 15 * 60 * 1e3;
|
|
24037
24219
|
var MAX_REVIEW_FUTURE_SKEW_MS = 60 * 1e3;
|
|
@@ -24214,7 +24396,7 @@ function simpleMeasuredAtByQuery(db, project, queryRows2, opts) {
|
|
|
24214
24396
|
eq25(runs.projectId, project.id),
|
|
24215
24397
|
eq25(runs.kind, RunKinds["answer-visibility"]),
|
|
24216
24398
|
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24217
|
-
|
|
24399
|
+
ne3(runs.trigger, RunTriggers.probe),
|
|
24218
24400
|
isNull5(runs.measurementScope),
|
|
24219
24401
|
isNull5(runs.measurementPlanVersionId)
|
|
24220
24402
|
)).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(SOURCE_LIMIT).all();
|
|
@@ -24336,7 +24518,7 @@ function measuredAtByQuery(db, state, opts) {
|
|
|
24336
24518
|
eq25(runs.projectId, project.id),
|
|
24337
24519
|
eq25(runs.kind, RunKinds["answer-visibility"]),
|
|
24338
24520
|
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24339
|
-
|
|
24521
|
+
ne3(runs.trigger, RunTriggers.probe),
|
|
24340
24522
|
isNull5(runs.measurementScope),
|
|
24341
24523
|
isNotNull2(runs.measurementPlanVersionId)
|
|
24342
24524
|
)).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(SOURCE_LIMIT).all();
|
|
@@ -24553,6 +24735,8 @@ function workspaceDto(db, state, opts) {
|
|
|
24553
24735
|
...group.parentGroupKey ? { parentGroupKey: group.parentGroupKey } : {}
|
|
24554
24736
|
})) ?? [],
|
|
24555
24737
|
markets: plan?.reportingScopes?.map((scope) => ({ stableKey: scope.stableKey, label: scope.label, usageEdges: scope.usageEdges, ...scope.groupKey ? { groupKey: scope.groupKey } : {} })) ?? [],
|
|
24738
|
+
// Tracked assignments have no market intersection, so Groups and Properties carry no market links.
|
|
24739
|
+
scopeOptions: plan ? planScopeOptions(plan, { marketLinks: false }) : simpleScopeOptions(),
|
|
24556
24740
|
tracked: trackedRows(db, state, rows, plan, opts),
|
|
24557
24741
|
savedSources: savedSources(db, state.project.id)
|
|
24558
24742
|
});
|
|
@@ -26184,7 +26368,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
26184
26368
|
}
|
|
26185
26369
|
|
|
26186
26370
|
// ../api-routes/src/history.ts
|
|
26187
|
-
import { and as and22, asc as asc3, count, desc as desc9, eq as eq27, gte as gte5, inArray as inArray11, like, ne as
|
|
26371
|
+
import { and as and22, asc as asc3, count, desc as desc9, eq as eq27, gte as gte5, inArray as inArray11, like, ne as ne4, or as or5 } from "drizzle-orm";
|
|
26188
26372
|
|
|
26189
26373
|
// ../api-routes/src/notification-redaction.ts
|
|
26190
26374
|
var REDACTED_URL = {
|
|
@@ -26263,7 +26447,7 @@ async function historyRoutes(app) {
|
|
|
26263
26447
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
26264
26448
|
const project = resolveProject(app.db, request.params.name);
|
|
26265
26449
|
const filters = [eq27(auditLog.projectId, project.id)];
|
|
26266
|
-
if (request.operatorAccess !== true) filters.push(
|
|
26450
|
+
if (request.operatorAccess !== true) filters.push(ne4(auditLog.entityType, "telemetry"));
|
|
26267
26451
|
addAuditHistoryFilters(filters, request.query);
|
|
26268
26452
|
const rows = app.db.select().from(auditLog).where(and22(...filters)).orderBy(desc9(auditLog.createdAt), desc9(auditLog.id)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
|
|
26269
26453
|
return reply.send(rows.map(formatAuditEntry));
|
|
@@ -26271,7 +26455,7 @@ async function historyRoutes(app) {
|
|
|
26271
26455
|
app.get("/history", async (request, reply) => {
|
|
26272
26456
|
const scopedProjectId = request.apiKey?.projectId;
|
|
26273
26457
|
const filters = scopedProjectId ? [eq27(auditLog.projectId, scopedProjectId)] : [];
|
|
26274
|
-
if (request.operatorAccess !== true) filters.push(
|
|
26458
|
+
if (request.operatorAccess !== true) filters.push(ne4(auditLog.entityType, "telemetry"));
|
|
26275
26459
|
addAuditHistoryFilters(filters, request.query);
|
|
26276
26460
|
const rows = app.db.select().from(auditLog).where(filters.length > 0 ? and22(...filters) : void 0).orderBy(desc9(auditLog.createdAt), desc9(auditLog.id)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
|
|
26277
26461
|
return reply.send(rows.map(formatAuditEntry));
|
|
@@ -26572,7 +26756,7 @@ function formatAuditEntry(row) {
|
|
|
26572
26756
|
function addAuditHistoryFilters(filters, query) {
|
|
26573
26757
|
if (query.since && !Number.isNaN(Date.parse(query.since))) filters.push(gte5(auditLog.createdAt, query.since));
|
|
26574
26758
|
if (query.action) filters.push(eq27(auditLog.action, query.action));
|
|
26575
|
-
if (query.actor === "api") filters.push(
|
|
26759
|
+
if (query.actor === "api") filters.push(or5(
|
|
26576
26760
|
eq27(auditLog.actor, "api"),
|
|
26577
26761
|
like(auditLog.actor, "api-key:%"),
|
|
26578
26762
|
like(auditLog.actor, "user:%")
|
|
@@ -26593,7 +26777,7 @@ function parseOptionalPositiveInt(value, max2) {
|
|
|
26593
26777
|
}
|
|
26594
26778
|
|
|
26595
26779
|
// ../api-routes/src/analytics.ts
|
|
26596
|
-
import { and as and23, desc as desc10, eq as eq28, gte as gte6, inArray as inArray12, lt as
|
|
26780
|
+
import { and as and23, desc as desc10, eq as eq28, gte as gte6, inArray as inArray12, lt as lt5 } from "drizzle-orm";
|
|
26597
26781
|
|
|
26598
26782
|
// ../api-routes/src/analytics-model-attribution.ts
|
|
26599
26783
|
function logicalSweeps(observations) {
|
|
@@ -26785,7 +26969,7 @@ async function analyticsRoutes(app) {
|
|
|
26785
26969
|
eq28(runs.kind, RunKinds["answer-visibility"]),
|
|
26786
26970
|
inArray12(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
26787
26971
|
notProbeRun(),
|
|
26788
|
-
|
|
26972
|
+
lt5(runs.createdAt, cutoff)
|
|
26789
26973
|
);
|
|
26790
26974
|
const anchorSweepTimes = app.db.selectDistinct({ createdAt: runs.createdAt }).from(runs).where(anchorRunPredicate).orderBy(desc10(runs.createdAt)).limit(ANCHOR_SWEEP_SCAN_LIMIT + 1).all().map((row) => row.createdAt);
|
|
26791
26975
|
const anchorScanTruncated = anchorSweepTimes.length > ANCHOR_SWEEP_SCAN_LIMIT;
|
|
@@ -27619,7 +27803,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
27619
27803
|
}
|
|
27620
27804
|
|
|
27621
27805
|
// ../api-routes/src/report.ts
|
|
27622
|
-
import { and as and29, desc as desc14, eq as eq33, gte as gte8, inArray as inArray15, lt as
|
|
27806
|
+
import { and as and29, desc as desc14, eq as eq33, gte as gte8, inArray as inArray15, lt as lt6, lte as lte6, ne as ne5, or as or6, sql as sql11 } from "drizzle-orm";
|
|
27623
27807
|
|
|
27624
27808
|
// ../api-routes/src/content.ts
|
|
27625
27809
|
import crypto23 from "crypto";
|
|
@@ -28590,141 +28774,22 @@ function escapeHtml2(value) {
|
|
|
28590
28774
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
28591
28775
|
}
|
|
28592
28776
|
function safeHref(value) {
|
|
28593
|
-
|
|
28594
|
-
if (!trimmed) return "#";
|
|
28595
|
-
if (trimmed.startsWith("/")) return escapeHtml2(trimmed);
|
|
28596
|
-
if (/^https?:\/\//i.test(trimmed)) return escapeHtml2(trimmed);
|
|
28597
|
-
if (/^mailto:/i.test(trimmed)) return escapeHtml2(trimmed);
|
|
28598
|
-
return "#";
|
|
28599
|
-
}
|
|
28600
|
-
function summarizeQueryParams(params) {
|
|
28601
|
-
const keys = Array.from(params.keys());
|
|
28602
|
-
const total = keys.length;
|
|
28603
|
-
if (total === 0) return "";
|
|
28604
|
-
const noun = total === 1 ? "param" : "params";
|
|
28605
|
-
const tag = inferAdSource(params);
|
|
28606
|
-
return tag ? `${tag} \xB7 ${total} ${noun}` : `${total} tracking ${noun}`;
|
|
28607
|
-
}
|
|
28608
|
-
function inferAdSource(params) {
|
|
28609
|
-
if (params.has("fbclid")) return "Facebook Ad";
|
|
28610
|
-
if (params.has("gclid") || params.has("gbraid") || params.has("wbraid")) return "Google Ad";
|
|
28611
|
-
if (params.has("msclkid")) return "Microsoft Ad";
|
|
28612
|
-
if (params.has("ttclid")) return "TikTok Ad";
|
|
28613
|
-
if (params.has("li_fat_id")) return "LinkedIn Ad";
|
|
28614
|
-
if (params.has("twclid")) return "X / Twitter Ad";
|
|
28615
|
-
if (params.has("epik")) return "Pinterest Ad";
|
|
28616
|
-
for (const k of params.keys()) {
|
|
28617
|
-
if (k.startsWith("hsa_")) return "Search Ad";
|
|
28618
|
-
}
|
|
28619
|
-
const src = params.get("utm_source");
|
|
28620
|
-
const med = params.get("utm_medium");
|
|
28621
|
-
if (src && med) return `${src} / ${med}`;
|
|
28622
|
-
if (src) return `Source: ${src}`;
|
|
28623
|
-
if (med) return `Medium: ${med}`;
|
|
28624
|
-
return null;
|
|
28777
|
+
return escapeHtml2(safeLinkHref(value));
|
|
28625
28778
|
}
|
|
28626
28779
|
function formatLandingPageHtml(raw) {
|
|
28627
|
-
const
|
|
28628
|
-
const
|
|
28629
|
-
|
|
28630
|
-
|
|
28631
|
-
const pathHtml = `<span class="page-path">${escapeHtml2(path8 || "/")}</span>`;
|
|
28632
|
-
if (!query) return pathHtml;
|
|
28633
|
-
let summary2 = "";
|
|
28634
|
-
try {
|
|
28635
|
-
summary2 = summarizeQueryParams(new URLSearchParams(query));
|
|
28636
|
-
} catch {
|
|
28637
|
-
summary2 = "tracking params";
|
|
28638
|
-
}
|
|
28639
|
-
if (!summary2) return pathHtml;
|
|
28640
|
-
return `${pathHtml}<span class="page-query" title="${escapeHtml2(value)}">${escapeHtml2(summary2)}</span>`;
|
|
28641
|
-
}
|
|
28642
|
-
function gscDateRange(report) {
|
|
28643
|
-
const summary2 = report.executiveSummary.gsc;
|
|
28644
|
-
const gsc = report.gsc;
|
|
28645
|
-
const start = summary2?.periodStart || gsc?.periodStart || gsc?.trend[0]?.date || "";
|
|
28646
|
-
const end = summary2?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
|
|
28647
|
-
return formatDateRange(start, end);
|
|
28648
|
-
}
|
|
28649
|
-
function pluralize(count4, singular, plural = `${singular}s`) {
|
|
28650
|
-
return count4 === 1 ? singular : plural;
|
|
28651
|
-
}
|
|
28652
|
-
var PROVIDER_DISPLAY_NAMES = {
|
|
28653
|
-
gemini: "Gemini",
|
|
28654
|
-
openai: "ChatGPT",
|
|
28655
|
-
claude: "Claude",
|
|
28656
|
-
perplexity: "Perplexity",
|
|
28657
|
-
local: "Local model",
|
|
28658
|
-
"cdp:chatgpt": "ChatGPT (browser)"
|
|
28659
|
-
};
|
|
28660
|
-
function providerDisplayName(name) {
|
|
28661
|
-
return PROVIDER_DISPLAY_NAMES[name] ?? name.charAt(0).toUpperCase() + name.slice(1);
|
|
28662
|
-
}
|
|
28663
|
-
function clientHorizonLabel(horizon) {
|
|
28664
|
-
switch (horizon) {
|
|
28665
|
-
case "immediate":
|
|
28666
|
-
return "Do now";
|
|
28667
|
-
case "short-term":
|
|
28668
|
-
return "This month";
|
|
28669
|
-
case "medium-term":
|
|
28670
|
-
return "Next quarter";
|
|
28671
|
-
}
|
|
28672
|
-
}
|
|
28673
|
-
function clientConfidenceLabel(confidence) {
|
|
28674
|
-
switch (confidence) {
|
|
28675
|
-
case "high":
|
|
28676
|
-
return "Strong evidence";
|
|
28677
|
-
case "medium":
|
|
28678
|
-
return "Some evidence";
|
|
28679
|
-
case "low":
|
|
28680
|
-
return "Worth trying";
|
|
28681
|
-
}
|
|
28682
|
-
}
|
|
28683
|
-
function clientTrendCopy(delta) {
|
|
28684
|
-
if (!delta) return null;
|
|
28685
|
-
if (delta.direction === "up") {
|
|
28686
|
-
return { text: `Up ${delta.deltaAbs.toFixed(1)} points ${compareCopy(delta)}`, tone: "positive", arrow: "\u2191" };
|
|
28687
|
-
}
|
|
28688
|
-
if (delta.direction === "down") {
|
|
28689
|
-
return { text: `Down ${Math.abs(delta.deltaAbs).toFixed(1)} points ${compareCopy(delta)}`, tone: "negative", arrow: "\u2193" };
|
|
28690
|
-
}
|
|
28691
|
-
return { text: `Holding steady ${compareCopy(delta)}`, tone: "neutral", arrow: "\u2192" };
|
|
28692
|
-
}
|
|
28693
|
-
function compareCopy(delta) {
|
|
28694
|
-
const window = delta.window ?? 1;
|
|
28695
|
-
return window >= 2 ? `vs prior ${window} checks (avg ${delta.prior}%)` : `since last check (was ${delta.prior}%)`;
|
|
28696
|
-
}
|
|
28697
|
-
function compactInlineList(items, limit = 3) {
|
|
28698
|
-
const visible = items.slice(0, limit);
|
|
28699
|
-
const more = items.length - visible.length;
|
|
28700
|
-
return `${visible.join(", ")}${more > 0 ? `, +${more} more` : ""}`;
|
|
28780
|
+
const page3 = describeLandingPage(raw);
|
|
28781
|
+
const pathHtml = `<span class="page-path">${escapeHtml2(page3.path)}</span>`;
|
|
28782
|
+
if (!page3.querySummary) return pathHtml;
|
|
28783
|
+
return `${pathHtml}<span class="page-query" title="${escapeHtml2(page3.raw)}">${escapeHtml2(page3.querySummary)}</span>`;
|
|
28701
28784
|
}
|
|
28702
28785
|
function renderProofChips(items, limit = 3) {
|
|
28703
28786
|
if (items.length === 0) return "";
|
|
28704
28787
|
const visible = items.slice(0, limit);
|
|
28705
28788
|
const more = items.length - visible.length;
|
|
28706
28789
|
const chips = visible.map((item2) => `<span class="proof-chip">${escapeHtml2(item2)}</span>`);
|
|
28707
|
-
if (more > 0) chips.push(`<span class="proof-chip"
|
|
28790
|
+
if (more > 0) chips.push(`<span class="proof-chip">${reportMoreChipLabel(more)}</span>`);
|
|
28708
28791
|
return `<div class="proof-chips">${chips.join("")}</div>`;
|
|
28709
28792
|
}
|
|
28710
|
-
function pressureTone2(label) {
|
|
28711
|
-
if (label === "High") return "negative";
|
|
28712
|
-
if (label === "Moderate") return "caution";
|
|
28713
|
-
if (label === "Low") return "positive";
|
|
28714
|
-
return "neutral";
|
|
28715
|
-
}
|
|
28716
|
-
function severityTone(severity) {
|
|
28717
|
-
switch (severity) {
|
|
28718
|
-
case "critical":
|
|
28719
|
-
return "negative";
|
|
28720
|
-
case "high":
|
|
28721
|
-
return "negative";
|
|
28722
|
-
case "medium":
|
|
28723
|
-
return "caution";
|
|
28724
|
-
case "low":
|
|
28725
|
-
return "neutral";
|
|
28726
|
-
}
|
|
28727
|
-
}
|
|
28728
28793
|
var STYLE = `
|
|
28729
28794
|
:root {
|
|
28730
28795
|
color-scheme: dark;
|
|
@@ -28942,6 +29007,7 @@ section.report-section .section-intro {
|
|
|
28942
29007
|
.source-bar-pct { color: ${COLORS.textFaint}; font-size: 11px; }
|
|
28943
29008
|
.driver-list { margin: 0; padding-left: 16px; font-size: 12px; color: ${COLORS.textMuted}; }
|
|
28944
29009
|
.driver-list li { margin: 2px 0; }
|
|
29010
|
+
.table-scroll { overflow-x: auto; }
|
|
28945
29011
|
table.report-table {
|
|
28946
29012
|
width: 100%;
|
|
28947
29013
|
border-collapse: collapse;
|
|
@@ -28964,7 +29030,7 @@ table.report-table th {
|
|
|
28964
29030
|
}
|
|
28965
29031
|
table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
28966
29032
|
table.report-table td.page-cell { max-width: 0; }
|
|
28967
|
-
table.insights-table { table-layout: fixed; }
|
|
29033
|
+
table.insights-table { table-layout: fixed; min-width: 680px; }
|
|
28968
29034
|
table.insights-table th.col-severity, table.insights-table td.col-severity { width: 96px; }
|
|
28969
29035
|
table.insights-table th.col-query, table.insights-table td.col-query { width: 18%; }
|
|
28970
29036
|
table.insights-table th.col-provider, table.insights-table td.col-provider { width: 88px; }
|
|
@@ -28995,6 +29061,7 @@ table.report-table td .badge {
|
|
|
28995
29061
|
font-weight: 600;
|
|
28996
29062
|
border: 1px solid;
|
|
28997
29063
|
}
|
|
29064
|
+
table.report-table td p.muted { margin: 2px 0 0; font-size: 12px; color: ${COLORS.textMuted}; }
|
|
28998
29065
|
.cell-cited { color: ${COLORS.positive}; font-weight: 600; }
|
|
28999
29066
|
.cell-not-cited { color: ${COLORS.textFaint}; }
|
|
29000
29067
|
.cell-pending { color: ${COLORS.textFaint}; font-style: italic; }
|
|
@@ -29020,7 +29087,7 @@ table.report-table td .badge {
|
|
|
29020
29087
|
}
|
|
29021
29088
|
.chart-grid {
|
|
29022
29089
|
display: grid;
|
|
29023
|
-
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
29090
|
+
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
|
29024
29091
|
gap: 16px;
|
|
29025
29092
|
}
|
|
29026
29093
|
.mention-branded-block {
|
|
@@ -29374,7 +29441,7 @@ table.report-table td .badge {
|
|
|
29374
29441
|
.client-progress-fill.tone-negative { background: ${COLORS.negative}b3; }
|
|
29375
29442
|
.client-evidence-grid {
|
|
29376
29443
|
display: grid;
|
|
29377
|
-
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
29444
|
+
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
|
29378
29445
|
gap: 16px;
|
|
29379
29446
|
}
|
|
29380
29447
|
.client-opportunity-list {
|
|
@@ -29468,6 +29535,7 @@ table.report-table td .badge {
|
|
|
29468
29535
|
print-color-adjust: exact;
|
|
29469
29536
|
}
|
|
29470
29537
|
.container { max-width: none; padding: 0; }
|
|
29538
|
+
.table-scroll { overflow: visible; }
|
|
29471
29539
|
section.report-section,
|
|
29472
29540
|
.executive-hero,
|
|
29473
29541
|
.headline-card,
|
|
@@ -29499,46 +29567,34 @@ function section(opts, body) {
|
|
|
29499
29567
|
function renderEmpty(message) {
|
|
29500
29568
|
return `<div class="empty-state">${escapeHtml2(message)}</div>`;
|
|
29501
29569
|
}
|
|
29502
|
-
function locationDisplay(location) {
|
|
29503
|
-
if (!location) return "";
|
|
29504
|
-
const place = [location.city, location.region, location.country].filter(Boolean).join(", ");
|
|
29505
|
-
return place ? `${location.label} (${place})` : location.label;
|
|
29506
|
-
}
|
|
29507
29570
|
function renderHeaderLocationFragment(location) {
|
|
29508
|
-
|
|
29509
|
-
return ` \xB7 Market: ${escapeHtml2(locationDisplay(location))}`;
|
|
29571
|
+
return ` \xB7 ${escapeHtml2(reportHeaderMarketLabel(location))}`;
|
|
29510
29572
|
}
|
|
29511
29573
|
function renderLocationCard(report) {
|
|
29512
|
-
const
|
|
29513
|
-
|
|
29514
|
-
|
|
29515
|
-
const
|
|
29516
|
-
|
|
29517
|
-
|
|
29518
|
-
const notIncluded = otherLocations.length > 0 ? compactInlineList(otherLocations, 4) : "None";
|
|
29519
|
-
const interpretation = location ? otherLocations.length > 0 ? `${otherLocations.length} configured ${pluralize(otherLocations.length, "market")} still ${otherLocations.length === 1 ? "needs" : "need"} a matching check before cross-market recommendations.` : "Single-market report; findings can be read as the current market view." : "No geographic hint was attached to this check; read findings as default-market or national results.";
|
|
29520
|
-
const providerCopy = handling.length > 0 ? weakLocationProviders.length > 0 ? `${weakLocationProviders.length} ${pluralize(weakLocationProviders.length, "provider")} need a closer location check.` : `${handling.length} ${pluralize(handling.length, "provider")} received the market context.` : "No provider-level location metadata is available for this report.";
|
|
29521
|
-
const warning = weakLocationProviders.length > 0 ? `<div class="scope-warning">
|
|
29522
|
-
<strong>Location handling needs review</strong>
|
|
29523
|
-
${escapeHtml2(compactInlineList(weakLocationProviders, 4))} used weak or indirect market handling. Treat provider-level differences cautiously.
|
|
29574
|
+
const scope = reportMarketScope(report);
|
|
29575
|
+
if (!scope) return "";
|
|
29576
|
+
const copy = REPORT_SECTION_COPY["executive-summary"].marketScope;
|
|
29577
|
+
const warning = scope.weakProviders !== null ? `<div class="scope-warning">
|
|
29578
|
+
<strong>${copy.warningTitle}</strong>
|
|
29579
|
+
${escapeHtml2(scope.weakProviders)} ${copy.warningDetail}
|
|
29524
29580
|
</div>` : "";
|
|
29525
29581
|
return `<div class="chart-card market-scope-card">
|
|
29526
|
-
<h3
|
|
29582
|
+
<h3>${copy.heading}</h3>
|
|
29527
29583
|
<div class="market-scope-grid">
|
|
29528
29584
|
<div class="scope-tile">
|
|
29529
|
-
<div class="scope-label"
|
|
29530
|
-
<div class="scope-value">${escapeHtml2(
|
|
29531
|
-
<div class="scope-copy"
|
|
29585
|
+
<div class="scope-label">${copy.currentLabel}</div>
|
|
29586
|
+
<div class="scope-value">${escapeHtml2(scope.currentValue)}</div>
|
|
29587
|
+
<div class="scope-copy">${copy.currentCopy}</div>
|
|
29532
29588
|
</div>
|
|
29533
29589
|
<div class="scope-tile">
|
|
29534
|
-
<div class="scope-label"
|
|
29535
|
-
<div class="scope-value">${escapeHtml2(
|
|
29536
|
-
<div class="scope-copy">${escapeHtml2(
|
|
29590
|
+
<div class="scope-label">${copy.notIncludedLabel}</div>
|
|
29591
|
+
<div class="scope-value">${escapeHtml2(scope.notIncludedValue)}</div>
|
|
29592
|
+
<div class="scope-copy">${escapeHtml2(scope.notIncludedCopy)}</div>
|
|
29537
29593
|
</div>
|
|
29538
29594
|
<div class="scope-tile">
|
|
29539
|
-
<div class="scope-label"
|
|
29540
|
-
<div class="scope-value">${
|
|
29541
|
-
<div class="scope-copy">${escapeHtml2(providerCopy)}</div>
|
|
29595
|
+
<div class="scope-label">${copy.providerLabel}</div>
|
|
29596
|
+
<div class="scope-value">${scope.providerValue}</div>
|
|
29597
|
+
<div class="scope-copy">${escapeHtml2(scope.providerCopy)}</div>
|
|
29542
29598
|
</div>
|
|
29543
29599
|
</div>
|
|
29544
29600
|
${warning}
|
|
@@ -29546,71 +29602,65 @@ function renderLocationCard(report) {
|
|
|
29546
29602
|
}
|
|
29547
29603
|
function renderExecutiveSummary(report) {
|
|
29548
29604
|
const s = report.executiveSummary;
|
|
29549
|
-
const
|
|
29550
|
-
const
|
|
29551
|
-
const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
|
|
29552
|
-
const citedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
|
|
29553
|
-
const mentionedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
|
|
29554
|
-
const headlineTitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
|
|
29555
|
-
const headlineSubtitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
|
|
29556
|
-
const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
|
|
29557
|
-
const actionCount = dedupeReportActions(report, priorityActions).length;
|
|
29605
|
+
const copy = REPORT_SECTION_COPY["executive-summary"];
|
|
29606
|
+
const headline = reportExecutiveHeadline(report);
|
|
29558
29607
|
const heroHtml = `<div class="executive-hero">
|
|
29559
29608
|
<div class="headline-card">
|
|
29560
29609
|
<div>
|
|
29561
|
-
<div class="hero-kicker"
|
|
29562
|
-
<div class="hero-title">${escapeHtml2(
|
|
29610
|
+
<div class="hero-kicker">${copy.heroKicker}</div>
|
|
29611
|
+
<div class="hero-title">${escapeHtml2(headline.title)}</div>
|
|
29563
29612
|
</div>
|
|
29564
|
-
<div class="hero-subtitle">${escapeHtml2(
|
|
29613
|
+
<div class="hero-subtitle">${escapeHtml2(headline.subtitle)}</div>
|
|
29565
29614
|
</div>
|
|
29566
29615
|
<div class="hero-proof-grid">
|
|
29567
29616
|
<div class="hero-proof">
|
|
29568
|
-
<div class="mini-label"
|
|
29569
|
-
<div class="mini-value tone-${trendTone}">${escapeHtml2(trendLabel)}</div>
|
|
29570
|
-
<div class="mini-copy">${escapeHtml2(citedFragment)}</div>
|
|
29617
|
+
<div class="mini-label">${copy.proofTiles.citationTrend}</div>
|
|
29618
|
+
<div class="mini-value tone-${headline.trendTone}">${escapeHtml2(headline.trendLabel)}</div>
|
|
29619
|
+
<div class="mini-copy">${escapeHtml2(headline.citedFragment)}</div>
|
|
29571
29620
|
</div>
|
|
29572
29621
|
<div class="hero-proof">
|
|
29573
|
-
<div class="mini-label"
|
|
29622
|
+
<div class="mini-label">${copy.proofTiles.mentionCoverage}</div>
|
|
29574
29623
|
<div class="mini-value">${s.mentionRate}%</div>
|
|
29575
|
-
<div class="mini-copy">${escapeHtml2(mentionedFragment)}</div>
|
|
29624
|
+
<div class="mini-copy">${escapeHtml2(headline.mentionedFragment)}</div>
|
|
29576
29625
|
</div>
|
|
29577
29626
|
<div class="hero-proof">
|
|
29578
|
-
<div class="mini-label"
|
|
29579
|
-
<div class="mini-value">${formatNumber(
|
|
29580
|
-
<div class="mini-copy"
|
|
29627
|
+
<div class="mini-label">${copy.proofTiles.prioritizedActions}</div>
|
|
29628
|
+
<div class="mini-value">${formatNumber(headline.prioritizedActionCount)}</div>
|
|
29629
|
+
<div class="mini-copy">${copy.prioritizedActionsCopy}</div>
|
|
29581
29630
|
</div>
|
|
29582
29631
|
</div>
|
|
29583
29632
|
</div>`;
|
|
29584
29633
|
const metrics = [
|
|
29585
29634
|
{
|
|
29586
|
-
label:
|
|
29635
|
+
label: copy.tiles.citationRate,
|
|
29587
29636
|
value: `${s.citationRate}%`,
|
|
29588
|
-
delta: `<span class="tone-${trendTone}">${trendLabel}</span> \xB7 ${citedFragment} \xB7 ${
|
|
29637
|
+
delta: `<span class="tone-${headline.trendTone}">${headline.trendLabel}</span> \xB7 ${headline.citedFragment} \xB7 ${headline.providerCountLabel}`
|
|
29589
29638
|
},
|
|
29590
29639
|
{
|
|
29591
|
-
label:
|
|
29640
|
+
label: copy.tiles.mentionRate,
|
|
29592
29641
|
value: `${s.mentionRate}%`,
|
|
29593
|
-
delta: mentionedFragment
|
|
29642
|
+
delta: headline.mentionedFragment
|
|
29594
29643
|
},
|
|
29595
29644
|
{
|
|
29596
|
-
label:
|
|
29645
|
+
label: copy.tiles.queriesTracked,
|
|
29597
29646
|
value: formatNumber(s.queryCount),
|
|
29598
|
-
delta:
|
|
29647
|
+
delta: headline.competitorCountLabel
|
|
29599
29648
|
}
|
|
29600
29649
|
];
|
|
29601
|
-
if (s.gsc) {
|
|
29602
|
-
const dateRange = gscDateRange(report);
|
|
29650
|
+
if (s.gsc && headline.gscDelta !== null) {
|
|
29603
29651
|
metrics.push({
|
|
29604
|
-
label:
|
|
29652
|
+
label: copy.tiles.gscClicks,
|
|
29605
29653
|
value: formatNumber(s.gsc.clicks),
|
|
29606
|
-
|
|
29654
|
+
// Only the date range can hold markup; the counts and separators cannot,
|
|
29655
|
+
// so escaping the whole line matches escaping the range alone.
|
|
29656
|
+
delta: escapeHtml2(headline.gscDelta)
|
|
29607
29657
|
});
|
|
29608
29658
|
}
|
|
29609
|
-
if (s.ga) {
|
|
29659
|
+
if (s.ga && headline.gaDelta !== null) {
|
|
29610
29660
|
metrics.push({
|
|
29611
|
-
label:
|
|
29661
|
+
label: copy.tiles.gaSessions,
|
|
29612
29662
|
value: formatNumber(s.ga.sessions),
|
|
29613
|
-
delta:
|
|
29663
|
+
delta: headline.gaDelta
|
|
29614
29664
|
});
|
|
29615
29665
|
}
|
|
29616
29666
|
const metricsHtml = `<div class="metric-grid">
|
|
@@ -29628,44 +29678,38 @@ function renderExecutiveSummary(report) {
|
|
|
29628
29678
|
const locationHtml = renderLocationCard(report);
|
|
29629
29679
|
return section(
|
|
29630
29680
|
{
|
|
29631
|
-
id: "executive-summary",
|
|
29632
|
-
eyebrow:
|
|
29633
|
-
title:
|
|
29634
|
-
intro:
|
|
29681
|
+
id: ReportSectionIds["executive-summary"],
|
|
29682
|
+
eyebrow: copy.eyebrow,
|
|
29683
|
+
title: copy.title,
|
|
29684
|
+
intro: copy.intro
|
|
29635
29685
|
},
|
|
29636
29686
|
heroHtml + metricsHtml + findingsHtml + locationHtml
|
|
29637
29687
|
);
|
|
29638
29688
|
}
|
|
29639
29689
|
function deltaToneClass(direction) {
|
|
29640
|
-
|
|
29641
|
-
|
|
29642
|
-
return "";
|
|
29643
|
-
}
|
|
29644
|
-
function deltaArrow(direction) {
|
|
29645
|
-
if (direction === "up") return "\u2191";
|
|
29646
|
-
if (direction === "down") return "\u2193";
|
|
29647
|
-
return "\u2192";
|
|
29690
|
+
const tone = reportDirectionTone(direction);
|
|
29691
|
+
return tone === "neutral" ? "" : `tone-${tone}`;
|
|
29648
29692
|
}
|
|
29649
29693
|
function renderRateDeltaTile(label, delta, unit) {
|
|
29650
29694
|
if (!delta) {
|
|
29651
|
-
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta"
|
|
29695
|
+
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta">${REPORT_SECTION_COPY["whats-changed"].rateTileEmpty}</div></div>`;
|
|
29652
29696
|
}
|
|
29653
29697
|
const valueSuffix = unit === "%" ? "%" : "";
|
|
29654
|
-
const deltaText =
|
|
29698
|
+
const deltaText = reportRateDeltaCopy(delta, unit);
|
|
29655
29699
|
return `<div class="metric">
|
|
29656
29700
|
<div class="label">${escapeHtml2(label)}</div>
|
|
29657
|
-
<div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${
|
|
29701
|
+
<div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${reportDeltaArrow(delta.direction)}</span></div>
|
|
29658
29702
|
<div class="delta">${deltaText}</div>
|
|
29659
29703
|
</div>`;
|
|
29660
29704
|
}
|
|
29661
29705
|
function renderTrafficDeltaTile(label, delta, countLabel, comparisonWindowDays) {
|
|
29662
29706
|
if (!delta) {
|
|
29663
|
-
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta"
|
|
29707
|
+
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta">${REPORT_SECTION_COPY["whats-changed"].trafficTileEmpty}</div></div>`;
|
|
29664
29708
|
}
|
|
29665
|
-
const deltaText = formatWindowCountDelta(delta, countLabel,
|
|
29709
|
+
const deltaText = formatWindowCountDelta(delta, countLabel, reportPriorWindowLabel(comparisonWindowDays));
|
|
29666
29710
|
return `<div class="metric">
|
|
29667
29711
|
<div class="label">${escapeHtml2(label)}</div>
|
|
29668
|
-
<div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${
|
|
29712
|
+
<div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${reportDeltaArrow(delta.direction)}</span></div>
|
|
29669
29713
|
<div class="delta">${deltaText}</div>
|
|
29670
29714
|
</div>`;
|
|
29671
29715
|
}
|
|
@@ -29673,24 +29717,21 @@ function renderProviderMovements(movements, audience) {
|
|
|
29673
29717
|
const meaningful = movements.filter((m) => m.direction !== "flat");
|
|
29674
29718
|
if (meaningful.length === 0) return "";
|
|
29675
29719
|
const isClient = audience === "client";
|
|
29720
|
+
const copy = isClient ? REPORT_SECTION_COPY["whats-changed"].client : REPORT_SECTION_COPY["whats-changed"].agency;
|
|
29676
29721
|
const rows = meaningful.map((m) => {
|
|
29677
|
-
const sign = m.deltaAbs > 0 ? "+" : "";
|
|
29678
29722
|
return `<tr>
|
|
29679
|
-
<td>${escapeHtml2(isClient ?
|
|
29723
|
+
<td>${escapeHtml2(isClient ? reportProviderDisplayName(m.provider) : m.provider)}</td>
|
|
29680
29724
|
<td class="numeric">${m.prior}%</td>
|
|
29681
29725
|
<td class="numeric">${m.current}%</td>
|
|
29682
|
-
<td class="numeric ${deltaToneClass(m.direction)}">${
|
|
29726
|
+
<td class="numeric ${deltaToneClass(m.direction)}">${reportMovementChangeCopy(m)}</td>
|
|
29683
29727
|
</tr>`;
|
|
29684
29728
|
}).join("");
|
|
29685
|
-
const
|
|
29686
|
-
|
|
29687
|
-
|
|
29688
|
-
|
|
29689
|
-
return `<div class="chart-card"><h3>${heading}</h3>
|
|
29690
|
-
<table class="report-table">
|
|
29691
|
-
<thead><tr><th>${colA}</th><th class="numeric">${colB}</th><th class="numeric">${colC}</th><th class="numeric">Change</th></tr></thead>
|
|
29729
|
+
const [engineHeader, priorHeader, currentHeader, changeHeader] = copy.movementHeaders;
|
|
29730
|
+
return `<div class="chart-card"><h3>${copy.movementsHeading}</h3>
|
|
29731
|
+
<div class="table-scroll"><table class="report-table">
|
|
29732
|
+
<thead><tr><th>${engineHeader}</th><th class="numeric">${priorHeader}</th><th class="numeric">${currentHeader}</th><th class="numeric">${changeHeader}</th></tr></thead>
|
|
29692
29733
|
<tbody>${rows}</tbody>
|
|
29693
|
-
</table>
|
|
29734
|
+
</table></div>
|
|
29694
29735
|
</div>`;
|
|
29695
29736
|
}
|
|
29696
29737
|
function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
@@ -29701,52 +29742,45 @@ function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
|
29701
29742
|
}
|
|
29702
29743
|
const isClient = audience === "client";
|
|
29703
29744
|
const rows = insights2.map((i) => {
|
|
29704
|
-
const tone =
|
|
29705
|
-
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral"
|
|
29745
|
+
const tone = reportInsightTone(i);
|
|
29746
|
+
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral">${reportInstanceCountLabel(i.instanceCount)}</span>` : "";
|
|
29706
29747
|
const severityCell = isClient ? "" : `<td><span class="badge tone-${tone}">${escapeHtml2(reportSeverityLabel(i.severity))}</span></td>`;
|
|
29707
29748
|
return `<tr>
|
|
29708
29749
|
${severityCell}
|
|
29709
29750
|
<td>${escapeHtml2(i.title)}${countChip}</td>
|
|
29710
29751
|
<td>${escapeHtml2(i.query)}</td>
|
|
29711
|
-
<td>${escapeHtml2(isClient ?
|
|
29752
|
+
<td>${escapeHtml2(isClient ? reportProviderDisplayName(i.provider) : i.provider)}</td>
|
|
29712
29753
|
</tr>`;
|
|
29713
29754
|
}).join("");
|
|
29714
|
-
const
|
|
29755
|
+
const headerCells = (isClient ? REPORT_SECTION_COPY["whats-changed"].client : REPORT_SECTION_COPY["whats-changed"].agency).insightHeaders;
|
|
29756
|
+
const headers = `<tr>${headerCells.map((header) => `<th>${header}</th>`).join("")}</tr>`;
|
|
29715
29757
|
return `<div class="chart-card"><h3>${escapeHtml2(heading)}</h3>
|
|
29716
|
-
<table class="report-table">
|
|
29758
|
+
<div class="table-scroll"><table class="report-table">
|
|
29717
29759
|
<thead>${headers}</thead>
|
|
29718
29760
|
<tbody>${rows}</tbody>
|
|
29719
|
-
</table>
|
|
29761
|
+
</table></div>
|
|
29720
29762
|
</div>`;
|
|
29721
29763
|
}
|
|
29722
29764
|
function renderWhatsChanged(report, audience) {
|
|
29723
29765
|
const w = report.whatsChanged;
|
|
29724
29766
|
const isClient = audience === "client";
|
|
29725
|
-
const
|
|
29726
|
-
const
|
|
29727
|
-
const
|
|
29767
|
+
const { client, agency } = REPORT_SECTION_COPY["whats-changed"];
|
|
29768
|
+
const copy = isClient ? client : agency;
|
|
29769
|
+
const heading = { id: ReportSectionIds["whats-changed"], eyebrow: copy.eyebrow, title: copy.title, intro: isClient ? "" : w.headline };
|
|
29728
29770
|
if (!w.enoughHistory && !w.gscClicksDelta && !w.aiReferralsDelta && w.wins.length === 0 && w.regressions.length === 0) {
|
|
29729
|
-
return section(
|
|
29730
|
-
{ id: "whats-changed", eyebrow, title, intro },
|
|
29731
|
-
renderEmpty(isClient ? "No comparison yet \u2014 trends will appear after a few more checks." : "Trends will appear after a few more checks.")
|
|
29732
|
-
);
|
|
29771
|
+
return section(heading, renderEmpty(copy.empty));
|
|
29733
29772
|
}
|
|
29734
29773
|
const rateTiles = `<div class="metric-grid">
|
|
29735
|
-
${renderRateDeltaTile(isClient ?
|
|
29736
|
-
${renderRateDeltaTile(isClient ?
|
|
29737
|
-
${renderRateDeltaTile(isClient ?
|
|
29738
|
-
${renderTrafficDeltaTile(
|
|
29739
|
-
${renderTrafficDeltaTile(
|
|
29774
|
+
${renderRateDeltaTile(isClient ? client.tiles.mentionRate : agency.tiles.citationRate, isClient ? w.mentionRate : w.citationRate, "%")}
|
|
29775
|
+
${renderRateDeltaTile(isClient ? client.tiles.citationRate : agency.tiles.mentionRate, isClient ? w.citationRate : w.mentionRate, "%")}
|
|
29776
|
+
${renderRateDeltaTile(isClient ? client.tiles.mentionedQueryCount : agency.tiles.citedQueryCount, isClient ? w.mentionedQueryCount : w.citedQueryCount, "count")}
|
|
29777
|
+
${renderTrafficDeltaTile(copy.tiles.gscClicks, w.gscClicksDelta, copy.gscCountLabel, w.comparisonWindowDays)}
|
|
29778
|
+
${renderTrafficDeltaTile(copy.tiles.aiReferrals, w.aiReferralsDelta, copy.aiReferralsCountLabel, w.comparisonWindowDays)}
|
|
29740
29779
|
</div>`;
|
|
29741
29780
|
const movements = renderProviderMovements(w.providerMovements, audience);
|
|
29742
|
-
const
|
|
29743
|
-
const
|
|
29744
|
-
|
|
29745
|
-
const regressions = renderWinsLosses(w.regressions, lossesHeading, isClient ? "Nothing got worse this period." : "No new regressions in the latest check.", audience);
|
|
29746
|
-
return section(
|
|
29747
|
-
{ id: "whats-changed", eyebrow, title, intro },
|
|
29748
|
-
`${rateTiles}${movements}${wins}${regressions}`
|
|
29749
|
-
);
|
|
29781
|
+
const wins = renderWinsLosses(w.wins, copy.winsHeading, copy.winsEmpty, audience);
|
|
29782
|
+
const regressions = renderWinsLosses(w.regressions, copy.regressionsHeading, copy.regressionsEmpty, audience);
|
|
29783
|
+
return section(heading, `${rateTiles}${movements}${wins}${regressions}`);
|
|
29750
29784
|
}
|
|
29751
29785
|
function renderProviderBars(rates) {
|
|
29752
29786
|
if (rates.length === 0) return "";
|
|
@@ -29765,45 +29799,50 @@ function renderProviderBars(rates) {
|
|
|
29765
29799
|
<text x="${labelWidth - 8}" y="${y + 16}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml2(r.provider)}</text>
|
|
29766
29800
|
<rect x="${labelWidth}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${COLORS.border}" opacity="0.4" rx="3" />
|
|
29767
29801
|
<rect x="${labelWidth}" y="${y}" width="${w}" height="${barHeight}" fill="${color}" rx="3" />
|
|
29768
|
-
<text x="${labelWidth + w + 6}" y="${y + 16}" fill="${COLORS.text}" font-size="11">${
|
|
29802
|
+
<text x="${labelWidth + w + 6}" y="${y + 16}" fill="${COLORS.text}" font-size="11">${reportProviderRateLabel(r)}</text>`;
|
|
29769
29803
|
}).join("");
|
|
29804
|
+
const title = REPORT_SECTION_COPY["citation-scorecard"].providerChartTitle;
|
|
29770
29805
|
return `<div class="chart-card">
|
|
29771
|
-
<h3
|
|
29772
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="
|
|
29806
|
+
<h3>${title}</h3>
|
|
29807
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${reportBarChartLabel(title)}">
|
|
29773
29808
|
${bars}
|
|
29774
29809
|
</svg>
|
|
29775
29810
|
</div>`;
|
|
29776
29811
|
}
|
|
29777
29812
|
function renderCitationMatrix(scorecard) {
|
|
29813
|
+
const copy = REPORT_SECTION_COPY["citation-scorecard"];
|
|
29814
|
+
const glyphs = copy.glyphs;
|
|
29778
29815
|
if (scorecard.queries.length === 0 || scorecard.providers.length === 0) {
|
|
29779
|
-
return renderEmpty(
|
|
29816
|
+
return renderEmpty(copy.empty);
|
|
29780
29817
|
}
|
|
29781
29818
|
const headers = scorecard.providers.map((p) => `<th>${escapeHtml2(p)}</th>`).join("");
|
|
29782
29819
|
const rows = scorecard.queries.map((q, qi) => {
|
|
29783
29820
|
const cells = scorecard.providers.map((_, pi) => {
|
|
29784
29821
|
const cell = scorecard.matrix[qi]?.[pi];
|
|
29785
29822
|
if (!cell) {
|
|
29786
|
-
return
|
|
29823
|
+
return `<td><span class="cell-pending">${glyphs.missingCell}</span></td>`;
|
|
29787
29824
|
}
|
|
29788
|
-
const citedGlyph = cell.citationState === CitationStates.cited ?
|
|
29789
|
-
const mentionedGlyph = cell.answerMentioned === true ?
|
|
29825
|
+
const citedGlyph = cell.citationState === CitationStates.cited ? `<span class="cell-cited">${glyphs.cited}</span>` : `<span class="cell-not-cited">${glyphs.notCited}</span>`;
|
|
29826
|
+
const mentionedGlyph = cell.answerMentioned === true ? `<span class="cell-cited">${glyphs.mentioned}</span>` : cell.answerMentioned === false ? `<span class="cell-not-cited">${glyphs.notMentioned}</span>` : `<span class="cell-pending">${glyphs.pending}</span>`;
|
|
29790
29827
|
return `<td>${citedGlyph} ${mentionedGlyph}</td>`;
|
|
29791
29828
|
}).join("");
|
|
29792
29829
|
return `<tr><td>${escapeHtml2(q)}</td>${cells}</tr>`;
|
|
29793
29830
|
}).join("");
|
|
29794
|
-
const
|
|
29795
|
-
|
|
29796
|
-
|
|
29831
|
+
const legendCopy = copy.legend;
|
|
29832
|
+
const legend = `<p class="section-intro" style="margin-top:0;font-size:11px;">${legendCopy.lead} <span class="cell-cited">${glyphs.cited}</span>/<span class="cell-not-cited">${glyphs.notCited}</span> ${legendCopy.citedMeaning} <span class="cell-cited">${glyphs.mentioned}</span>/<span class="cell-not-cited">${glyphs.notMentioned}</span> ${legendCopy.mentionedMeaning} <span class="cell-pending">${glyphs.pending}</span> ${legendCopy.pendingMeaning}</p>`;
|
|
29833
|
+
return `${legend}<div class="table-scroll"><table class="report-table">
|
|
29834
|
+
<thead><tr><th>${copy.queryHeader}</th>${headers}</tr></thead>
|
|
29797
29835
|
<tbody>${rows}</tbody>
|
|
29798
|
-
</table>`;
|
|
29836
|
+
</table></div>`;
|
|
29799
29837
|
}
|
|
29800
29838
|
function renderCitationScorecard(report) {
|
|
29801
29839
|
const body = `
|
|
29802
29840
|
${renderProviderBars(report.citationScorecard.providerRates)}
|
|
29803
29841
|
${renderCitationMatrix(report.citationScorecard)}
|
|
29804
29842
|
`;
|
|
29843
|
+
const copy = REPORT_SECTION_COPY["citation-scorecard"];
|
|
29805
29844
|
return section(
|
|
29806
|
-
{ id: "citation-scorecard", eyebrow:
|
|
29845
|
+
{ id: ReportSectionIds["citation-scorecard"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
29807
29846
|
body
|
|
29808
29847
|
);
|
|
29809
29848
|
}
|
|
@@ -29835,19 +29874,16 @@ function renderCompetitorBars(landscape, canonical) {
|
|
|
29835
29874
|
{ label: canonical, count: landscape.projectCitationCount, isProject: true },
|
|
29836
29875
|
...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
|
|
29837
29876
|
];
|
|
29838
|
-
|
|
29877
|
+
const title = REPORT_SECTION_COPY["competitor-landscape"].citationsChartTitle;
|
|
29878
|
+
return renderLandscapeBars(data, title, reportBarChartLabel(title));
|
|
29839
29879
|
}
|
|
29840
29880
|
function renderMentionBars(section2, canonical, title) {
|
|
29841
29881
|
const data = [
|
|
29842
29882
|
{ label: canonical, count: section2.projectMentionCount, isProject: true },
|
|
29843
29883
|
...section2.competitors.map((c) => ({ label: c.domain, count: c.mentionCount, isProject: false }))
|
|
29844
29884
|
];
|
|
29845
|
-
return renderLandscapeBars(data, title,
|
|
29885
|
+
return renderLandscapeBars(data, title, reportBarChartLabel(title));
|
|
29846
29886
|
}
|
|
29847
|
-
var MENTION_SCOPE_LABEL = {
|
|
29848
|
-
"non-brand": "non-brand queries",
|
|
29849
|
-
pooled: "pooled queries \xB7 classification unavailable"
|
|
29850
|
-
};
|
|
29851
29887
|
function renderReportShareOfVoice(report) {
|
|
29852
29888
|
return [report.mentionLandscape.nonBrand?.shareOfVoice, report.mentionLandscape.branded?.shareOfVoice?.queryClass === "branded" ? report.mentionLandscape.branded.shareOfVoice : void 0].map((share) => share ? `<div class="chart-note"><p>${escapeHtml2(shareOfVoiceSummary(share.percent, share.queryClass, share))}</p>${share.reason ? `<p>${escapeHtml2(shareOfVoiceReason(share.reason))}</p>` : ""}</div>` : "").join("");
|
|
29853
29889
|
}
|
|
@@ -29858,19 +29894,20 @@ function renderCompetitorLandscape(report) {
|
|
|
29858
29894
|
const brandedMentions = mentionLandscape.branded;
|
|
29859
29895
|
const hasBrandedMentions = (brandedMentions?.totalAnswerSnapshots ?? 0) > 0;
|
|
29860
29896
|
const noMentionData = mentionLandscape.competitors.length === 0 && mentionLandscape.projectMentionCount === 0 && !hasBrandedMentions;
|
|
29897
|
+
const copy = REPORT_SECTION_COPY["competitor-landscape"];
|
|
29861
29898
|
if (noCitationData && noMentionData) {
|
|
29862
29899
|
return section(
|
|
29863
|
-
{ id: "competitor-landscape", eyebrow:
|
|
29864
|
-
renderEmpty(
|
|
29900
|
+
{ id: ReportSectionIds["competitor-landscape"], eyebrow: copy.eyebrow, title: copy.title },
|
|
29901
|
+
renderEmpty(copy.empty)
|
|
29865
29902
|
);
|
|
29866
29903
|
}
|
|
29867
29904
|
const mentionByDomain = new Map(mentionLandscape.competitors.map((m) => [m.domain, m]));
|
|
29868
29905
|
const rows = competitors2.map((c) => {
|
|
29869
|
-
const tone =
|
|
29906
|
+
const tone = reportPressureTone(c.pressureLabel);
|
|
29870
29907
|
const mention = mentionByDomain.get(c.domain);
|
|
29871
29908
|
const mentionCount = mention?.mentionCount ?? 0;
|
|
29872
29909
|
const mentionTotal = mention?.totalCount ?? mentionLandscape.totalAnswerSnapshots;
|
|
29873
|
-
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length}
|
|
29910
|
+
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${reportCitedUrlCount(c.theirCitedPages.length)}</summary>
|
|
29874
29911
|
<ul>${c.theirCitedPages.map((p) => `<li><a href="${safeHref(p.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml2(p.url)}</a> <span class="cited-for">${escapeHtml2(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
|
|
29875
29912
|
</details>` : "";
|
|
29876
29913
|
return `<tr>
|
|
@@ -29879,52 +29916,39 @@ function renderCompetitorLandscape(report) {
|
|
|
29879
29916
|
<td class="numeric">${c.citationCount} / ${c.totalCount}</td>
|
|
29880
29917
|
<td class="numeric">${mentionCount} / ${mentionTotal}</td>
|
|
29881
29918
|
<td class="numeric">${c.sharePct}%</td>
|
|
29882
|
-
<td>${escapeHtml2(c.citedQueries
|
|
29919
|
+
<td>${escapeHtml2(reportTruncatedList(c.citedQueries, 5))}${pagesDisclosure}</td>
|
|
29883
29920
|
</tr>`;
|
|
29884
29921
|
}).join("");
|
|
29885
|
-
const
|
|
29886
|
-
const
|
|
29887
|
-
const table = competitors2.length > 0 ? `<table class="report-table">
|
|
29888
|
-
<thead><tr><th
|
|
29922
|
+
const mentionCopy = reportCompetitorMentionCopy(mentionLandscape);
|
|
29923
|
+
const headers = copy.headers;
|
|
29924
|
+
const table = competitors2.length > 0 ? `<div class="table-scroll"><table class="report-table">
|
|
29925
|
+
<thead><tr><th>${headers.domain}</th><th>${headers.pressure}</th><th>${headers.citations}</th><th class="numeric" title="${escapeHtml2(mentionCopy.mentionsTooltip)}">${escapeHtml2(mentionCopy.mentionsHeader)}</th><th class="numeric" title="${copy.citationShareTooltip}">${headers.citationShare}</th><th>${headers.citedQueries}</th></tr></thead>
|
|
29889
29926
|
<tbody>${rows}</tbody>
|
|
29890
|
-
</table>` : renderEmpty(
|
|
29927
|
+
</table></div>` : renderEmpty(copy.noCompetitors);
|
|
29891
29928
|
const citationBars = renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain);
|
|
29892
29929
|
const mentionBars = renderMentionBars(
|
|
29893
29930
|
mentionLandscape,
|
|
29894
29931
|
report.meta.project.canonicalDomain,
|
|
29895
|
-
|
|
29932
|
+
mentionCopy.mentionsChartTitle
|
|
29896
29933
|
);
|
|
29897
29934
|
const charts = citationBars && mentionBars ? `<div class="chart-grid">${citationBars}${mentionBars}</div>` : `${citationBars}${mentionBars}`;
|
|
29898
29935
|
const mentionShareUnavailable = !mentionLandscape.nonBrand?.shareOfVoice && mentionLandscape.projectMentionCount === 0 && mentionLandscape.competitors.length > 0 && mentionLandscape.competitors.every((row) => row.sharePct === null);
|
|
29899
|
-
const mentionShareNote = mentionShareUnavailable ? `<p class="chart-note"
|
|
29900
|
-
const brandedBars = hasBrandedMentions && brandedMentions ? renderMentionBars(brandedMentions, report.meta.project.canonicalDomain,
|
|
29936
|
+
const mentionShareNote = mentionShareUnavailable ? `<p class="chart-note">${escapeHtml2(mentionCopy.mentionShareUnavailable)}</p>` : "";
|
|
29937
|
+
const brandedBars = hasBrandedMentions && brandedMentions ? renderMentionBars(brandedMentions, report.meta.project.canonicalDomain, copy.brandedChartTitle) : "";
|
|
29901
29938
|
const brandedBlock = brandedBars ? `<div class="mention-branded-block">
|
|
29902
|
-
<p class="chart-note"
|
|
29939
|
+
<p class="chart-note">${mentionCopy.brandedNote}</p>
|
|
29903
29940
|
${brandedBars}
|
|
29904
29941
|
</div>` : "";
|
|
29905
29942
|
return section(
|
|
29906
29943
|
{
|
|
29907
|
-
id: "competitor-landscape",
|
|
29908
|
-
eyebrow:
|
|
29909
|
-
title:
|
|
29910
|
-
intro:
|
|
29944
|
+
id: ReportSectionIds["competitor-landscape"],
|
|
29945
|
+
eyebrow: copy.eyebrow,
|
|
29946
|
+
title: copy.title,
|
|
29947
|
+
intro: copy.intro
|
|
29911
29948
|
},
|
|
29912
29949
|
`${mentionShareNote}${charts}${table}${brandedBlock}`
|
|
29913
29950
|
);
|
|
29914
29951
|
}
|
|
29915
|
-
var SOURCE_CATEGORY_TONE = {
|
|
29916
|
-
competitor: "negative",
|
|
29917
|
-
directory: "caution",
|
|
29918
|
-
forum: "caution",
|
|
29919
|
-
news: "neutral",
|
|
29920
|
-
reference: "neutral",
|
|
29921
|
-
blog: "neutral",
|
|
29922
|
-
social: "neutral",
|
|
29923
|
-
video: "neutral",
|
|
29924
|
-
ecommerce: "neutral",
|
|
29925
|
-
academic: "neutral",
|
|
29926
|
-
other: "neutral"
|
|
29927
|
-
};
|
|
29928
29952
|
function renderCategoryBars(buckets) {
|
|
29929
29953
|
if (buckets.length === 0) return "";
|
|
29930
29954
|
const total = buckets.reduce((s, b) => s + b.count, 0);
|
|
@@ -29932,7 +29956,7 @@ function renderCategoryBars(buckets) {
|
|
|
29932
29956
|
const max2 = Math.max(...buckets.map((b) => b.count), 1);
|
|
29933
29957
|
const rows = buckets.map((b) => {
|
|
29934
29958
|
const pct = b.count / max2 * 100;
|
|
29935
|
-
const tone =
|
|
29959
|
+
const tone = reportSourceCategoryTone(b.category);
|
|
29936
29960
|
const color = tone === "negative" ? COLORS.negative : tone === "caution" ? COLORS.caution : COLORS.accent;
|
|
29937
29961
|
return `
|
|
29938
29962
|
<div class="source-bar-row">
|
|
@@ -29940,11 +29964,11 @@ function renderCategoryBars(buckets) {
|
|
|
29940
29964
|
<div class="source-bar-track">
|
|
29941
29965
|
<div class="source-bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div>
|
|
29942
29966
|
</div>
|
|
29943
|
-
<div class="source-bar-value">${b.count} <span class="source-bar-pct"
|
|
29967
|
+
<div class="source-bar-value">${b.count} <span class="source-bar-pct">${reportSourceCategoryShareLabel(b.sharePct)}</span></div>
|
|
29944
29968
|
</div>`;
|
|
29945
29969
|
}).join("");
|
|
29946
29970
|
return `<div class="chart-card">
|
|
29947
|
-
<h3
|
|
29971
|
+
<h3>${REPORT_SECTION_COPY["ai-source-origin"].categoriesHeading}</h3>
|
|
29948
29972
|
<div class="source-bars">${rows}</div>
|
|
29949
29973
|
</div>`;
|
|
29950
29974
|
}
|
|
@@ -29960,7 +29984,7 @@ function renderShareBars(heading, rows, countLabel) {
|
|
|
29960
29984
|
<div class="source-bar-track">
|
|
29961
29985
|
<div class="source-bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div>
|
|
29962
29986
|
</div>
|
|
29963
|
-
<div class="source-bar-value">${formatNumber(r.count)} <span class="source-bar-pct">${escapeHtml2(countLabel
|
|
29987
|
+
<div class="source-bar-value">${formatNumber(r.count)} <span class="source-bar-pct">${escapeHtml2(reportShareBarShareLabel(countLabel, r.sharePct))}</span></div>
|
|
29964
29988
|
</div>`;
|
|
29965
29989
|
}).join("");
|
|
29966
29990
|
return `<div class="chart-card">
|
|
@@ -29970,32 +29994,34 @@ function renderShareBars(heading, rows, countLabel) {
|
|
|
29970
29994
|
}
|
|
29971
29995
|
function renderAiSourceOrigin(report) {
|
|
29972
29996
|
const origin = report.aiSourceOrigin;
|
|
29997
|
+
const copy = REPORT_SECTION_COPY["ai-source-origin"];
|
|
29973
29998
|
if (origin.categories.length === 0 && origin.topDomains.length === 0) {
|
|
29974
29999
|
return section(
|
|
29975
|
-
{ id: "ai-source-origin", eyebrow:
|
|
29976
|
-
renderEmpty(
|
|
30000
|
+
{ id: ReportSectionIds["ai-source-origin"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30001
|
+
renderEmpty(copy.empty)
|
|
29977
30002
|
);
|
|
29978
30003
|
}
|
|
29979
|
-
const
|
|
29980
|
-
const headlineFragment =
|
|
30004
|
+
const headline = reportSourceOriginHeadline(origin.categories);
|
|
30005
|
+
const headlineFragment = headline ? `<p class="source-origin-headline"><strong>${headline.share}</strong> ${headline.detail}</p>` : "";
|
|
29981
30006
|
const rows = origin.topDomains.map((d) => `
|
|
29982
30007
|
<tr>
|
|
29983
30008
|
<td>${escapeHtml2(d.domain)}</td>
|
|
29984
30009
|
<td class="numeric">${d.count}</td>
|
|
29985
|
-
<td>${d.isCompetitor ?
|
|
30010
|
+
<td>${d.isCompetitor ? `<span class="badge tone-negative">${copy.trackedCompetitorTag}</span>` : `<span class="badge tone-neutral">${copy.externalTag}</span>`}</td>
|
|
29986
30011
|
</tr>`).join("");
|
|
29987
|
-
const
|
|
29988
|
-
|
|
29989
|
-
|
|
30012
|
+
const [domainHeader, citationsHeader, tagHeader] = copy.topSourceHeaders;
|
|
30013
|
+
const table = origin.topDomains.length > 0 ? `<div class="chart-card"><h3>${copy.topSourcesHeading}</h3>
|
|
30014
|
+
<div class="table-scroll"><table class="report-table">
|
|
30015
|
+
<thead><tr><th>${domainHeader}</th><th class="numeric">${citationsHeader}</th><th>${tagHeader}</th></tr></thead>
|
|
29990
30016
|
<tbody>${rows}</tbody>
|
|
29991
|
-
</table>
|
|
30017
|
+
</table></div>
|
|
29992
30018
|
</div>` : "";
|
|
29993
30019
|
return section(
|
|
29994
30020
|
{
|
|
29995
|
-
id: "ai-source-origin",
|
|
29996
|
-
eyebrow:
|
|
29997
|
-
title:
|
|
29998
|
-
intro:
|
|
30021
|
+
id: ReportSectionIds["ai-source-origin"],
|
|
30022
|
+
eyebrow: copy.eyebrow,
|
|
30023
|
+
title: copy.title,
|
|
30024
|
+
intro: copy.intro
|
|
29999
30025
|
},
|
|
30000
30026
|
`${headlineFragment}${table}${renderCategoryBars(origin.categories)}`
|
|
30001
30027
|
);
|
|
@@ -30022,7 +30048,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
30022
30048
|
}).join("");
|
|
30023
30049
|
return `<div class="chart-card">
|
|
30024
30050
|
<h3>${escapeHtml2(title)}</h3>
|
|
30025
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml2(title)}
|
|
30051
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml2(reportLineChartLabel(title))}">
|
|
30026
30052
|
<line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
|
|
30027
30053
|
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max2)}</text>
|
|
30028
30054
|
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
|
|
@@ -30034,10 +30060,11 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
30034
30060
|
}
|
|
30035
30061
|
function renderGsc(report) {
|
|
30036
30062
|
const gsc = report.gsc;
|
|
30063
|
+
const copy = REPORT_SECTION_COPY.gsc;
|
|
30037
30064
|
if (!gsc) {
|
|
30038
30065
|
return section(
|
|
30039
|
-
{ id:
|
|
30040
|
-
renderEmpty(
|
|
30066
|
+
{ id: ReportSectionIds.gsc, eyebrow: copy.eyebrow, title: copy.title },
|
|
30067
|
+
renderEmpty(copy.empty)
|
|
30041
30068
|
);
|
|
30042
30069
|
}
|
|
30043
30070
|
const rows = gsc.topQueries.map((q) => `
|
|
@@ -30050,48 +30077,48 @@ function renderGsc(report) {
|
|
|
30050
30077
|
<td><span class="badge tone-neutral">${escapeHtml2(q.category)}</span></td>
|
|
30051
30078
|
</tr>`).join("");
|
|
30052
30079
|
const categoryBars = renderShareBars(
|
|
30053
|
-
|
|
30080
|
+
copy.intentHeading,
|
|
30054
30081
|
gsc.categoryBreakdown.map((c, index2) => ({
|
|
30055
30082
|
label: c.category,
|
|
30056
30083
|
count: c.clicks,
|
|
30057
30084
|
sharePct: c.sharePct,
|
|
30058
30085
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30059
30086
|
})),
|
|
30060
|
-
|
|
30087
|
+
copy.intentCountLabel
|
|
30061
30088
|
);
|
|
30062
30089
|
const trendChart = renderLineChart(
|
|
30063
30090
|
gsc.trend.map((t) => ({ x: t.date, y: t.clicks, label: t.date.slice(5) })),
|
|
30064
30091
|
COLORS.accent,
|
|
30065
|
-
|
|
30092
|
+
copy.trendTitle
|
|
30066
30093
|
);
|
|
30067
30094
|
const crossoverBlocks = [];
|
|
30068
30095
|
if (gsc.trackedButNoGsc.length > 0) {
|
|
30069
|
-
crossoverBlocks.push(`<div class="chart-card"><h3
|
|
30070
|
-
<p class="section-intro"
|
|
30096
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>${copy.untrackedDemand.heading}</h3>
|
|
30097
|
+
<p class="section-intro">${copy.untrackedDemand.subtitle}</p>
|
|
30071
30098
|
${renderProofChips(gsc.trackedButNoGsc, 6)}
|
|
30072
30099
|
</div>`);
|
|
30073
30100
|
}
|
|
30074
30101
|
if (gsc.gscButNotTracked.length > 0) {
|
|
30075
|
-
crossoverBlocks.push(`<div class="chart-card"><h3
|
|
30076
|
-
<p class="section-intro"
|
|
30102
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>${copy.suggestedQueries.heading}</h3>
|
|
30103
|
+
<p class="section-intro">${copy.suggestedQueries.subtitle}</p>
|
|
30077
30104
|
${renderProofChips(gsc.gscButNotTracked, 6)}
|
|
30078
30105
|
</div>`);
|
|
30079
30106
|
}
|
|
30080
|
-
const
|
|
30107
|
+
const [queryHeader, clicksHeader, impressionsHeader, ctrHeader, positionHeader, categoryHeader] = copy.topQueryHeaders;
|
|
30081
30108
|
return section(
|
|
30082
|
-
{ id:
|
|
30109
|
+
{ id: ReportSectionIds.gsc, eyebrow: copy.eyebrow, title: copy.title, intro: reportGscIntro(report) },
|
|
30083
30110
|
`<div class="metric-grid">
|
|
30084
|
-
<div class="metric"><div class="label"
|
|
30085
|
-
<div class="metric"><div class="label"
|
|
30086
|
-
<div class="metric"><div class="label"
|
|
30087
|
-
<div class="metric"><div class="label"
|
|
30111
|
+
<div class="metric"><div class="label">${copy.tiles.clicks}</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
|
|
30112
|
+
<div class="metric"><div class="label">${copy.tiles.impressions}</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
|
|
30113
|
+
<div class="metric"><div class="label">${copy.tiles.ctr}</div><div class="value">${formatRatio(gsc.ctr)}</div></div>
|
|
30114
|
+
<div class="metric"><div class="label">${copy.tiles.position}</div><div class="value">${gsc.avgPosition.toFixed(1)}</div></div>
|
|
30088
30115
|
</div>
|
|
30089
30116
|
${trendChart}
|
|
30090
|
-
<div class="chart-card"><h3
|
|
30091
|
-
<table class="report-table">
|
|
30092
|
-
<thead><tr><th
|
|
30117
|
+
<div class="chart-card"><h3>${copy.topQueriesHeading}</h3>
|
|
30118
|
+
<div class="table-scroll"><table class="report-table">
|
|
30119
|
+
<thead><tr><th>${queryHeader}</th><th class="numeric">${clicksHeader}</th><th class="numeric">${impressionsHeader}</th><th class="numeric">${ctrHeader}</th><th class="numeric">${positionHeader}</th><th>${categoryHeader}</th></tr></thead>
|
|
30093
30120
|
<tbody>${rows}</tbody>
|
|
30094
|
-
</table>
|
|
30121
|
+
</table></div>
|
|
30095
30122
|
</div>
|
|
30096
30123
|
${categoryBars}
|
|
30097
30124
|
${crossoverBlocks.join("\n")}`
|
|
@@ -30099,10 +30126,11 @@ function renderGsc(report) {
|
|
|
30099
30126
|
}
|
|
30100
30127
|
function renderGa(report) {
|
|
30101
30128
|
const ga = report.ga;
|
|
30129
|
+
const copy = REPORT_SECTION_COPY.ga;
|
|
30102
30130
|
if (!ga) {
|
|
30103
30131
|
return section(
|
|
30104
|
-
{ id:
|
|
30105
|
-
renderEmpty(
|
|
30132
|
+
{ id: ReportSectionIds.ga, eyebrow: copy.eyebrow, title: copy.title },
|
|
30133
|
+
renderEmpty(copy.empty)
|
|
30106
30134
|
);
|
|
30107
30135
|
}
|
|
30108
30136
|
const pageRows = ga.topLandingPages.map((p) => `
|
|
@@ -30112,48 +30140,50 @@ function renderGa(report) {
|
|
|
30112
30140
|
<td class="numeric">${formatNumber(p.organicSessions)}</td>
|
|
30113
30141
|
</tr>`).join("");
|
|
30114
30142
|
const channelBars = renderShareBars(
|
|
30115
|
-
|
|
30143
|
+
copy.channelsHeading,
|
|
30116
30144
|
ga.channelBreakdown.map((c, index2) => ({
|
|
30117
30145
|
label: c.channel,
|
|
30118
30146
|
count: c.sessions,
|
|
30119
30147
|
sharePct: c.sharePct,
|
|
30120
30148
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30121
30149
|
})),
|
|
30122
|
-
|
|
30150
|
+
copy.channelsCountLabel
|
|
30123
30151
|
);
|
|
30152
|
+
const [pageHeader, sessionsHeader, organicHeader] = copy.topPageHeaders;
|
|
30124
30153
|
return section(
|
|
30125
|
-
{ id:
|
|
30154
|
+
{ id: ReportSectionIds.ga, eyebrow: copy.eyebrow, title: copy.title, intro: reportGaIntro(ga) },
|
|
30126
30155
|
`<div class="metric-grid">
|
|
30127
|
-
<div class="metric"><div class="label"
|
|
30128
|
-
<div class="metric"><div class="label"
|
|
30129
|
-
<div class="metric"><div class="label"
|
|
30156
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
|
|
30157
|
+
<div class="metric"><div class="label">${copy.tiles.users}</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
|
|
30158
|
+
<div class="metric"><div class="label">${copy.tiles.organicSessions}</div><div class="value">${formatNumber(ga.totalOrganicSessions)}</div></div>
|
|
30130
30159
|
</div>
|
|
30131
|
-
<div class="chart-card"><h3
|
|
30132
|
-
<table class="report-table">
|
|
30133
|
-
<thead><tr><th
|
|
30160
|
+
<div class="chart-card"><h3>${copy.topPagesHeading}</h3>
|
|
30161
|
+
<div class="table-scroll"><table class="report-table">
|
|
30162
|
+
<thead><tr><th>${pageHeader}</th><th class="numeric">${sessionsHeader}</th><th class="numeric">${organicHeader}</th></tr></thead>
|
|
30134
30163
|
<tbody>${pageRows}</tbody>
|
|
30135
|
-
</table>
|
|
30164
|
+
</table></div>
|
|
30136
30165
|
</div>
|
|
30137
30166
|
${channelBars}`
|
|
30138
30167
|
);
|
|
30139
30168
|
}
|
|
30140
30169
|
function renderSocial(report) {
|
|
30141
30170
|
const social = report.socialReferrals;
|
|
30171
|
+
const copy = REPORT_SECTION_COPY["social-referrals"];
|
|
30142
30172
|
if (!social) {
|
|
30143
30173
|
return section(
|
|
30144
|
-
{ id: "social-referrals", eyebrow:
|
|
30145
|
-
renderEmpty(
|
|
30174
|
+
{ id: ReportSectionIds["social-referrals"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30175
|
+
renderEmpty(copy.empty)
|
|
30146
30176
|
);
|
|
30147
30177
|
}
|
|
30148
30178
|
const channelBars = renderShareBars(
|
|
30149
|
-
|
|
30179
|
+
copy.channelsHeading,
|
|
30150
30180
|
social.channels.map((c, index2) => ({
|
|
30151
30181
|
label: c.channelGroup,
|
|
30152
30182
|
count: c.sessions,
|
|
30153
30183
|
sharePct: c.sharePct,
|
|
30154
30184
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30155
30185
|
})),
|
|
30156
|
-
|
|
30186
|
+
copy.channelsCountLabel
|
|
30157
30187
|
);
|
|
30158
30188
|
const campaignRows = social.topCampaigns.map((c) => `
|
|
30159
30189
|
<tr>
|
|
@@ -30161,39 +30191,41 @@ function renderSocial(report) {
|
|
|
30161
30191
|
<td>${escapeHtml2(c.medium)}</td>
|
|
30162
30192
|
<td class="numeric">${formatNumber(c.sessions)}</td>
|
|
30163
30193
|
</tr>`).join("");
|
|
30194
|
+
const [sourceHeader, mediumHeader, sessionsHeader] = copy.campaignHeaders;
|
|
30164
30195
|
return section(
|
|
30165
|
-
{ id: "social-referrals", eyebrow:
|
|
30196
|
+
{ id: ReportSectionIds["social-referrals"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30166
30197
|
`<div class="metric-grid">
|
|
30167
|
-
<div class="metric"><div class="label"
|
|
30168
|
-
<div class="metric"><div class="label"
|
|
30169
|
-
<div class="metric"><div class="label"
|
|
30198
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
|
|
30199
|
+
<div class="metric"><div class="label">${copy.tiles.organic}</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
|
|
30200
|
+
<div class="metric"><div class="label">${copy.tiles.paid}</div><div class="value">${formatNumber(social.paidSessions)}</div></div>
|
|
30170
30201
|
</div>
|
|
30171
30202
|
${channelBars}
|
|
30172
|
-
<div class="chart-card"><h3
|
|
30173
|
-
<table class="report-table">
|
|
30174
|
-
<thead><tr><th
|
|
30203
|
+
<div class="chart-card"><h3>${copy.campaignsHeading}</h3>
|
|
30204
|
+
<div class="table-scroll"><table class="report-table">
|
|
30205
|
+
<thead><tr><th>${sourceHeader}</th><th>${mediumHeader}</th><th class="numeric">${sessionsHeader}</th></tr></thead>
|
|
30175
30206
|
<tbody>${campaignRows}</tbody>
|
|
30176
|
-
</table>
|
|
30207
|
+
</table></div>
|
|
30177
30208
|
</div>`
|
|
30178
30209
|
);
|
|
30179
30210
|
}
|
|
30180
30211
|
function renderAiReferrals(report) {
|
|
30181
30212
|
const ai = report.aiReferrals;
|
|
30213
|
+
const copy = REPORT_SECTION_COPY["ai-referrals"];
|
|
30182
30214
|
if (!ai) {
|
|
30183
30215
|
return section(
|
|
30184
|
-
{ id: "ai-referrals", eyebrow:
|
|
30185
|
-
renderEmpty(
|
|
30216
|
+
{ id: ReportSectionIds["ai-referrals"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30217
|
+
renderEmpty(copy.empty)
|
|
30186
30218
|
);
|
|
30187
30219
|
}
|
|
30188
30220
|
const sourceBars = renderShareBars(
|
|
30189
|
-
|
|
30221
|
+
copy.sourcesHeading,
|
|
30190
30222
|
ai.bySource.map((s, index2) => ({
|
|
30191
30223
|
label: s.source,
|
|
30192
30224
|
count: s.sessions,
|
|
30193
30225
|
sharePct: s.sharePct,
|
|
30194
30226
|
color: COLORS.series[(index2 + 2) % COLORS.series.length]
|
|
30195
30227
|
})),
|
|
30196
|
-
|
|
30228
|
+
copy.sourcesCountLabel
|
|
30197
30229
|
);
|
|
30198
30230
|
const pageRows = ai.topLandingPages.map((p) => `
|
|
30199
30231
|
<tr>
|
|
@@ -30203,58 +30235,50 @@ function renderAiReferrals(report) {
|
|
|
30203
30235
|
const trendChart = renderLineChart(
|
|
30204
30236
|
ai.trend.map((t) => ({ x: t.date, y: t.sessions, label: t.date.slice(5) })),
|
|
30205
30237
|
COLORS.series[2],
|
|
30206
|
-
|
|
30238
|
+
copy.trendTitle
|
|
30207
30239
|
);
|
|
30240
|
+
const [pageHeader, sessionsHeader] = copy.topPageHeaders;
|
|
30208
30241
|
return section(
|
|
30209
|
-
{ id: "ai-referrals", eyebrow:
|
|
30242
|
+
{ id: ReportSectionIds["ai-referrals"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30210
30243
|
`<div class="metric-grid">
|
|
30211
|
-
<div class="metric"><div class="label"
|
|
30244
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
|
|
30212
30245
|
</div>
|
|
30213
30246
|
${trendChart}
|
|
30214
30247
|
${sourceBars}
|
|
30215
|
-
<div class="chart-card"><h3
|
|
30216
|
-
<table class="report-table">
|
|
30217
|
-
<thead><tr><th
|
|
30248
|
+
<div class="chart-card"><h3>${copy.topPagesHeading}</h3>
|
|
30249
|
+
<div class="table-scroll"><table class="report-table">
|
|
30250
|
+
<thead><tr><th>${pageHeader}</th><th class="numeric">${sessionsHeader}</th></tr></thead>
|
|
30218
30251
|
<tbody>${pageRows}</tbody>
|
|
30219
|
-
</table>
|
|
30252
|
+
</table></div>
|
|
30220
30253
|
</div>`
|
|
30221
30254
|
);
|
|
30222
30255
|
}
|
|
30223
|
-
function serverActivityHeading(audience, hasData, windowDays2) {
|
|
30224
|
-
const isClient = audience === "client";
|
|
30225
|
-
return {
|
|
30226
|
-
id: "server-activity",
|
|
30227
|
-
eyebrow: isClient ? "AI engine attention" : "Section 10",
|
|
30228
|
-
title: "AI Visibility \u2014 Server-Side",
|
|
30229
|
-
intro: isClient ? hasData ? `What AI engines actually do in your server logs over the last ${windowDays2} days \u2014 the other half of citations.` : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
|
|
30230
|
-
};
|
|
30231
|
-
}
|
|
30232
30256
|
function renderServerActivity(report, audience) {
|
|
30233
30257
|
const sa = report.serverActivity;
|
|
30234
30258
|
const isClient = audience === "client";
|
|
30235
30259
|
const windowDays2 = report.meta.periodDays;
|
|
30236
|
-
const
|
|
30237
|
-
const priorWindowLabel =
|
|
30260
|
+
const copy = REPORT_SECTION_COPY["server-activity"];
|
|
30261
|
+
const priorWindowLabel = reportPriorWindowLabel(windowDays2);
|
|
30238
30262
|
if (!sa) {
|
|
30239
30263
|
if (isClient) return "";
|
|
30240
30264
|
return section(
|
|
30241
|
-
|
|
30242
|
-
renderEmpty(
|
|
30265
|
+
reportServerActivityHeading("agency", false, windowDays2),
|
|
30266
|
+
renderEmpty(copy.agency.emptyNotConnected)
|
|
30243
30267
|
);
|
|
30244
30268
|
}
|
|
30245
30269
|
if (!sa.hasData) {
|
|
30246
30270
|
return section(
|
|
30247
|
-
|
|
30248
|
-
renderEmpty(isClient ?
|
|
30271
|
+
reportServerActivityHeading(audience, false, windowDays2),
|
|
30272
|
+
renderEmpty(isClient ? copy.client.empty : copy.agency.empty)
|
|
30249
30273
|
);
|
|
30250
30274
|
}
|
|
30251
30275
|
const formatDelta = (d, suffix) => {
|
|
30252
|
-
const
|
|
30253
|
-
if (!
|
|
30254
|
-
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml2(
|
|
30276
|
+
const copy2 = formatDeltaCopy(d, suffix, priorWindowLabel);
|
|
30277
|
+
if (!copy2) return "";
|
|
30278
|
+
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml2(copy2)}</span>`;
|
|
30255
30279
|
};
|
|
30256
|
-
const referralRedirectNote =
|
|
30257
|
-
const referralSubtitle = [formatDelta(sa.referralArrivals,
|
|
30280
|
+
const referralRedirectNote = reportReferralRedirectNote(sa.referralRedirects);
|
|
30281
|
+
const referralSubtitle = [formatDelta(sa.referralArrivals, copy.countNouns.sessions), escapeHtml2(sa.referralArrivalsClassSummary), referralRedirectNote].filter(Boolean).join(" \xB7 ");
|
|
30258
30282
|
if (isClient) {
|
|
30259
30283
|
const crawlerRequests = {
|
|
30260
30284
|
current: sa.verifiedCrawlerHits.current + sa.unverifiedCrawlerHits.current,
|
|
@@ -30264,11 +30288,11 @@ function renderServerActivity(report, audience) {
|
|
|
30264
30288
|
sa.verifiedCrawlerHits.prior + sa.unverifiedCrawlerHits.prior
|
|
30265
30289
|
)
|
|
30266
30290
|
};
|
|
30267
|
-
const crawlerTrustSummary =
|
|
30268
|
-
const crawlerDelta = formatDelta(crawlerRequests,
|
|
30291
|
+
const crawlerTrustSummary = reportCrawlerTrustSummary(sa.verifiedCrawlerHits.current, sa.unverifiedCrawlerHits.current);
|
|
30292
|
+
const crawlerDelta = formatDelta(crawlerRequests, copy.countNouns.requests);
|
|
30269
30293
|
const crawlerSubtitle = crawlerDelta ? `${escapeHtml2(crawlerTrustSummary)} \xB7 ${crawlerDelta}` : escapeHtml2(crawlerTrustSummary);
|
|
30270
|
-
const userFetchDelta = formatDelta(sa.aiUserFetchHits,
|
|
30271
|
-
const userFetchSubtitle = userFetchDelta || escapeHtml2(
|
|
30294
|
+
const userFetchDelta = formatDelta(sa.aiUserFetchHits, copy.countNouns.requests);
|
|
30295
|
+
const userFetchSubtitle = userFetchDelta || escapeHtml2(copy.client.userFetchFallback);
|
|
30272
30296
|
const clientOperators = sa.byOperator.filter((o) => o.verifiedHits > 0 || o.unverifiedHits > 0 || o.userFetchHits > 0 || o.referralArrivals > 0).slice(0, 5);
|
|
30273
30297
|
const clientOperatorRows = clientOperators.map((o) => `
|
|
30274
30298
|
<tr>
|
|
@@ -30277,36 +30301,37 @@ function renderServerActivity(report, audience) {
|
|
|
30277
30301
|
<td class="numeric">${formatNumber(o.userFetchHits)}</td>
|
|
30278
30302
|
<td class="numeric">${formatNumber(o.referralArrivals)}</td>
|
|
30279
30303
|
</tr>`).join("");
|
|
30304
|
+
const [toolHeader, botRequestsHeader, userFetchesHeader2, referralSessionsHeader2] = reportServerActivityClientOperatorHeaders(windowDays2);
|
|
30280
30305
|
return section(
|
|
30281
|
-
|
|
30306
|
+
reportServerActivityHeading("client", true, windowDays2),
|
|
30282
30307
|
`<div class="metric-grid">
|
|
30283
30308
|
<div class="metric">
|
|
30284
|
-
<div class="label"
|
|
30309
|
+
<div class="label">${copy.client.tiles.botRequests}</div>
|
|
30285
30310
|
<div class="value">${formatNumber(crawlerRequests.current)}</div>
|
|
30286
30311
|
<div class="subtitle">${crawlerSubtitle}</div>
|
|
30287
30312
|
</div>
|
|
30288
30313
|
<div class="metric">
|
|
30289
|
-
<div class="label"
|
|
30314
|
+
<div class="label">${copy.client.tiles.userFetches}</div>
|
|
30290
30315
|
<div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
|
|
30291
30316
|
<div class="subtitle">${userFetchSubtitle}</div>
|
|
30292
30317
|
</div>
|
|
30293
30318
|
<div class="metric">
|
|
30294
|
-
<div class="label"
|
|
30319
|
+
<div class="label">${copy.client.tiles.referralSessions}</div>
|
|
30295
30320
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
30296
30321
|
<div class="subtitle">${referralSubtitle}</div>
|
|
30297
30322
|
</div>
|
|
30298
30323
|
</div>
|
|
30299
|
-
${clientOperatorRows ? `<div class="chart-card"><h3
|
|
30300
|
-
<table class="report-table">
|
|
30301
|
-
<thead><tr><th
|
|
30324
|
+
${clientOperatorRows ? `<div class="chart-card"><h3>${copy.client.operatorsHeading}</h3>
|
|
30325
|
+
<div class="table-scroll"><table class="report-table">
|
|
30326
|
+
<thead><tr><th>${toolHeader}</th><th class="numeric">${botRequestsHeader}</th><th class="numeric">${userFetchesHeader2}</th><th class="numeric">${referralSessionsHeader2}</th></tr></thead>
|
|
30302
30327
|
<tbody>${clientOperatorRows}</tbody>
|
|
30303
|
-
</table>
|
|
30304
|
-
<p class="meta"
|
|
30328
|
+
</table></div>
|
|
30329
|
+
<p class="meta">${copy.client.operatorsFootnote}</p>
|
|
30305
30330
|
</div>` : ""}`
|
|
30306
30331
|
);
|
|
30307
30332
|
}
|
|
30308
30333
|
const operatorRows = sa.byOperator.map((o) => {
|
|
30309
|
-
const deltaText = o.deltaPct
|
|
30334
|
+
const deltaText = reportServerActivityOperatorDelta(o.deltaPct);
|
|
30310
30335
|
const toneClass = o.deltaPct === null ? "" : `tone-${deltaTone(o.deltaPct)}`;
|
|
30311
30336
|
return `
|
|
30312
30337
|
<tr>
|
|
@@ -30321,7 +30346,7 @@ function renderServerActivity(report, audience) {
|
|
|
30321
30346
|
const pathRows = sa.topCrawledPaths.map((p) => `
|
|
30322
30347
|
<tr>
|
|
30323
30348
|
<td class="page-cell">${formatLandingPageHtml(p.path)}</td>
|
|
30324
|
-
<td class="numeric">${formatNumber(
|
|
30349
|
+
<td class="numeric">${formatNumber(reportServerActivityPathHits(p))}</td>
|
|
30325
30350
|
<td class="numeric meta">${formatNumber(p.verifiedHits)}</td>
|
|
30326
30351
|
<td class="numeric">${p.distinctOperators}</td>
|
|
30327
30352
|
</tr>`).join("");
|
|
@@ -30340,75 +30365,82 @@ function renderServerActivity(report, audience) {
|
|
|
30340
30365
|
const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
|
|
30341
30366
|
sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
|
|
30342
30367
|
COLORS.series[1],
|
|
30343
|
-
|
|
30368
|
+
reportServerActivityTrendTitle(windowDays2)
|
|
30344
30369
|
) : "";
|
|
30370
|
+
const agency = copy.agency;
|
|
30371
|
+
const tiles = reportServerActivityAgencyTiles(windowDays2);
|
|
30372
|
+
const [operatorHeader, verifiedHeader, unverifiedHeader, userFetchesHeader, referralSessionsHeader, deltaHeader] = reportServerActivityAgencyOperatorHeaders(windowDays2);
|
|
30373
|
+
const [crawledPathHeader, hitsHeader, verifiedHitsHeader, operatorCountHeader] = agency.crawledPathHeaders;
|
|
30374
|
+
const [productHeader, productSessionsHeader, landingPathCountHeader] = agency.referralProductHeaders;
|
|
30375
|
+
const [landingPathHeader, landingSessionsHeader, productCountHeader] = agency.referralLandingHeaders;
|
|
30345
30376
|
return section(
|
|
30346
|
-
|
|
30377
|
+
reportServerActivityHeading("agency", true, windowDays2),
|
|
30347
30378
|
`<div class="metric-grid">
|
|
30348
30379
|
<div class="metric">
|
|
30349
|
-
<div class="label"
|
|
30380
|
+
<div class="label">${tiles.verified}</div>
|
|
30350
30381
|
<div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
|
|
30351
|
-
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits,
|
|
30382
|
+
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, copy.countNouns.hits)}</div>
|
|
30352
30383
|
</div>
|
|
30353
30384
|
<div class="metric">
|
|
30354
|
-
<div class="label"
|
|
30385
|
+
<div class="label">${tiles.unverified}</div>
|
|
30355
30386
|
<div class="value">${formatNumber(sa.unverifiedCrawlerHits.current)}</div>
|
|
30356
|
-
<div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits,
|
|
30387
|
+
<div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits, copy.countNouns.hits)}</div>
|
|
30357
30388
|
</div>
|
|
30358
30389
|
<div class="metric">
|
|
30359
|
-
<div class="label"
|
|
30390
|
+
<div class="label">${tiles.userFetches}</div>
|
|
30360
30391
|
<div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
|
|
30361
|
-
<div class="subtitle">${formatDelta(sa.aiUserFetchHits,
|
|
30392
|
+
<div class="subtitle">${formatDelta(sa.aiUserFetchHits, copy.countNouns.hits)}</div>
|
|
30362
30393
|
</div>
|
|
30363
30394
|
<div class="metric">
|
|
30364
|
-
<div class="label"
|
|
30395
|
+
<div class="label">${tiles.referralSessions}</div>
|
|
30365
30396
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
30366
30397
|
<div class="subtitle">${referralSubtitle}</div>
|
|
30367
30398
|
</div>
|
|
30368
30399
|
</div>
|
|
30369
30400
|
${trendChart}
|
|
30370
|
-
${operatorRows ? `<div class="chart-card"><h3
|
|
30371
|
-
<p class="meta"
|
|
30372
|
-
<table class="report-table">
|
|
30373
|
-
<thead><tr><th
|
|
30401
|
+
${operatorRows ? `<div class="chart-card"><h3>${agency.operatorsHeading}</h3>
|
|
30402
|
+
<p class="meta">${agency.operatorsNote}</p>
|
|
30403
|
+
<div class="table-scroll"><table class="report-table">
|
|
30404
|
+
<thead><tr><th>${operatorHeader}</th><th class="numeric">${verifiedHeader}</th><th class="numeric">${unverifiedHeader}</th><th class="numeric">${userFetchesHeader}</th><th class="numeric">${referralSessionsHeader}</th><th class="numeric">${deltaHeader}</th></tr></thead>
|
|
30374
30405
|
<tbody>${operatorRows}</tbody>
|
|
30375
|
-
</table>
|
|
30406
|
+
</table></div>
|
|
30376
30407
|
</div>` : ""}
|
|
30377
|
-
${pathRows ? `<div class="chart-card"><h3
|
|
30378
|
-
<p class="meta"
|
|
30379
|
-
<table class="report-table">
|
|
30380
|
-
<thead><tr><th
|
|
30408
|
+
${pathRows ? `<div class="chart-card"><h3>${agency.crawledPathsHeading}</h3>
|
|
30409
|
+
<p class="meta">${reportServerActivityCrawledPathsNote(windowDays2)}</p>
|
|
30410
|
+
<div class="table-scroll"><table class="report-table">
|
|
30411
|
+
<thead><tr><th>${crawledPathHeader}</th><th class="numeric">${hitsHeader}</th><th class="numeric">${verifiedHitsHeader}</th><th class="numeric">${operatorCountHeader}</th></tr></thead>
|
|
30381
30412
|
<tbody>${pathRows}</tbody>
|
|
30382
|
-
</table>
|
|
30413
|
+
</table></div>
|
|
30383
30414
|
</div>` : ""}
|
|
30384
|
-
${referralProductRows ? `<div class="chart-card"><h3
|
|
30385
|
-
<p class="meta"
|
|
30386
|
-
<table class="report-table">
|
|
30387
|
-
<thead><tr><th
|
|
30415
|
+
${referralProductRows ? `<div class="chart-card"><h3>${agency.referralProductsHeading}</h3>
|
|
30416
|
+
<p class="meta">${agency.referralProductsNote}</p>
|
|
30417
|
+
<div class="table-scroll"><table class="report-table">
|
|
30418
|
+
<thead><tr><th>${productHeader}</th><th class="numeric">${productSessionsHeader}</th><th class="numeric">${landingPathCountHeader}</th></tr></thead>
|
|
30388
30419
|
<tbody>${referralProductRows}</tbody>
|
|
30389
|
-
</table>
|
|
30420
|
+
</table></div>
|
|
30390
30421
|
</div>` : ""}
|
|
30391
|
-
${referralLandingRows ? `<div class="chart-card"><h3
|
|
30392
|
-
<table class="report-table">
|
|
30393
|
-
<thead><tr><th
|
|
30422
|
+
${referralLandingRows ? `<div class="chart-card"><h3>${agency.referralLandingHeading}</h3>
|
|
30423
|
+
<div class="table-scroll"><table class="report-table">
|
|
30424
|
+
<thead><tr><th>${landingPathHeader}</th><th class="numeric">${landingSessionsHeader}</th><th class="numeric">${productCountHeader}</th></tr></thead>
|
|
30394
30425
|
<tbody>${referralLandingRows}</tbody>
|
|
30395
|
-
</table>
|
|
30426
|
+
</table></div>
|
|
30396
30427
|
</div>` : ""}`
|
|
30397
30428
|
);
|
|
30398
30429
|
}
|
|
30399
30430
|
function renderIndexingHealth(report) {
|
|
30400
30431
|
const ih = report.indexingHealth;
|
|
30432
|
+
const copy = REPORT_SECTION_COPY["indexing-health"];
|
|
30401
30433
|
if (!ih) {
|
|
30402
30434
|
return section(
|
|
30403
|
-
{ id: "indexing-health", eyebrow:
|
|
30404
|
-
renderEmpty(
|
|
30435
|
+
{ id: ReportSectionIds["indexing-health"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30436
|
+
renderEmpty(copy.empty)
|
|
30405
30437
|
);
|
|
30406
30438
|
}
|
|
30407
30439
|
const segments = [
|
|
30408
|
-
{ label:
|
|
30409
|
-
{ label:
|
|
30410
|
-
{ label:
|
|
30411
|
-
{ label:
|
|
30440
|
+
{ label: copy.segments.indexed, count: ih.indexed, color: COLORS.positive },
|
|
30441
|
+
{ label: copy.segments.notIndexed, count: ih.notIndexed, color: COLORS.caution },
|
|
30442
|
+
{ label: copy.segments.deindexed, count: ih.deindexed, color: COLORS.negative },
|
|
30443
|
+
{ label: copy.segments.unknown, count: ih.unknown, color: COLORS.neutral }
|
|
30412
30444
|
].filter((s) => s.count > 0);
|
|
30413
30445
|
const total = segments.reduce((s, x) => s + x.count, 0) || 1;
|
|
30414
30446
|
const width = 600;
|
|
@@ -30420,166 +30452,171 @@ function renderIndexingHealth(report) {
|
|
|
30420
30452
|
acc += w;
|
|
30421
30453
|
return `<rect x="${x}" y="0" width="${w}" height="${height}" fill="${s.color}" />`;
|
|
30422
30454
|
}).join("");
|
|
30423
|
-
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml2(s.label
|
|
30455
|
+
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml2(reportIndexingLegendLabel(s.label, s.count))}</span>`).join("");
|
|
30424
30456
|
return section(
|
|
30425
|
-
{ id: "indexing-health", eyebrow:
|
|
30457
|
+
{ id: ReportSectionIds["indexing-health"], eyebrow: copy.eyebrow, title: copy.title, intro: reportIndexingIntro(ih.provider) },
|
|
30426
30458
|
`<div class="metric-grid">
|
|
30427
|
-
<div class="metric"><div class="label"
|
|
30428
|
-
<div class="metric"><div class="label"
|
|
30429
|
-
<div class="metric"><div class="label"
|
|
30459
|
+
<div class="metric"><div class="label">${copy.tiles.indexed}</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
|
|
30460
|
+
<div class="metric"><div class="label">${copy.tiles.total}</div><div class="value">${formatNumber(ih.total)}</div></div>
|
|
30461
|
+
<div class="metric"><div class="label">${copy.tiles.share}</div><div class="value">${ih.indexedPct}%</div></div>
|
|
30430
30462
|
</div>
|
|
30431
30463
|
<div class="chart-card">
|
|
30432
|
-
<h3
|
|
30433
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="
|
|
30464
|
+
<h3>${copy.coverageHeading}</h3>
|
|
30465
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${copy.coverageLabel}">${bars}</svg>
|
|
30434
30466
|
<div class="legend">${legend}</div>
|
|
30435
30467
|
</div>`
|
|
30436
30468
|
);
|
|
30437
30469
|
}
|
|
30438
30470
|
function renderCitationsTrend(report) {
|
|
30439
30471
|
const trend = report.citationsTrend;
|
|
30472
|
+
const copy = REPORT_SECTION_COPY["citations-trend"];
|
|
30440
30473
|
if (trend.length === 0) {
|
|
30441
30474
|
return section(
|
|
30442
|
-
{ id: "citations-trend", eyebrow:
|
|
30443
|
-
renderEmpty(
|
|
30475
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30476
|
+
renderEmpty(copy.empty)
|
|
30444
30477
|
);
|
|
30445
30478
|
}
|
|
30446
30479
|
if (isTrendBaseline(trend)) {
|
|
30447
30480
|
return section(
|
|
30448
|
-
{ id: "citations-trend", eyebrow:
|
|
30449
|
-
renderEmpty(
|
|
30481
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30482
|
+
renderEmpty(reportCitationsTrendBaseline(trend.length))
|
|
30450
30483
|
);
|
|
30451
30484
|
}
|
|
30452
30485
|
const chart = renderLineChart(
|
|
30453
30486
|
trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
|
|
30454
30487
|
COLORS.positive,
|
|
30455
|
-
|
|
30488
|
+
copy.chartTitle,
|
|
30456
30489
|
220
|
|
30457
30490
|
);
|
|
30458
30491
|
const rows = trend.map((t) => `
|
|
30459
30492
|
<tr>
|
|
30460
30493
|
<td>${formatDate(t.date)}</td>
|
|
30461
30494
|
<td class="numeric">${t.citationRate}% <span class="cell-pending">(${t.citedQueryCount}/${t.totalQueryCount})</span></td>
|
|
30462
|
-
<td>${t.providerRates
|
|
30495
|
+
<td>${escapeHtml2(reportTrendProviderRates(t.providerRates))}</td>
|
|
30463
30496
|
</tr>`).join("");
|
|
30497
|
+
const [checkHeader, citedQueriesHeader, engineRatesHeader] = copy.breakdownHeaders;
|
|
30464
30498
|
return section(
|
|
30465
|
-
{ id: "citations-trend", eyebrow:
|
|
30499
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30466
30500
|
`${chart}
|
|
30467
|
-
<div class="chart-card"><h3
|
|
30468
|
-
<table class="report-table">
|
|
30469
|
-
<thead><tr><th
|
|
30501
|
+
<div class="chart-card"><h3>${copy.breakdownHeading}</h3>
|
|
30502
|
+
<div class="table-scroll"><table class="report-table">
|
|
30503
|
+
<thead><tr><th>${checkHeader}</th><th class="numeric">${citedQueriesHeader}</th><th>${engineRatesHeader}</th></tr></thead>
|
|
30470
30504
|
<tbody>${rows}</tbody>
|
|
30471
|
-
</table>
|
|
30505
|
+
</table></div>
|
|
30472
30506
|
</div>`
|
|
30473
30507
|
);
|
|
30474
30508
|
}
|
|
30475
30509
|
function renderInsights(report) {
|
|
30476
30510
|
const list = report.insights;
|
|
30511
|
+
const copy = REPORT_SECTION_COPY.insights;
|
|
30477
30512
|
if (list.length === 0) {
|
|
30478
30513
|
return section(
|
|
30479
|
-
{ id:
|
|
30480
|
-
renderEmpty(
|
|
30514
|
+
{ id: ReportSectionIds.insights, eyebrow: copy.eyebrow, title: copy.title },
|
|
30515
|
+
renderEmpty(copy.empty)
|
|
30481
30516
|
);
|
|
30482
30517
|
}
|
|
30483
30518
|
const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
|
|
30484
30519
|
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count: count4 }) => {
|
|
30485
|
-
const tone =
|
|
30486
|
-
const countChip = count4 > 1 ? ` <span class="badge tone-neutral"
|
|
30520
|
+
const tone = reportInsightTone(i);
|
|
30521
|
+
const countChip = count4 > 1 ? ` <span class="badge tone-neutral">${reportInstanceCountLabel(count4)}</span>` : "";
|
|
30487
30522
|
return `<tr>
|
|
30488
30523
|
<td class="col-severity"><span class="badge tone-${tone}">${escapeHtml2(reportSeverityLabel(i.severity))}</span></td>
|
|
30489
30524
|
<td class="col-title">${escapeHtml2(i.title)}${countChip}</td>
|
|
30490
30525
|
<td class="col-query">${escapeHtml2(i.query)}</td>
|
|
30491
30526
|
<td class="col-provider">${escapeHtml2(i.provider)}</td>
|
|
30492
|
-
<td class="col-recommendation">${i.recommendation ? escapeHtml2(i.recommendation) :
|
|
30527
|
+
<td class="col-recommendation">${i.recommendation ? escapeHtml2(i.recommendation) : `<span class="cell-pending">${copy.noRecommendation}</span>`}</td>
|
|
30493
30528
|
</tr>`;
|
|
30494
30529
|
}).join("");
|
|
30530
|
+
const [severityHeader, titleHeader, queryHeader, providerHeader, recommendationHeader] = copy.headers;
|
|
30495
30531
|
return section(
|
|
30496
|
-
{ id:
|
|
30497
|
-
`<table class="report-table insights-table">
|
|
30532
|
+
{ id: ReportSectionIds.insights, eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30533
|
+
`<div class="table-scroll"><table class="report-table insights-table">
|
|
30498
30534
|
<thead><tr>
|
|
30499
|
-
<th class="col-severity"
|
|
30500
|
-
<th class="col-title"
|
|
30501
|
-
<th class="col-query"
|
|
30502
|
-
<th class="col-provider"
|
|
30503
|
-
<th class="col-recommendation"
|
|
30535
|
+
<th class="col-severity">${severityHeader}</th>
|
|
30536
|
+
<th class="col-title">${titleHeader}</th>
|
|
30537
|
+
<th class="col-query">${queryHeader}</th>
|
|
30538
|
+
<th class="col-provider">${providerHeader}</th>
|
|
30539
|
+
<th class="col-recommendation">${recommendationHeader}</th>
|
|
30504
30540
|
</tr></thead>
|
|
30505
30541
|
<tbody>${rows}</tbody>
|
|
30506
|
-
</table>`
|
|
30542
|
+
</table></div>`
|
|
30507
30543
|
);
|
|
30508
30544
|
}
|
|
30509
30545
|
function renderOpportunities(report) {
|
|
30510
30546
|
const opps = dedupeReportOpportunities(report);
|
|
30511
30547
|
if (opps.length === 0) return "";
|
|
30512
30548
|
const canonical = report.meta.project.canonicalDomain;
|
|
30549
|
+
const copy = REPORT_SECTION_COPY["content-opportunities"];
|
|
30513
30550
|
const highlights = `<div class="opportunity-grid">
|
|
30514
30551
|
${opps.slice(0, 3).map((o) => `<article class="opportunity-card">
|
|
30515
|
-
<div class="opportunity-score" title="
|
|
30552
|
+
<div class="opportunity-score" title="${copy.scoreCardTooltip}">${Math.round(o.score)}<span class="opportunity-score-suffix">${copy.scoreSuffix}</span></div>
|
|
30516
30553
|
<h3>${escapeHtml2(o.query)}</h3>
|
|
30517
|
-
<p>${escapeHtml2(
|
|
30554
|
+
<p>${escapeHtml2(reportOpportunityActionLine(o))}</p>
|
|
30518
30555
|
${renderProofChips(o.drivers, 2)}
|
|
30519
30556
|
</article>`).join("")}
|
|
30520
30557
|
</div>`;
|
|
30521
30558
|
const rows = opps.slice(0, 10).map((o) => {
|
|
30522
|
-
const ourPage = o.ourBestPage ? `<a href="${safeHref(absolutizeProjectUrl(o.ourBestPage.url, canonical))}" target="_blank" rel="noopener noreferrer">${escapeHtml2(o.ourBestPage.url)}</a>` :
|
|
30523
|
-
const winning = o.winningCompetitor ? `<a href="${safeHref(o.winningCompetitor.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml2(o.winningCompetitor.domain)}</a>` :
|
|
30524
|
-
const drivers = o.drivers.length > 0 ? `<ul class="driver-list">${o.drivers.map((d) => `<li>${escapeHtml2(d)}</li>`).join("")}</ul>` :
|
|
30559
|
+
const ourPage = o.ourBestPage ? `<a href="${safeHref(absolutizeProjectUrl(o.ourBestPage.url, canonical))}" target="_blank" rel="noopener noreferrer">${escapeHtml2(o.ourBestPage.url)}</a>` : `<span class="cell-not-cited">${copy.noPage}</span>`;
|
|
30560
|
+
const winning = o.winningCompetitor ? `<a href="${safeHref(o.winningCompetitor.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml2(o.winningCompetitor.domain)}</a>` : `<span class="cell-not-cited">${copy.noWinningCompetitor}</span>`;
|
|
30561
|
+
const drivers = o.drivers.length > 0 ? `<ul class="driver-list">${o.drivers.map((d) => `<li>${escapeHtml2(d)}</li>`).join("")}</ul>` : `<span class="cell-not-cited">${copy.noDriverSignal}</span>`;
|
|
30525
30562
|
const surfaceTone = o.winnabilityClass === "ceded" ? "tone-caution" : "tone-neutral";
|
|
30526
30563
|
return `<tr>
|
|
30527
30564
|
<td>${escapeHtml2(o.query)}</td>
|
|
30528
30565
|
<td><span class="badge tone-neutral">${escapeHtml2(contentActionLabel(o.action))}</span></td>
|
|
30529
30566
|
<td><span class="badge ${surfaceTone}">${escapeHtml2(winnabilityClassLabel(o.winnabilityClass))}</span></td>
|
|
30530
|
-
<td class="numeric" title="
|
|
30567
|
+
<td class="numeric" title="${copy.scoreHeaderTooltip}">${Math.round(o.score)}</td>
|
|
30531
30568
|
<td>${drivers}</td>
|
|
30532
30569
|
<td>${ourPage}</td>
|
|
30533
30570
|
<td>${winning}</td>
|
|
30534
30571
|
<td><span class="badge tone-neutral">${escapeHtml2(actionConfidenceLabel(o.actionConfidence))}</span></td>
|
|
30535
30572
|
</tr>`;
|
|
30536
30573
|
}).join("");
|
|
30574
|
+
const [queryHeader, actionHeader, winnabilityHeader, scoreHeader, whyHeader, ourPageHeader, winningHeader, confidenceHeader] = copy.headers;
|
|
30537
30575
|
return section(
|
|
30538
30576
|
{
|
|
30539
|
-
id: "content-opportunities",
|
|
30540
|
-
eyebrow:
|
|
30541
|
-
title:
|
|
30542
|
-
intro:
|
|
30577
|
+
id: ReportSectionIds["content-opportunities"],
|
|
30578
|
+
eyebrow: copy.eyebrow,
|
|
30579
|
+
title: copy.title,
|
|
30580
|
+
intro: copy.intro
|
|
30543
30581
|
},
|
|
30544
|
-
`${highlights}<table class="report-table">
|
|
30545
|
-
<thead><tr><th
|
|
30582
|
+
`${highlights}<div class="table-scroll"><table class="report-table">
|
|
30583
|
+
<thead><tr><th>${queryHeader}</th><th>${actionHeader}</th><th>${winnabilityHeader}</th><th class="numeric" title="${copy.scoreHeaderTooltip}">${scoreHeader}</th><th>${whyHeader}</th><th>${ourPageHeader}</th><th>${winningHeader}</th><th>${confidenceHeader}</th></tr></thead>
|
|
30546
30584
|
<tbody>${rows}</tbody>
|
|
30547
|
-
</table>`
|
|
30585
|
+
</table></div>`
|
|
30548
30586
|
);
|
|
30549
30587
|
}
|
|
30550
30588
|
function renderContentGaps(report) {
|
|
30551
30589
|
const gaps = report.contentGaps;
|
|
30552
30590
|
if (gaps.length === 0) return "";
|
|
30591
|
+
const copy = REPORT_SECTION_COPY["content-gaps"];
|
|
30553
30592
|
const rows = gaps.slice(0, 10).map((g) => {
|
|
30554
|
-
const competitorList = g.competitorDomains.slice(0, 5).map(escapeHtml2).join(", ");
|
|
30555
|
-
const more = g.competitorDomains.length > 5 ? `, +${g.competitorDomains.length - 5} more` : "";
|
|
30556
30593
|
return `<tr>
|
|
30557
30594
|
<td>${escapeHtml2(g.query)}</td>
|
|
30558
30595
|
<td class="numeric">${g.competitorCount}</td>
|
|
30559
|
-
<td>${
|
|
30560
|
-
<td class="numeric">${
|
|
30596
|
+
<td>${escapeHtml2(reportCompactList(g.competitorDomains, 5))}</td>
|
|
30597
|
+
<td class="numeric">${reportMissRateLabel(g.missRate)}</td>
|
|
30561
30598
|
</tr>`;
|
|
30562
30599
|
}).join("");
|
|
30600
|
+
const [queryHeader, competitorsHeader, domainsHeader, missRateHeader] = copy.headers;
|
|
30563
30601
|
return section(
|
|
30564
30602
|
{
|
|
30565
|
-
id: "content-gaps",
|
|
30566
|
-
eyebrow:
|
|
30567
|
-
title:
|
|
30568
|
-
intro:
|
|
30603
|
+
id: ReportSectionIds["content-gaps"],
|
|
30604
|
+
eyebrow: copy.eyebrow,
|
|
30605
|
+
title: copy.title,
|
|
30606
|
+
intro: copy.intro
|
|
30569
30607
|
},
|
|
30570
|
-
`<table class="report-table">
|
|
30571
|
-
<thead><tr><th
|
|
30608
|
+
`<div class="table-scroll"><table class="report-table">
|
|
30609
|
+
<thead><tr><th>${queryHeader}</th><th class="numeric">${competitorsHeader}</th><th>${domainsHeader}</th><th class="numeric">${missRateHeader}</th></tr></thead>
|
|
30572
30610
|
<tbody>${rows}</tbody>
|
|
30573
|
-
</table>`
|
|
30611
|
+
</table></div>`
|
|
30574
30612
|
);
|
|
30575
30613
|
}
|
|
30576
30614
|
function renderRecommendedNextSteps(report) {
|
|
30577
30615
|
const steps = report.recommendedNextSteps;
|
|
30616
|
+
const copy = REPORT_SECTION_COPY["recommended-next-steps"];
|
|
30617
|
+
const heading = { id: ReportSectionIds["recommended-next-steps"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro };
|
|
30578
30618
|
if (steps.length === 0) {
|
|
30579
|
-
return section(
|
|
30580
|
-
{ id: "recommended-next-steps", eyebrow: "Section 16", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
|
|
30581
|
-
renderEmpty("No outstanding actions.")
|
|
30582
|
-
);
|
|
30619
|
+
return section(heading, renderEmpty(copy.empty));
|
|
30583
30620
|
}
|
|
30584
30621
|
const items = steps.map((s) => `
|
|
30585
30622
|
<div class="step">
|
|
@@ -30587,17 +30624,12 @@ function renderRecommendedNextSteps(report) {
|
|
|
30587
30624
|
<span class="title">${escapeHtml2(s.title)}</span>
|
|
30588
30625
|
<span class="rationale">${escapeHtml2(s.rationale)}</span>
|
|
30589
30626
|
</div>`).join("");
|
|
30590
|
-
return section(
|
|
30591
|
-
{ id: "recommended-next-steps", eyebrow: "Section 16", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
|
|
30592
|
-
`<div class="steps">${items}</div>`
|
|
30593
|
-
);
|
|
30594
|
-
}
|
|
30595
|
-
function actionAudienceMatches(action, audience) {
|
|
30596
|
-
return action.audience === "both" || action.audience === audience;
|
|
30627
|
+
return section(heading, `<div class="steps">${items}</div>`);
|
|
30597
30628
|
}
|
|
30598
30629
|
function renderActionCards(actions, audience) {
|
|
30599
30630
|
const isClient = audience === "client";
|
|
30600
|
-
|
|
30631
|
+
const copy = isClient ? REPORT_SECTION_COPY["client-action-plan"] : REPORT_SECTION_COPY["agency-action-plan"];
|
|
30632
|
+
if (actions.length === 0) return renderEmpty(copy.empty);
|
|
30601
30633
|
return `<div class="action-card-grid">
|
|
30602
30634
|
${actions.map((action, idx) => {
|
|
30603
30635
|
const tone = reportActionTone(action);
|
|
@@ -30605,17 +30637,17 @@ function renderActionCards(actions, audience) {
|
|
|
30605
30637
|
const evidence = action.evidence.length > 0 ? `<ul>${action.evidence.map((item2) => `<li>${escapeHtml2(item2)}</li>`).join("")}</ul>` : "";
|
|
30606
30638
|
const proof = renderProofChips(action.evidence.length > 0 ? action.evidence : action.why, 3);
|
|
30607
30639
|
const details = why || evidence ? `<details class="action-details">
|
|
30608
|
-
<summary>${
|
|
30609
|
-
${why ? `<div><strong>${
|
|
30610
|
-
${evidence ? `<div><strong>${
|
|
30640
|
+
<summary>${copy.detailsSummary}</summary>
|
|
30641
|
+
${why ? `<div><strong>${copy.whyLabel}</strong>${why}</div>` : ""}
|
|
30642
|
+
${evidence ? `<div><strong>${copy.evidenceLabel}</strong>${evidence}</div>` : ""}
|
|
30611
30643
|
</details>` : "";
|
|
30612
|
-
const horizonLabel =
|
|
30613
|
-
const confidenceLabel =
|
|
30644
|
+
const horizonLabel = reportActionHorizonBadge(audience, action.horizon);
|
|
30645
|
+
const confidenceLabel = reportActionConfidenceBadge(audience, action.confidence);
|
|
30614
30646
|
const categoryBadge = isClient ? "" : `<span class="badge tone-neutral">${escapeHtml2(reportActionCategoryLabel(action.category))}</span>`;
|
|
30615
|
-
const successLabel =
|
|
30647
|
+
const successLabel = copy.successLabel;
|
|
30616
30648
|
return `<article class="action-card">
|
|
30617
30649
|
<div class="action-head">
|
|
30618
|
-
<div class="action-rank" title="${
|
|
30650
|
+
<div class="action-rank" title="${copy.rankTitle}">${idx + 1}</div>
|
|
30619
30651
|
<div>
|
|
30620
30652
|
<div class="action-meta">
|
|
30621
30653
|
<span class="badge tone-${tone}">${escapeHtml2(horizonLabel)}</span>
|
|
@@ -30634,71 +30666,68 @@ function renderActionCards(actions, audience) {
|
|
|
30634
30666
|
</div>`;
|
|
30635
30667
|
}
|
|
30636
30668
|
function renderAudienceActionPlan(report, audience) {
|
|
30637
|
-
const
|
|
30638
|
-
const
|
|
30669
|
+
const id = audience === "client" ? ReportSectionIds["client-action-plan"] : ReportSectionIds["agency-action-plan"];
|
|
30670
|
+
const copy = REPORT_SECTION_COPY[id];
|
|
30639
30671
|
return section(
|
|
30640
|
-
{
|
|
30641
|
-
|
|
30642
|
-
eyebrow: audience === "client" ? "Action plan" : "Agency actions",
|
|
30643
|
-
title: audience === "client" ? "What to do next" : "Agency Action Plan",
|
|
30644
|
-
intro: audience === "client" ? "Approve these in order. They are sorted by what will move the needle fastest." : "The highest-leverage work, sorted by urgency and evidence strength."
|
|
30645
|
-
},
|
|
30646
|
-
renderActionCards(actions, audience)
|
|
30672
|
+
{ id, eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30673
|
+
renderActionCards(reportAudienceActions(report, audience), audience)
|
|
30647
30674
|
);
|
|
30648
30675
|
}
|
|
30649
30676
|
function renderClientSummary(report) {
|
|
30650
30677
|
const s = report.executiveSummary;
|
|
30651
30678
|
const sc = report.citationScorecard;
|
|
30679
|
+
const copy = REPORT_SECTION_COPY["client-summary"];
|
|
30652
30680
|
const totalQ = s.totalQueryCount ?? 0;
|
|
30653
30681
|
const heroNumber = totalQ > 0 ? `${s.mentionRate}%` : "\u2014";
|
|
30654
|
-
const heroSentence =
|
|
30655
|
-
const trend =
|
|
30682
|
+
const heroSentence = reportClientHeroSentence(totalQ, s.mentionedQueryCount);
|
|
30683
|
+
const trend = reportClientTrendCopy(report.whatsChanged.mentionRate);
|
|
30656
30684
|
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
|
|
30657
30685
|
const hero = `<div class="client-hero">
|
|
30658
|
-
<div class="client-hero-eyebrow"
|
|
30686
|
+
<div class="client-hero-eyebrow">${copy.heroEyebrow}</div>
|
|
30659
30687
|
<div class="client-hero-number">${heroNumber}</div>
|
|
30660
30688
|
<p class="client-hero-sentence">${escapeHtml2(heroSentence)}</p>
|
|
30661
30689
|
${heroTrend}
|
|
30662
30690
|
</div>`;
|
|
30663
|
-
const providerSubtitle = sc.providers
|
|
30691
|
+
const providerSubtitle = reportClientProvidersSubtitle(sc.providers, s.queryCount);
|
|
30664
30692
|
const tiles = `<div class="client-metric-grid">
|
|
30665
30693
|
<div class="client-metric-tile">
|
|
30666
|
-
<div class="label"
|
|
30694
|
+
<div class="label">${copy.tiles.mentioned}</div>
|
|
30667
30695
|
<div class="value">${s.mentionRate}%</div>
|
|
30668
|
-
<div class="subtitle">${
|
|
30696
|
+
<div class="subtitle">${reportClientMentionedSubtitle(s.mentionedQueryCount, totalQ)}</div>
|
|
30669
30697
|
</div>
|
|
30670
30698
|
<div class="client-metric-tile">
|
|
30671
|
-
<div class="label"
|
|
30699
|
+
<div class="label">${copy.tiles.cited}</div>
|
|
30672
30700
|
<div class="value">${s.citationRate}%</div>
|
|
30673
|
-
<div class="subtitle">${
|
|
30701
|
+
<div class="subtitle">${reportClientCitedSubtitle(s.citedQueryCount, totalQ)}</div>
|
|
30674
30702
|
</div>
|
|
30675
30703
|
<div class="client-metric-tile">
|
|
30676
|
-
<div class="label"
|
|
30704
|
+
<div class="label">${copy.tiles.providers}</div>
|
|
30677
30705
|
<div class="value">${formatNumber(s.providerCount)}</div>
|
|
30678
30706
|
<div class="subtitle">${escapeHtml2(providerSubtitle)}</div>
|
|
30679
30707
|
</div>
|
|
30680
30708
|
</div>`;
|
|
30709
|
+
const { lead, mention, link, closing } = copy.explainer;
|
|
30681
30710
|
const explainer = `<div class="client-explainer">
|
|
30682
|
-
<strong
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
30711
|
+
<strong>${lead}</strong>
|
|
30712
|
+
${mention.article} <span class="term">${mention.term}</span> ${mention.definition}
|
|
30713
|
+
${link.article} <span class="term">${link.term}</span> ${link.definition}
|
|
30714
|
+
${closing}
|
|
30686
30715
|
</div>`;
|
|
30687
30716
|
const questions = sc.queries.length > 0 ? `<div class="client-card">
|
|
30688
|
-
<h3
|
|
30689
|
-
<p class="card-subtitle"
|
|
30717
|
+
<h3>${copy.queriesHeading}</h3>
|
|
30718
|
+
<p class="card-subtitle">${reportClientQueriesSubtitle(sc.queries.length)}</p>
|
|
30690
30719
|
<ol class="client-questions-list">
|
|
30691
30720
|
${sc.queries.map((q, i) => `<li><span class="qnum">${String(i + 1).padStart(2, "0")}</span><span>"${escapeHtml2(q)}"</span></li>`).join("")}
|
|
30692
30721
|
</ol>
|
|
30693
30722
|
</div>` : "";
|
|
30694
30723
|
const providerBars = sc.providerRates.length > 0 ? `<div class="client-card">
|
|
30695
|
-
<h3
|
|
30696
|
-
<p class="card-subtitle"
|
|
30724
|
+
<h3>${copy.providerBarsHeading}</h3>
|
|
30725
|
+
<p class="card-subtitle">${copy.providerBarsSubtitle}</p>
|
|
30697
30726
|
<div class="client-bar-list">
|
|
30698
30727
|
${sc.providerRates.map((r) => {
|
|
30699
30728
|
const pct = Math.max(r.mentionRate, 1.5);
|
|
30700
30729
|
return `<div class="client-bar-row">
|
|
30701
|
-
<span class="bar-label">${escapeHtml2(
|
|
30730
|
+
<span class="bar-label">${escapeHtml2(reportProviderDisplayName(r.provider))}</span>
|
|
30702
30731
|
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
30703
30732
|
<span class="bar-value">${r.mentionRate}% <span class="bar-value-sub">(${r.mentionedCount}/${r.totalCount})</span></span>
|
|
30704
30733
|
</div>`;
|
|
@@ -30715,34 +30744,35 @@ function renderClientEvidenceSummary(report) {
|
|
|
30715
30744
|
const opportunities = dedupeReportOpportunities(report).slice(0, 5);
|
|
30716
30745
|
const aiMax = ai.length > 0 ? Math.max(...ai.map((d) => d.count)) : 0;
|
|
30717
30746
|
const gscMax = gsc ? Math.max(...gsc.topQueries.slice(0, 5).map((q) => q.impressions), 1) : 0;
|
|
30747
|
+
const copy = REPORT_SECTION_COPY["client-evidence-summary"];
|
|
30718
30748
|
const cards = [];
|
|
30719
30749
|
if (ai.length > 0) {
|
|
30720
30750
|
cards.push(`<div class="client-card">
|
|
30721
|
-
<h3
|
|
30722
|
-
<p class="card-subtitle"
|
|
30751
|
+
<h3>${copy.sources.heading}</h3>
|
|
30752
|
+
<p class="card-subtitle">${copy.sources.subtitle}</p>
|
|
30723
30753
|
<div class="client-bar-list">
|
|
30724
30754
|
${ai.map((d) => {
|
|
30725
30755
|
const pct = aiMax > 0 ? Math.max(d.count / aiMax * 100, 1.5) : 0;
|
|
30726
|
-
const label = escapeHtml2(d.domain) + (d.isCompetitor ?
|
|
30756
|
+
const label = escapeHtml2(d.domain) + (d.isCompetitor ? ` <span style="color:${COLORS.textFaint};font-size:11px;">${copy.sources.competitorTag}</span>` : "");
|
|
30727
30757
|
return `<div class="client-bar-row">
|
|
30728
30758
|
<span class="bar-label">${label}</span>
|
|
30729
30759
|
<div class="bar-track"><div class="bar-fill bar-fill-neutral" style="width:${pct}%"></div></div>
|
|
30730
|
-
<span class="bar-value">${
|
|
30760
|
+
<span class="bar-value">${reportClientSourceCount(d.count)}</span>
|
|
30731
30761
|
</div>`;
|
|
30732
30762
|
}).join("")}
|
|
30733
30763
|
</div>
|
|
30734
30764
|
</div>`);
|
|
30735
30765
|
}
|
|
30736
30766
|
if (indexing) {
|
|
30737
|
-
const tone = indexing.indexedPct
|
|
30767
|
+
const tone = reportClientIndexingTone(indexing.indexedPct);
|
|
30738
30768
|
const fillPct = Math.max(indexing.indexedPct, 1.5);
|
|
30739
30769
|
cards.push(`<div class="client-card">
|
|
30740
|
-
<h3
|
|
30741
|
-
<p class="card-subtitle"
|
|
30770
|
+
<h3>${copy.indexing.heading}</h3>
|
|
30771
|
+
<p class="card-subtitle">${copy.indexing.subtitle}</p>
|
|
30742
30772
|
<div class="client-progress-number tone-${tone}">${indexing.indexedPct}%</div>
|
|
30743
|
-
<div style="font-size:12px;color:${COLORS.textMuted};">${
|
|
30773
|
+
<div style="font-size:12px;color:${COLORS.textMuted};">${reportClientIndexedPages(indexing.indexed, indexing.total)}</div>
|
|
30744
30774
|
<div class="client-progress-bar"><div class="client-progress-fill tone-${tone}" style="width:${fillPct}%"></div></div>
|
|
30745
|
-
<p style="margin:0;font-size:12px;color:${COLORS.textMuted};"><strong style="color:${COLORS.text};">${formatNumber(indexing.notIndexed)}</strong> ${
|
|
30775
|
+
<p style="margin:0;font-size:12px;color:${COLORS.textMuted};"><strong style="color:${COLORS.text};">${formatNumber(indexing.notIndexed)}</strong> ${reportClientNotIndexedTail(indexing.notIndexed)}</p>
|
|
30746
30776
|
</div>`);
|
|
30747
30777
|
}
|
|
30748
30778
|
if (gsc) {
|
|
@@ -30753,53 +30783,54 @@ function renderClientEvidenceSummary(report) {
|
|
|
30753
30783
|
return `<div class="client-bar-row">
|
|
30754
30784
|
<span class="bar-label">${escapeHtml2(q.query)}</span>
|
|
30755
30785
|
<div class="bar-track"><div class="bar-fill bar-fill-sky" style="width:${pct}%"></div></div>
|
|
30756
|
-
<span class="bar-value">${
|
|
30786
|
+
<span class="bar-value">${reportClientSearchCount(q.impressions)}</span>
|
|
30757
30787
|
</div>`;
|
|
30758
30788
|
}).join("")}
|
|
30759
30789
|
</div>` : "";
|
|
30760
30790
|
cards.push(`<div class="client-card">
|
|
30761
|
-
<h3
|
|
30762
|
-
<p class="card-subtitle"
|
|
30791
|
+
<h3>${copy.search.heading}</h3>
|
|
30792
|
+
<p class="card-subtitle">${copy.search.subtitleLead} <strong style="color:${COLORS.text};">${formatNumber(gsc.totalImpressions)}</strong> ${copy.search.subtitleMiddle} <strong style="color:${COLORS.text};">${formatNumber(gsc.totalClicks)}</strong> ${reportClientClicksNoun(gsc.totalClicks)} ${copy.search.subtitleTail}</p>
|
|
30763
30793
|
${queryRows2}
|
|
30764
30794
|
</div>`);
|
|
30765
30795
|
}
|
|
30766
30796
|
if (opportunities.length > 0) {
|
|
30767
30797
|
cards.push(`<div class="client-card">
|
|
30768
|
-
<h3
|
|
30769
|
-
<p class="card-subtitle"
|
|
30798
|
+
<h3>${copy.opportunities.heading}</h3>
|
|
30799
|
+
<p class="card-subtitle">${copy.opportunities.subtitle}</p>
|
|
30770
30800
|
<ul class="client-opportunity-list">
|
|
30771
30801
|
${opportunities.map((o) => `<li>
|
|
30772
30802
|
<div class="op-query">${escapeHtml2(o.query)}</div>
|
|
30773
|
-
<div class="op-action">${escapeHtml2(contentActionLabel(o.action))}${o.winnabilityClass === "ceded" ?
|
|
30803
|
+
<div class="op-action">${escapeHtml2(contentActionLabel(o.action))}${o.winnabilityClass === "ceded" ? ` <span class="badge tone-caution">${copy.opportunities.cededTag}</span>` : ""}</div>
|
|
30774
30804
|
</li>`).join("")}
|
|
30775
30805
|
</ul>
|
|
30776
30806
|
</div>`);
|
|
30777
30807
|
}
|
|
30778
30808
|
return section(
|
|
30779
30809
|
{
|
|
30780
|
-
id: "client-evidence-summary",
|
|
30781
|
-
eyebrow:
|
|
30782
|
-
title:
|
|
30783
|
-
intro:
|
|
30810
|
+
id: ReportSectionIds["client-evidence-summary"],
|
|
30811
|
+
eyebrow: copy.eyebrow,
|
|
30812
|
+
title: copy.title,
|
|
30813
|
+
intro: copy.intro
|
|
30784
30814
|
},
|
|
30785
|
-
cards.length > 0 ? `<div class="client-evidence-grid">${cards.join("")}</div>` : renderEmpty(
|
|
30815
|
+
cards.length > 0 ? `<div class="client-evidence-grid">${cards.join("")}</div>` : renderEmpty(copy.empty)
|
|
30786
30816
|
);
|
|
30787
30817
|
}
|
|
30788
30818
|
function renderAgencyDiagnostics(report) {
|
|
30789
|
-
const
|
|
30819
|
+
const copy = REPORT_SECTION_COPY["agency-diagnostics"];
|
|
30820
|
+
const diagnostics = report.agencyDiagnostics.diagnostics.filter((d) => d.title !== copy.hiddenTitle);
|
|
30790
30821
|
const body = diagnostics.length > 0 ? `<div class="diagnostics-grid">
|
|
30791
30822
|
${diagnostics.map((d) => `<div class="diagnostic-card tone-${d.severity}">
|
|
30792
30823
|
<h3>${escapeHtml2(d.title)}</h3>
|
|
30793
30824
|
<p>${escapeHtml2(d.detail)}</p>
|
|
30794
30825
|
${renderProofChips(d.evidence, 3)}
|
|
30795
30826
|
</div>`).join("")}
|
|
30796
|
-
</div>` : renderEmpty(
|
|
30827
|
+
</div>` : renderEmpty(copy.empty);
|
|
30797
30828
|
return section(
|
|
30798
30829
|
{
|
|
30799
|
-
id: "agency-diagnostics",
|
|
30800
|
-
eyebrow:
|
|
30801
|
-
title:
|
|
30802
|
-
intro:
|
|
30830
|
+
id: ReportSectionIds["agency-diagnostics"],
|
|
30831
|
+
eyebrow: copy.eyebrow,
|
|
30832
|
+
title: copy.title,
|
|
30833
|
+
intro: copy.intro
|
|
30803
30834
|
},
|
|
30804
30835
|
body
|
|
30805
30836
|
);
|
|
@@ -30855,9 +30886,9 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30855
30886
|
<body>
|
|
30856
30887
|
<div class="container">
|
|
30857
30888
|
<header class="header">
|
|
30858
|
-
<div class="eyebrow"
|
|
30889
|
+
<div class="eyebrow">${REPORT_HEADER_COPY.eyebrow}</div>
|
|
30859
30890
|
<h1>${escapeHtml2(report.meta.project.displayName)}</h1>
|
|
30860
|
-
<div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${report.visibility?.selection.mode === "advanced" ? ` \xB7 ${escapeHtml2(reportVisibilityLocationLabel(report.visibility))}` : renderHeaderLocationFragment(report.meta.location)} \xB7
|
|
30891
|
+
<div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${report.visibility?.selection.mode === "advanced" ? ` \xB7 ${escapeHtml2(reportVisibilityLocationLabel(report.visibility))}` : renderHeaderLocationFragment(report.meta.location)} \xB7 ${reportHeaderPeriodLabel(report.meta.periodDays)} \xB7 ${REPORT_HEADER_COPY.generated} ${formatDate(report.meta.generatedAt)}</div>
|
|
30861
30892
|
</header>
|
|
30862
30893
|
${sections}
|
|
30863
30894
|
<footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml2(formatIsoDate(report.meta.generatedAt))}</footer>
|
|
@@ -30871,8 +30902,8 @@ function renderReportVisibility(visibility) {
|
|
|
30871
30902
|
const historyPopulations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.trend.some((point) => point.answerCount > 0));
|
|
30872
30903
|
const rateCell = (rate2) => `<td><strong>${escapeHtml2(reportVisibilityRate(rate2))}</strong><p class="muted">${escapeHtml2(reportVisibilityEvidence(rate2))}</p></td>`;
|
|
30873
30904
|
const headers = (labels) => `<thead><tr>${labels.map((label) => `<th>${escapeHtml2(label)}</th>`).join("")}</tr></thead>`;
|
|
30874
|
-
const summary2 = `<table>${headers([REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.queries, REPORT_VISIBILITY_COPY.answers, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited])}<tbody>${populations.map((population) => `<tr><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td><td>${population.summary.queryCount}</td><td>${population.summary.answerCount}</td>${rateCell(population.summary.mentionCoverage)}${rateCell(population.summary.citationCoverage)}</tr>`).join("")}</tbody></table>`;
|
|
30875
|
-
const trend = `<details><summary>${escapeHtml2(reportVisibilityHistoryLabel(visibility))}</summary><div class="table-scroll"><table>${headers([REPORT_VISIBILITY_COPY.date, REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited, REPORT_VISIBILITY_COPY.comparison])}<tbody>${historyPopulations.flatMap((population) => population.trend.map((point) => `<tr><td><time datetime="${escapeHtml2(point.createdAt)}">${escapeHtml2(point.createdAt.slice(0, 10))}</time></td><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td>${rateCell(point.mentionCoverage)}${rateCell(point.citationCoverage)}<td>${escapeHtml2(reportVisibilityComparison(point.continuity.state, point.continuity.comparedRunId !== null && !population.trend.some((previous) => previous.runId === point.continuity.comparedRunId)))}</td></tr>`)).join("")}</tbody></table></div></details>`;
|
|
30905
|
+
const summary2 = `<table class="report-table">${headers([REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.queries, REPORT_VISIBILITY_COPY.answers, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited])}<tbody>${populations.map((population) => `<tr><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td><td>${population.summary.queryCount}</td><td>${population.summary.answerCount}</td>${rateCell(population.summary.mentionCoverage)}${rateCell(population.summary.citationCoverage)}</tr>`).join("")}</tbody></table>`;
|
|
30906
|
+
const trend = `<details><summary>${escapeHtml2(reportVisibilityHistoryLabel(visibility))}</summary><div class="table-scroll"><table class="report-table">${headers([REPORT_VISIBILITY_COPY.date, REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited, REPORT_VISIBILITY_COPY.comparison])}<tbody>${historyPopulations.flatMap((population) => population.trend.map((point) => `<tr><td><time datetime="${escapeHtml2(point.createdAt)}">${escapeHtml2(point.createdAt.slice(0, 10))}</time></td><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td>${rateCell(point.mentionCoverage)}${rateCell(point.citationCoverage)}<td>${escapeHtml2(reportVisibilityComparison(point.continuity.state, point.continuity.comparedRunId !== null && !population.trend.some((previous) => previous.runId === point.continuity.comparedRunId)))}</td></tr>`)).join("")}</tbody></table></div></details>`;
|
|
30876
30907
|
return `<section id="client-summary" class="report-section" aria-label="${escapeHtml2(REPORT_VISIBILITY_COPY.title)}"><h2>${escapeHtml2(REPORT_VISIBILITY_COPY.title)}</h2><p>${escapeHtml2(visibility.selection.mode === "advanced" ? REPORT_VISIBILITY_COPY.description : REPORT_VISIBILITY_COPY.simpleDescription)}</p><p>${escapeHtml2(reportVisibilityMeasurementLabel(visibility))}</p><div class="table-scroll">${summary2}</div>${trend}</section>`;
|
|
30877
30908
|
}
|
|
30878
30909
|
|
|
@@ -31402,7 +31433,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31402
31433
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
31403
31434
|
and29(
|
|
31404
31435
|
eq33(trafficSources.projectId, projectId),
|
|
31405
|
-
|
|
31436
|
+
ne5(trafficSources.status, TrafficSourceStatuses.archived)
|
|
31406
31437
|
)
|
|
31407
31438
|
).all();
|
|
31408
31439
|
if (sourceRows.length === 0) return null;
|
|
@@ -31420,7 +31451,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31420
31451
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31421
31452
|
eq33(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
31422
31453
|
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31423
|
-
exclusiveEnd ?
|
|
31454
|
+
exclusiveEnd ? lt6(crawlerEventsHourly.tsHour, windowEndIso) : lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31424
31455
|
)
|
|
31425
31456
|
).get()?.total ?? 0
|
|
31426
31457
|
);
|
|
@@ -31428,9 +31459,9 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31428
31459
|
db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31429
31460
|
and29(
|
|
31430
31461
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31431
|
-
|
|
31462
|
+
ne5(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
31432
31463
|
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31433
|
-
exclusiveEnd ?
|
|
31464
|
+
exclusiveEnd ? lt6(crawlerEventsHourly.tsHour, windowEndIso) : lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31434
31465
|
)
|
|
31435
31466
|
).get()?.total ?? 0
|
|
31436
31467
|
);
|
|
@@ -31444,7 +31475,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31444
31475
|
eq33(aiReferralEventsHourly.projectId, projectId),
|
|
31445
31476
|
countableReferralCondition(),
|
|
31446
31477
|
gte8(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
31447
|
-
exclusiveEnd ?
|
|
31478
|
+
exclusiveEnd ? lt6(aiReferralEventsHourly.tsHour, windowEndIso) : lte6(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
31448
31479
|
)
|
|
31449
31480
|
).get();
|
|
31450
31481
|
return aiReferralClassCounts(Number(row?.total ?? 0), Number(row?.paid ?? 0), Number(row?.organic ?? 0));
|
|
@@ -31454,7 +31485,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31454
31485
|
and29(
|
|
31455
31486
|
eq33(aiUserFetchEventsHourly.projectId, projectId),
|
|
31456
31487
|
gte8(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
31457
|
-
exclusiveEnd ?
|
|
31488
|
+
exclusiveEnd ? lt6(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte6(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
31458
31489
|
)
|
|
31459
31490
|
).get()?.total ?? 0
|
|
31460
31491
|
);
|
|
@@ -31495,7 +31526,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31495
31526
|
and29(
|
|
31496
31527
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31497
31528
|
gte8(crawlerEventsHourly.tsHour, priorStart),
|
|
31498
|
-
|
|
31529
|
+
lt6(crawlerEventsHourly.tsHour, headlineStart)
|
|
31499
31530
|
)
|
|
31500
31531
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
31501
31532
|
const referralByOperatorRows = db.select({
|
|
@@ -31786,7 +31817,7 @@ function buildInsightList(db, projectId, locationFilter) {
|
|
|
31786
31817
|
and29(
|
|
31787
31818
|
eq33(runs.projectId, projectId),
|
|
31788
31819
|
eq33(runs.kind, RunKinds["answer-visibility"]),
|
|
31789
|
-
|
|
31820
|
+
or6(eq33(runs.status, RunStatuses.completed), eq33(runs.status, RunStatuses.partial)),
|
|
31790
31821
|
notProbeRun()
|
|
31791
31822
|
)
|
|
31792
31823
|
).orderBy(desc14(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
@@ -31932,11 +31963,6 @@ function buildProviderLocationHandling(providersInRun) {
|
|
|
31932
31963
|
};
|
|
31933
31964
|
});
|
|
31934
31965
|
}
|
|
31935
|
-
function compactList(items, limit = 3) {
|
|
31936
|
-
const visible = items.slice(0, limit);
|
|
31937
|
-
const extra = items.length - visible.length;
|
|
31938
|
-
return extra > 0 ? `${visible.join(", ")}, +${extra} more` : visible.join(", ");
|
|
31939
|
-
}
|
|
31940
31966
|
function contentActionVerb(action) {
|
|
31941
31967
|
switch (action) {
|
|
31942
31968
|
case "create":
|
|
@@ -31954,7 +31980,7 @@ function confidenceFromEvidence(count4) {
|
|
|
31954
31980
|
if (count4 >= 1) return "medium";
|
|
31955
31981
|
return "low";
|
|
31956
31982
|
}
|
|
31957
|
-
function
|
|
31983
|
+
function actionAudienceMatches(action, audience) {
|
|
31958
31984
|
return action.audience === "both" || action.audience === audience;
|
|
31959
31985
|
}
|
|
31960
31986
|
function configuredCompetitorDomainsForActionPlan(db, projectId, projectCompetitorDomains) {
|
|
@@ -32065,10 +32091,10 @@ function buildReportActionPlan(input) {
|
|
|
32065
32091
|
if (input.gsc && (input.gsc.trackedButNoGsc.length > 0 || input.gsc.gscButNotTracked.length > 0)) {
|
|
32066
32092
|
const evidence = [];
|
|
32067
32093
|
if (input.gsc.trackedButNoGsc.length > 0) {
|
|
32068
|
-
evidence.push(`Tracked with no GSC demand: ${
|
|
32094
|
+
evidence.push(`Tracked with no GSC demand: ${reportCompactList(input.gsc.trackedButNoGsc)}`);
|
|
32069
32095
|
}
|
|
32070
32096
|
if (input.gsc.gscButNotTracked.length > 0) {
|
|
32071
|
-
evidence.push(`Search demand not tracked in AEO: ${
|
|
32097
|
+
evidence.push(`Search demand not tracked in AEO: ${reportCompactList(input.gsc.gscButNotTracked)}`);
|
|
32072
32098
|
}
|
|
32073
32099
|
actions.push({
|
|
32074
32100
|
audience: "agency",
|
|
@@ -32100,8 +32126,8 @@ function buildReportActionPlan(input) {
|
|
|
32100
32126
|
"They are stronger evidence than a generic topic list because the model is already retrieving competing content."
|
|
32101
32127
|
],
|
|
32102
32128
|
evidence: [
|
|
32103
|
-
`"${topGap.query}" missed at ${
|
|
32104
|
-
`Cited competitors: ${
|
|
32129
|
+
`"${topGap.query}" missed at ${formatWholePercent(topGap.missRate)} with ${topGap.competitorCount} competitor${topGap.competitorCount === 1 ? "" : "s"} cited`,
|
|
32130
|
+
`Cited competitors: ${reportCompactList(topGap.competitorDomains)}`
|
|
32105
32131
|
],
|
|
32106
32132
|
successMetric: "The top content-gap query moves from missed to cited or mentioned after the recommended content work ships.",
|
|
32107
32133
|
confidence: topGap.competitorCount >= 2 ? "high" : "medium"
|
|
@@ -32111,10 +32137,10 @@ function buildReportActionPlan(input) {
|
|
|
32111
32137
|
const ignoredProviders = input.providerLocationHandling.filter((p) => p.treatment === "ignored" || p.treatment === "browser-geo").map((p) => p.provider);
|
|
32112
32138
|
const evidence = [
|
|
32113
32139
|
`Current report location: ${input.reportLocation.label}`,
|
|
32114
|
-
`Other configured locations: ${
|
|
32140
|
+
`Other configured locations: ${reportCompactList(input.reportLocation.otherConfiguredLabels)}`
|
|
32115
32141
|
];
|
|
32116
32142
|
if (ignoredProviders.length > 0) {
|
|
32117
|
-
evidence.push(`Providers with weak/indirect location handling: ${
|
|
32143
|
+
evidence.push(`Providers with weak/indirect location handling: ${reportCompactList(ignoredProviders)}`);
|
|
32118
32144
|
}
|
|
32119
32145
|
actions.push({
|
|
32120
32146
|
audience: "agency",
|
|
@@ -32186,7 +32212,7 @@ function buildClientSummary(reportLike) {
|
|
|
32186
32212
|
return {
|
|
32187
32213
|
headline,
|
|
32188
32214
|
overview,
|
|
32189
|
-
actionItems: reportLike.actionPlan.filter((a) =>
|
|
32215
|
+
actionItems: reportLike.actionPlan.filter((a) => actionAudienceMatches(a, "client")).slice(0, 3),
|
|
32190
32216
|
confidenceNotes
|
|
32191
32217
|
};
|
|
32192
32218
|
}
|
|
@@ -32211,8 +32237,8 @@ function buildAgencyDiagnostics(input) {
|
|
|
32211
32237
|
detail: input.gsc.trackedButNoGsc.length > 0 || input.gsc.gscButNotTracked.length > 0 ? "The tracked AEO query set and real search demand are not fully aligned." : "Tracked AEO queries and high-impression non-brand GSC queries are aligned for the current window.",
|
|
32212
32238
|
severity: input.gsc.trackedButNoGsc.length > 0 || input.gsc.gscButNotTracked.length > 0 ? "caution" : "positive",
|
|
32213
32239
|
evidence: [
|
|
32214
|
-
...input.gsc.trackedButNoGsc.length > 0 ? [`Tracked with no GSC demand: ${
|
|
32215
|
-
...input.gsc.gscButNotTracked.length > 0 ? [`GSC queries not tracked in AEO: ${
|
|
32240
|
+
...input.gsc.trackedButNoGsc.length > 0 ? [`Tracked with no GSC demand: ${reportCompactList(input.gsc.trackedButNoGsc)}`] : [],
|
|
32241
|
+
...input.gsc.gscButNotTracked.length > 0 ? [`GSC queries not tracked in AEO: ${reportCompactList(input.gsc.gscButNotTracked)}`] : []
|
|
32216
32242
|
]
|
|
32217
32243
|
});
|
|
32218
32244
|
}
|
|
@@ -32237,7 +32263,7 @@ function buildAgencyDiagnostics(input) {
|
|
|
32237
32263
|
evidence: input.contentOpportunities.slice(0, 3).map((o) => `${o.query}: ${o.action} (${Math.round(o.score)})`)
|
|
32238
32264
|
});
|
|
32239
32265
|
return {
|
|
32240
|
-
priorities: input.actionPlan.filter((a) =>
|
|
32266
|
+
priorities: input.actionPlan.filter((a) => actionAudienceMatches(a, "agency")).slice(0, 6),
|
|
32241
32267
|
diagnostics
|
|
32242
32268
|
};
|
|
32243
32269
|
}
|
|
@@ -32357,7 +32383,7 @@ function buildWhatsChanged(input) {
|
|
|
32357
32383
|
function buildProjectReport(db, projectName, periodDays) {
|
|
32358
32384
|
const project = resolveProject(db, projectName);
|
|
32359
32385
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
32360
|
-
const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" });
|
|
32386
|
+
const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" }, { includeComparison: false });
|
|
32361
32387
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
32362
32388
|
const historyWindow = { from: new Date(Date.parse(generatedAt) - periodDays * 864e5).toISOString(), to: generatedAt };
|
|
32363
32389
|
const visibility = {
|
|
@@ -32668,10 +32694,10 @@ async function reportRoutes(app) {
|
|
|
32668
32694
|
}
|
|
32669
32695
|
|
|
32670
32696
|
// ../api-routes/src/organic-evidence.ts
|
|
32671
|
-
import { and as and31, desc as desc15, eq as eq35, gte as gte10, lte as lte8, or as
|
|
32697
|
+
import { and as and31, desc as desc15, eq as eq35, gte as gte10, lte as lte8, or as or8, sql as sql13 } from "drizzle-orm";
|
|
32672
32698
|
|
|
32673
32699
|
// ../api-routes/src/ga-measurement-analysis.ts
|
|
32674
|
-
import { and as and30, eq as eq34, gte as gte9, lte as lte7, or as
|
|
32700
|
+
import { and as and30, eq as eq34, gte as gte9, lte as lte7, or as or7, sql as sql12 } from "drizzle-orm";
|
|
32675
32701
|
var UNKNOWN_LANDING_PAGE = "(not set)";
|
|
32676
32702
|
function addDays(date, offset) {
|
|
32677
32703
|
const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
@@ -32845,7 +32871,7 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32845
32871
|
if (parsedHostScope.data === "marketing") {
|
|
32846
32872
|
const lowerHost = sql12`lower(${hostColumn})`;
|
|
32847
32873
|
const normalizedHost2 = sql12`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
|
|
32848
|
-
conditions.push(
|
|
32874
|
+
conditions.push(or7(...marketingHosts.flatMap((host) => [
|
|
32849
32875
|
sql12`${normalizedHost2} = ${host}`,
|
|
32850
32876
|
sql12`${normalizedHost2} like ${`%.${host}`}`
|
|
32851
32877
|
])));
|
|
@@ -32853,14 +32879,14 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32853
32879
|
if (pathPrefix && pathPrefix !== "/") {
|
|
32854
32880
|
const rawPath = sql12`case when instr(${rawPathColumn}, '?') = 0 then ${rawPathColumn} else substr(${rawPathColumn}, 1, instr(${rawPathColumn}, '?') - 1) end`;
|
|
32855
32881
|
const landingPath = sql12`case when trim(coalesce(${normalizedPathColumn}, '')) = '' then ${rawPath} else ${normalizedPathColumn} end`;
|
|
32856
|
-
conditions.push(
|
|
32882
|
+
conditions.push(or7(sql12`${landingPath} = ${pathPrefix}`, sql12`${landingPath} like ${`${pathPrefix}/%`}`));
|
|
32857
32883
|
}
|
|
32858
32884
|
return conditions;
|
|
32859
32885
|
};
|
|
32860
32886
|
const acquisitionScope = scopedConditions(gaAcquisitionDaily.hostName, gaAcquisitionDaily.landingPageNormalized, gaAcquisitionDaily.landingPage);
|
|
32861
32887
|
const leadLandingScope = scopedConditions(gaLeadEventsDaily.hostName, gaLeadEventsDaily.landingPageNormalized, gaLeadEventsDaily.landingPage);
|
|
32862
32888
|
const acquisitionAnchor = db.select({ date: sql12`max(${gaAcquisitionDaily.date})` }).from(gaAcquisitionDaily).where(and30(eq34(gaAcquisitionDaily.projectId, project.id), ...acquisitionScope)).get()?.date ?? null;
|
|
32863
|
-
const leadAnchor = db.select({ date: sql12`max(${gaLeadEventsDaily.date})` }).from(gaLeadEventsDaily).where(and30(eq34(gaLeadEventsDaily.projectId, project.id),
|
|
32889
|
+
const leadAnchor = db.select({ date: sql12`max(${gaLeadEventsDaily.date})` }).from(gaLeadEventsDaily).where(and30(eq34(gaLeadEventsDaily.projectId, project.id), or7(
|
|
32864
32890
|
eq34(gaLeadEventsDaily.attributionScope, "channel"),
|
|
32865
32891
|
and30(eq34(gaLeadEventsDaily.attributionScope, "landing-page"), ...leadLandingScope)
|
|
32866
32892
|
))).get()?.date ?? null;
|
|
@@ -32881,7 +32907,7 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32881
32907
|
date: sql12`min(${gaDailyTotals.date})`
|
|
32882
32908
|
}).from(gaDailyTotals).where(and30(
|
|
32883
32909
|
eq34(gaDailyTotals.projectId, project.id),
|
|
32884
|
-
|
|
32910
|
+
or7(
|
|
32885
32911
|
sql12`${gaDailyTotals.engagementRate} is not null`,
|
|
32886
32912
|
sql12`${gaDailyTotals.newUsers} is not null`
|
|
32887
32913
|
)
|
|
@@ -33228,7 +33254,7 @@ function buildOrganicEvidence(db, projectName, periodDays) {
|
|
|
33228
33254
|
observedDays: sql13`count(distinct ${gaAcquisitionDaily.date})`
|
|
33229
33255
|
}).from(gaAcquisitionDaily).where(and31(
|
|
33230
33256
|
eq35(gaAcquisitionDaily.projectId, project.id),
|
|
33231
|
-
|
|
33257
|
+
or8(...normalizedMarketingHosts.map((candidate) => sql13`lower(trim(${gaAcquisitionDaily.hostName})) = ${candidate} or lower(trim(${gaAcquisitionDaily.hostName})) like ${`%.${candidate}`}`))
|
|
33232
33258
|
)).get()) : null;
|
|
33233
33259
|
const gaCoverage = nativeAcquisitionActive ? nativeCoverage : legacyGaCoverage;
|
|
33234
33260
|
const asOfDate = gscCoverage?.endDate ?? gaCoverage?.endDate ?? null;
|
|
@@ -34570,7 +34596,7 @@ async function resultsExportRoutes(app) {
|
|
|
34570
34596
|
}
|
|
34571
34597
|
|
|
34572
34598
|
// ../api-routes/src/composites.ts
|
|
34573
|
-
import { eq as eq39, and as and35, desc as desc17, sql as sql14, like as like3, or as
|
|
34599
|
+
import { eq as eq39, and as and35, desc as desc17, sql as sql14, like as like3, or as or9, inArray as inArray18 } from "drizzle-orm";
|
|
34574
34600
|
var TOP_INSIGHT_LIMIT = 5;
|
|
34575
34601
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
34576
34602
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -34767,7 +34793,7 @@ async function compositeRoutes(app) {
|
|
|
34767
34793
|
}).from(querySnapshots).innerJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(
|
|
34768
34794
|
and35(
|
|
34769
34795
|
eq39(queries.projectId, project.id),
|
|
34770
|
-
|
|
34796
|
+
or9(
|
|
34771
34797
|
sql14`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
34772
34798
|
sql14`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
34773
34799
|
sql14`${querySnapshots.citedUrls} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -34779,7 +34805,7 @@ async function compositeRoutes(app) {
|
|
|
34779
34805
|
const insightMatches = app.db.select().from(insights).where(
|
|
34780
34806
|
and35(
|
|
34781
34807
|
eq39(insights.projectId, project.id),
|
|
34782
|
-
|
|
34808
|
+
or9(
|
|
34783
34809
|
like3(insights.title, pattern),
|
|
34784
34810
|
like3(insights.query, pattern),
|
|
34785
34811
|
sql14`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -47297,7 +47323,7 @@ async function googleRoutes(app, opts) {
|
|
|
47297
47323
|
|
|
47298
47324
|
// ../api-routes/src/google-marketing.ts
|
|
47299
47325
|
import crypto33 from "crypto";
|
|
47300
|
-
import { and as and38, desc as desc20, eq as eq45, isNull as isNull6, lt as
|
|
47326
|
+
import { and as and38, desc as desc20, eq as eq45, isNull as isNull6, lt as lt7, ne as ne6, or as or10, sql as sql16 } from "drizzle-orm";
|
|
47301
47327
|
import { z as z4 } from "zod";
|
|
47302
47328
|
var snapshotPageQuerySchema = z4.object({
|
|
47303
47329
|
limit: z4.coerce.number().int().min(1).max(GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX).optional(),
|
|
@@ -48512,7 +48538,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48512
48538
|
const existing = tx.select().from(runs).where(and38(
|
|
48513
48539
|
eq45(runs.projectId, project.id),
|
|
48514
48540
|
eq45(runs.kind, RunKinds["google-ads-sync"]),
|
|
48515
|
-
|
|
48541
|
+
or10(eq45(runs.status, RunStatuses.queued), eq45(runs.status, RunStatuses.running))
|
|
48516
48542
|
)).orderBy(desc20(runs.createdAt)).get();
|
|
48517
48543
|
if (existing) {
|
|
48518
48544
|
run = existing;
|
|
@@ -48548,7 +48574,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48548
48574
|
const existing = tx.select().from(runs).where(and38(
|
|
48549
48575
|
eq45(runs.projectId, project.id),
|
|
48550
48576
|
eq45(runs.kind, RunKinds["gtm-sync"]),
|
|
48551
|
-
|
|
48577
|
+
or10(eq45(runs.status, RunStatuses.queued), eq45(runs.status, RunStatuses.running))
|
|
48552
48578
|
)).orderBy(desc20(runs.createdAt)).get();
|
|
48553
48579
|
if (existing) {
|
|
48554
48580
|
run = existing;
|
|
@@ -48578,9 +48604,9 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48578
48604
|
const cursor = decodeSnapshotCursor(query.cursor);
|
|
48579
48605
|
const conditions = [eq45(googleAdsRawSnapshots.projectId, project.id)];
|
|
48580
48606
|
if (cursor) {
|
|
48581
|
-
conditions.push(
|
|
48582
|
-
|
|
48583
|
-
and38(eq45(googleAdsRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48607
|
+
conditions.push(or10(
|
|
48608
|
+
lt7(googleAdsRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48609
|
+
and38(eq45(googleAdsRawSnapshots.capturedAt, cursor.capturedAt), lt7(googleAdsRawSnapshots.id, cursor.id))
|
|
48584
48610
|
));
|
|
48585
48611
|
}
|
|
48586
48612
|
const limit = query.limit ?? 50;
|
|
@@ -48610,9 +48636,9 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48610
48636
|
const cursor = decodeSnapshotCursor(query.cursor);
|
|
48611
48637
|
const conditions = [eq45(gtmRawSnapshots.projectId, project.id)];
|
|
48612
48638
|
if (cursor) {
|
|
48613
|
-
conditions.push(
|
|
48614
|
-
|
|
48615
|
-
and38(eq45(gtmRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48639
|
+
conditions.push(or10(
|
|
48640
|
+
lt7(gtmRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48641
|
+
and38(eq45(gtmRawSnapshots.capturedAt, cursor.capturedAt), lt7(gtmRawSnapshots.id, cursor.id))
|
|
48616
48642
|
));
|
|
48617
48643
|
}
|
|
48618
48644
|
const limit = query.limit ?? 50;
|
|
@@ -48798,7 +48824,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48798
48824
|
const duplicate = app.db.select({ id: conversionTrackingContracts.id }).from(conversionTrackingContracts).where(and38(
|
|
48799
48825
|
eq45(conversionTrackingContracts.projectId, project.id),
|
|
48800
48826
|
eq45(conversionTrackingContracts.name, body.name),
|
|
48801
|
-
|
|
48827
|
+
ne6(conversionTrackingContracts.id, existing.id)
|
|
48802
48828
|
)).get();
|
|
48803
48829
|
if (duplicate) throw alreadyExists("Conversion tracking contract", body.name);
|
|
48804
48830
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -48887,7 +48913,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48887
48913
|
|
|
48888
48914
|
// ../api-routes/src/ads.ts
|
|
48889
48915
|
import crypto36 from "crypto";
|
|
48890
|
-
import { eq as eq47, and as and40, asc as asc9, desc as desc21, gt as gt3, gte as gte12, lt as
|
|
48916
|
+
import { eq as eq47, and as and40, asc as asc9, desc as desc21, gt as gt3, gte as gte12, lt as lt8, lte as lte11, ne as ne7, inArray as inArray20, or as or12, isNull as isNull8, isNotNull as isNotNull4, sql as sql18 } from "drizzle-orm";
|
|
48891
48917
|
|
|
48892
48918
|
// ../api-routes/src/ads-activation.ts
|
|
48893
48919
|
import crypto34 from "crypto";
|
|
@@ -50273,7 +50299,7 @@ async function executeApprovedAdsActivation(dependencies, request) {
|
|
|
50273
50299
|
|
|
50274
50300
|
// ../api-routes/src/ads-activation-routes.ts
|
|
50275
50301
|
import crypto35 from "crypto";
|
|
50276
|
-
import { and as and39, asc as asc8, eq as eq46, gt as gt2, isNotNull as isNotNull3, isNull as isNull7, lte as lte10, or as
|
|
50302
|
+
import { and as and39, asc as asc8, eq as eq46, gt as gt2, isNotNull as isNotNull3, isNull as isNull7, lte as lte10, or as or11, sql as sql17 } from "drizzle-orm";
|
|
50277
50303
|
var MAX_ACTIVATION_GRANT_LIFETIME_MS = 24 * 60 * 60 * 1e3;
|
|
50278
50304
|
var ACTIVATION_LEASE_MS = 5 * 6e4;
|
|
50279
50305
|
var ACTIVATION_REVOCATION_SETTLEMENT_MS = ACTIVATION_LEASE_MS;
|
|
@@ -50399,7 +50425,7 @@ function createActivationStore(app, request) {
|
|
|
50399
50425
|
}).where(and39(
|
|
50400
50426
|
eq46(adsOperations.id, existing.id),
|
|
50401
50427
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
50402
|
-
|
|
50428
|
+
or11(
|
|
50403
50429
|
isNull7(adsOperations.leaseOwner),
|
|
50404
50430
|
eq46(adsOperations.leaseOwner, input.leaseOwner),
|
|
50405
50431
|
isNull7(adsOperations.leaseExpiresAt),
|
|
@@ -50817,29 +50843,29 @@ async function executeActivationGrant(app, opts, request, project, executorApiKe
|
|
|
50817
50843
|
return activationResponse(opts, result.grant, result.operation);
|
|
50818
50844
|
}
|
|
50819
50845
|
function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLease) {
|
|
50820
|
-
const leaseAvailable =
|
|
50846
|
+
const leaseAvailable = or11(
|
|
50821
50847
|
isNull7(adsOperations.leaseOwner),
|
|
50822
50848
|
isNull7(adsOperations.leaseExpiresAt),
|
|
50823
50849
|
lte10(adsOperations.leaseExpiresAt, nowIso)
|
|
50824
50850
|
);
|
|
50825
|
-
const campaignHasMutationEvidence =
|
|
50851
|
+
const campaignHasMutationEvidence = or11(
|
|
50826
50852
|
eq46(adsOperationSteps.state, AdsOperationStepStates.executing),
|
|
50827
50853
|
eq46(adsOperationSteps.state, AdsOperationStepStates.active),
|
|
50828
50854
|
eq46(adsOperationSteps.state, AdsOperationStepStates.rollback_executing),
|
|
50829
50855
|
eq46(adsOperationSteps.state, AdsOperationStepStates.rollback_failed),
|
|
50830
50856
|
eq46(adsOperationSteps.state, AdsOperationStepStates.unknown)
|
|
50831
50857
|
);
|
|
50832
|
-
const eligibleState = allowLiveRevocationLease ?
|
|
50858
|
+
const eligibleState = allowLiveRevocationLease ? or11(
|
|
50833
50859
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50834
50860
|
and39(
|
|
50835
50861
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
50836
|
-
|
|
50862
|
+
or11(
|
|
50837
50863
|
isNotNull3(adsActivationGrants.revocationRequestedAt),
|
|
50838
50864
|
and39(leaseAvailable, campaignHasMutationEvidence)
|
|
50839
50865
|
)
|
|
50840
50866
|
)
|
|
50841
50867
|
) : and39(
|
|
50842
|
-
|
|
50868
|
+
or11(
|
|
50843
50869
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50844
50870
|
eq46(adsOperations.state, AdsOperationStates.pending)
|
|
50845
50871
|
),
|
|
@@ -50983,7 +51009,7 @@ async function containUrgentAdsActivationCampaignsOnce(app, opts) {
|
|
|
50983
51009
|
lastReconciledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
50984
51010
|
}).where(and39(
|
|
50985
51011
|
eq46(adsOperations.id, operation.id),
|
|
50986
|
-
|
|
51012
|
+
or11(
|
|
50987
51013
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50988
51014
|
eq46(adsOperations.state, AdsOperationStates.pending)
|
|
50989
51015
|
)
|
|
@@ -51009,11 +51035,11 @@ async function sweepStaleAdsActivationsOnce(app, opts) {
|
|
|
51009
51035
|
updatedAt: nowIso
|
|
51010
51036
|
}).where(and39(
|
|
51011
51037
|
eq46(adsOperations.id, operation.id),
|
|
51012
|
-
|
|
51038
|
+
or11(
|
|
51013
51039
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
51014
51040
|
and39(
|
|
51015
51041
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
51016
|
-
|
|
51042
|
+
or11(
|
|
51017
51043
|
isNull7(adsOperations.leaseOwner),
|
|
51018
51044
|
isNull7(adsOperations.leaseExpiresAt),
|
|
51019
51045
|
lte10(adsOperations.leaseExpiresAt, nowIso)
|
|
@@ -51046,11 +51072,11 @@ async function sweepStaleAdsActivationsOnce(app, opts) {
|
|
|
51046
51072
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
51047
51073
|
app.db.update(adsOperations).set({ updatedAt: failedAt }).where(and39(
|
|
51048
51074
|
eq46(adsOperations.id, operation.id),
|
|
51049
|
-
|
|
51075
|
+
or11(
|
|
51050
51076
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
51051
51077
|
and39(
|
|
51052
51078
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
51053
|
-
|
|
51079
|
+
or11(
|
|
51054
51080
|
isNull7(adsOperations.leaseOwner),
|
|
51055
51081
|
isNull7(adsOperations.leaseExpiresAt),
|
|
51056
51082
|
lte10(adsOperations.leaseExpiresAt, failedAt)
|
|
@@ -52236,7 +52262,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
|
|
|
52236
52262
|
enforceBackoff ? lte11(adsOperations.updatedAt, unknownCutoff) : void 0
|
|
52237
52263
|
) : and40(
|
|
52238
52264
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52239
|
-
|
|
52265
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52240
52266
|
);
|
|
52241
52267
|
const claimed = app.db.update(adsOperations).set({
|
|
52242
52268
|
state: AdsOperationStates.reconciling,
|
|
@@ -52248,7 +52274,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
|
|
|
52248
52274
|
eq47(adsOperations.id, row.id),
|
|
52249
52275
|
eq47(adsOperations.projectId, row.projectId),
|
|
52250
52276
|
eq47(adsOperations.reconcileAttempts, row.reconcileAttempts),
|
|
52251
|
-
|
|
52277
|
+
lt8(adsOperations.reconcileAttempts, policy.maxAttempts),
|
|
52252
52278
|
claimableState
|
|
52253
52279
|
)).returning({ id: adsOperations.id }).all();
|
|
52254
52280
|
if (claimed.length === 0) return void 0;
|
|
@@ -52558,28 +52584,28 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52558
52584
|
);
|
|
52559
52585
|
}
|
|
52560
52586
|
);
|
|
52561
|
-
const retryableStateWhere =
|
|
52587
|
+
const retryableStateWhere = or12(
|
|
52562
52588
|
and40(
|
|
52563
52589
|
eq47(adsOperations.state, AdsOperationStates.pending),
|
|
52564
52590
|
lte11(adsOperations.updatedAt, pendingCutoff)
|
|
52565
52591
|
),
|
|
52566
52592
|
and40(
|
|
52567
52593
|
eq47(adsOperations.state, AdsOperationStates.unknown),
|
|
52568
|
-
|
|
52594
|
+
or12(...unknownRetryWindows)
|
|
52569
52595
|
),
|
|
52570
52596
|
and40(
|
|
52571
52597
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52572
|
-
|
|
52598
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52573
52599
|
)
|
|
52574
52600
|
);
|
|
52575
52601
|
const staleReconcileWhere = and40(
|
|
52576
|
-
|
|
52577
|
-
|
|
52578
|
-
|
|
52602
|
+
lt8(adsOperations.reconcileAttempts, config.policy.maxAttempts),
|
|
52603
|
+
ne7(adsOperations.kind, AdsOperationKinds.campaign_tree_activate),
|
|
52604
|
+
or12(
|
|
52579
52605
|
eq47(adsOperations.reconcileStrategy, AdsReconcileStrategies.known_entity),
|
|
52580
52606
|
and40(
|
|
52581
52607
|
eq47(adsOperations.reconcileStrategy, AdsReconcileStrategies.create_fingerprint),
|
|
52582
|
-
|
|
52608
|
+
or12(
|
|
52583
52609
|
isNotNull4(adsOperations.entityId),
|
|
52584
52610
|
inArray20(adsOperations.state, [
|
|
52585
52611
|
AdsOperationStates.pending,
|
|
@@ -52591,9 +52617,9 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52591
52617
|
retryableStateWhere
|
|
52592
52618
|
);
|
|
52593
52619
|
const exhaustedRows = app.db.select().from(adsOperations).where(and40(
|
|
52594
|
-
|
|
52620
|
+
ne7(adsOperations.kind, AdsOperationKinds.campaign_tree_activate),
|
|
52595
52621
|
gte12(adsOperations.reconcileAttempts, config.policy.maxAttempts),
|
|
52596
|
-
|
|
52622
|
+
or12(
|
|
52597
52623
|
and40(
|
|
52598
52624
|
eq47(adsOperations.state, AdsOperationStates.pending),
|
|
52599
52625
|
lte11(adsOperations.updatedAt, pendingCutoff)
|
|
@@ -52601,12 +52627,12 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52601
52627
|
eq47(adsOperations.state, AdsOperationStates.unknown),
|
|
52602
52628
|
and40(
|
|
52603
52629
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52604
|
-
|
|
52630
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52605
52631
|
)
|
|
52606
52632
|
),
|
|
52607
|
-
|
|
52633
|
+
or12(
|
|
52608
52634
|
isNull8(adsOperations.errorCode),
|
|
52609
|
-
|
|
52635
|
+
ne7(adsOperations.errorCode, ADS_RECONCILIATION_QUARANTINED)
|
|
52610
52636
|
)
|
|
52611
52637
|
)).orderBy(asc9(adsOperations.updatedAt)).limit(config.batchSize).all();
|
|
52612
52638
|
for (const row of exhaustedRows) {
|
|
@@ -52952,7 +52978,7 @@ async function adsRoutes(app, opts) {
|
|
|
52952
52978
|
const rows = app.db.select().from(adsOperations).where(and40(
|
|
52953
52979
|
eq47(adsOperations.projectId, project.id),
|
|
52954
52980
|
inArray20(adsOperations.state, parsed.data.state),
|
|
52955
|
-
cursor ?
|
|
52981
|
+
cursor ? or12(
|
|
52956
52982
|
gt3(adsOperations.createdAt, cursor.createdAt),
|
|
52957
52983
|
and40(
|
|
52958
52984
|
eq47(adsOperations.createdAt, cursor.createdAt),
|
|
@@ -58084,7 +58110,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
58084
58110
|
}
|
|
58085
58111
|
|
|
58086
58112
|
// ../api-routes/src/backlinks-filter.ts
|
|
58087
|
-
import { and as and44, ne as
|
|
58113
|
+
import { and as and44, ne as ne8, notLike } from "drizzle-orm";
|
|
58088
58114
|
var BACKLINK_FILTER_PATTERNS = [
|
|
58089
58115
|
"*.google.com",
|
|
58090
58116
|
"*.googleusercontent.com",
|
|
@@ -58101,10 +58127,10 @@ function backlinkCrawlerExclusionClause() {
|
|
|
58101
58127
|
for (const pattern of BACKLINK_FILTER_PATTERNS) {
|
|
58102
58128
|
if (pattern.startsWith("*.")) {
|
|
58103
58129
|
const suffix = pattern.slice(2);
|
|
58104
|
-
conditions.push(
|
|
58130
|
+
conditions.push(ne8(backlinkDomains.linkingDomain, suffix));
|
|
58105
58131
|
conditions.push(notLike(backlinkDomains.linkingDomain, `%.${suffix}`));
|
|
58106
58132
|
} else {
|
|
58107
|
-
conditions.push(
|
|
58133
|
+
conditions.push(ne8(backlinkDomains.linkingDomain, pattern));
|
|
58108
58134
|
}
|
|
58109
58135
|
}
|
|
58110
58136
|
const combined = and44(...conditions);
|
|
@@ -75138,10 +75164,10 @@ async function ackCloudflareQueueMessages(client, options) {
|
|
|
75138
75164
|
|
|
75139
75165
|
// ../api-routes/src/traffic-event-ingest.ts
|
|
75140
75166
|
import crypto43 from "crypto";
|
|
75141
|
-
import { and as and47, eq as eq53, gte as gte13, lt as
|
|
75167
|
+
import { and as and47, eq as eq53, gte as gte13, lt as lt10, lte as lte12, sql as sql22 } from "drizzle-orm";
|
|
75142
75168
|
|
|
75143
75169
|
// ../api-routes/src/raw-event-sample-retention.ts
|
|
75144
|
-
import { and as and46, eq as eq52, lt as
|
|
75170
|
+
import { and as and46, eq as eq52, lt as lt9, sql as sql21 } from "drizzle-orm";
|
|
75145
75171
|
var RAW_EVENT_SAMPLE_RETENTION_MS = 30 * 24 * 60 * 6e4;
|
|
75146
75172
|
var RAW_EVENT_SAMPLE_RETENTION_SWEEP_INTERVAL_MS = 24 * 60 * 6e4;
|
|
75147
75173
|
var CANONICAL_UTC_TIMESTAMP_GLOB = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9][0-9]Z";
|
|
@@ -75156,7 +75182,7 @@ function deleteExpiredRawEventSamples(db, cutoff, sourceId) {
|
|
|
75156
75182
|
db.delete(rawEventSamples).where(and46(
|
|
75157
75183
|
sourceScope,
|
|
75158
75184
|
sql21`${rawEventSamples.ts} GLOB ${CANONICAL_UTC_TIMESTAMP_GLOB}`,
|
|
75159
|
-
|
|
75185
|
+
lt9(rawEventSamples.ts, cutoff)
|
|
75160
75186
|
)).run();
|
|
75161
75187
|
db.delete(rawEventSamples).where(and46(
|
|
75162
75188
|
sourceScope,
|
|
@@ -75418,7 +75444,7 @@ function writeTrafficEventBatch(opts) {
|
|
|
75418
75444
|
const existing = tx.select({ total: sql22`COUNT(*)` }).from(rawEventSamples).where(and47(
|
|
75419
75445
|
eq53(rawEventSamples.sourceId, opts.sourceId),
|
|
75420
75446
|
gte13(rawEventSamples.ts, hour.start),
|
|
75421
|
-
|
|
75447
|
+
lt10(rawEventSamples.ts, hour.end)
|
|
75422
75448
|
)).get();
|
|
75423
75449
|
remaining = Math.max(0, hourlySampleLimit - Number(existing?.total ?? 0));
|
|
75424
75450
|
}
|
|
@@ -75475,7 +75501,7 @@ function writeTrafficEventBatch(opts) {
|
|
|
75475
75501
|
}
|
|
75476
75502
|
|
|
75477
75503
|
// ../api-routes/src/traffic-sync-lease.ts
|
|
75478
|
-
import { and as and48, eq as eq54, isNull as isNull9, lte as lte13, or as
|
|
75504
|
+
import { and as and48, eq as eq54, isNull as isNull9, lte as lte13, or as or13 } from "drizzle-orm";
|
|
75479
75505
|
function leaseExpiresAt(now, ttlMs) {
|
|
75480
75506
|
const nowMs = Date.parse(now);
|
|
75481
75507
|
if (!Number.isFinite(nowMs)) throw new RangeError("Traffic sync lease now must be an ISO instant");
|
|
@@ -75490,7 +75516,7 @@ function tryClaimTrafficSyncLease(input) {
|
|
|
75490
75516
|
syncLeaseExpiresAt: expiresAt
|
|
75491
75517
|
}).where(and48(
|
|
75492
75518
|
eq54(trafficSources.id, input.sourceId),
|
|
75493
|
-
|
|
75519
|
+
or13(
|
|
75494
75520
|
isNull9(trafficSources.syncLeaseOwner),
|
|
75495
75521
|
lte13(trafficSources.syncLeaseExpiresAt, input.now),
|
|
75496
75522
|
eq54(trafficSources.syncLeaseOwner, input.owner)
|
|
@@ -80794,7 +80820,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
80794
80820
|
];
|
|
80795
80821
|
|
|
80796
80822
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
80797
|
-
import { and as and53, eq as eq62, gte as gte15, inArray as inArray23, ne as
|
|
80823
|
+
import { and as and53, eq as eq62, gte as gte15, inArray as inArray23, ne as ne9, sql as sql24 } from "drizzle-orm";
|
|
80798
80824
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
80799
80825
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
80800
80826
|
function isCloudflareDirectPush(source) {
|
|
@@ -80835,7 +80861,7 @@ function loadProbes(ctx) {
|
|
|
80835
80861
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
80836
80862
|
and53(
|
|
80837
80863
|
eq62(trafficSources.projectId, ctx.project.id),
|
|
80838
|
-
|
|
80864
|
+
ne9(trafficSources.status, TrafficSourceStatuses.archived)
|
|
80839
80865
|
)
|
|
80840
80866
|
).all();
|
|
80841
80867
|
return rows.map((r) => ({
|
|
@@ -81987,7 +82013,7 @@ async function doctorRoutes(app, opts) {
|
|
|
81987
82013
|
|
|
81988
82014
|
// ../api-routes/src/discovery/routes.ts
|
|
81989
82015
|
import crypto46 from "crypto";
|
|
81990
|
-
import { and as and54, desc as desc27, eq as eq63, gte as gte16, inArray as inArray24, isNull as isNull10, or as
|
|
82016
|
+
import { and as and54, desc as desc27, eq as eq63, gte as gte16, inArray as inArray24, isNull as isNull10, or as or14 } from "drizzle-orm";
|
|
81991
82017
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
81992
82018
|
async function discoveryRoutes(app, opts) {
|
|
81993
82019
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -82037,7 +82063,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
82037
82063
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
82038
82064
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
82039
82065
|
// upgrade, never a wrong reuse.
|
|
82040
|
-
locations.length === 0 ?
|
|
82066
|
+
locations.length === 0 ? or14(isNull10(discoverySessions.locations), eq63(discoverySessions.locations, locations)) : eq63(discoverySessions.locations, locations),
|
|
82041
82067
|
// Seed provider set is identity: a different phrasing distribution
|
|
82042
82068
|
// must never reuse another set's session. Null (= Gemini-only
|
|
82043
82069
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
@@ -82667,7 +82693,7 @@ function dedupeStrings(input) {
|
|
|
82667
82693
|
|
|
82668
82694
|
// ../api-routes/src/technical-aeo.ts
|
|
82669
82695
|
import crypto48 from "crypto";
|
|
82670
|
-
import { and as and55, asc as asc11, count as count2, desc as desc28, eq as eq65, inArray as inArray25, isNotNull as isNotNull5, isNull as isNull11, lt as
|
|
82696
|
+
import { and as and55, asc as asc11, count as count2, desc as desc28, eq as eq65, inArray as inArray25, isNotNull as isNotNull5, isNull as isNull11, lt as lt11, or as or15, sql as sql25 } from "drizzle-orm";
|
|
82671
82697
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
82672
82698
|
var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
|
|
82673
82699
|
...SiteCrawlFetchedStates,
|
|
@@ -83063,7 +83089,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83063
83089
|
return emptyScore(project.name);
|
|
83064
83090
|
}
|
|
83065
83091
|
const snap = latest.snap;
|
|
83066
|
-
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq65(siteAuditSnapshots.runId, runs.id)).where(and55(...baseFilters,
|
|
83092
|
+
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq65(siteAuditSnapshots.runId, runs.id)).where(and55(...baseFilters, lt11(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc28(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
|
|
83067
83093
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
83068
83094
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
83069
83095
|
return {
|
|
@@ -83309,7 +83335,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83309
83335
|
eq65(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
|
|
83310
83336
|
eq65(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
|
|
83311
83337
|
eq65(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
|
|
83312
|
-
)).where(and55(...graphScope,
|
|
83338
|
+
)).where(and55(...graphScope, lt11(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc11(siteCrawlGraphNodes.sampleRank)).all();
|
|
83313
83339
|
const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
|
|
83314
83340
|
...row,
|
|
83315
83341
|
healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey })
|
|
@@ -83327,18 +83353,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83327
83353
|
eq65(graphSourceNode.runId, siteCrawlGraphEdges.runId),
|
|
83328
83354
|
eq65(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
83329
83355
|
eq65(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
|
|
83330
|
-
|
|
83356
|
+
lt11(graphSourceNode.sampleRank, maxNodes)
|
|
83331
83357
|
)).innerJoin(graphTargetNode, and55(
|
|
83332
83358
|
eq65(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
|
|
83333
83359
|
eq65(graphTargetNode.runId, siteCrawlGraphEdges.runId),
|
|
83334
83360
|
eq65(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
83335
83361
|
eq65(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
|
|
83336
|
-
|
|
83362
|
+
lt11(graphTargetNode.sampleRank, maxNodes)
|
|
83337
83363
|
)).where(and55(
|
|
83338
83364
|
eq65(siteCrawlGraphEdges.projectId, project.id),
|
|
83339
83365
|
eq65(siteCrawlGraphEdges.runId, snapshot.runId),
|
|
83340
83366
|
eq65(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
|
|
83341
|
-
|
|
83367
|
+
lt11(siteCrawlGraphEdges.sampleRank, maxEdges),
|
|
83342
83368
|
graphLinkKindFilter
|
|
83343
83369
|
)).orderBy(asc11(siteCrawlGraphEdges.sampleRank)).all();
|
|
83344
83370
|
const totalNodes = persistedLayout.totalNodes;
|
|
@@ -83437,7 +83463,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83437
83463
|
eq65(siteCrawlEdges.attemptId, scope.attemptId),
|
|
83438
83464
|
eq65(siteCrawlEdges.internal, true),
|
|
83439
83465
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
83440
|
-
|
|
83466
|
+
or15(
|
|
83441
83467
|
inArray25(siteCrawlEdges.sourceNodeKey, frontier),
|
|
83442
83468
|
inArray25(siteCrawlEdges.targetNodeKey, frontier)
|
|
83443
83469
|
)
|
|
@@ -83684,11 +83710,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83684
83710
|
if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
|
|
83685
83711
|
const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq65(siteCrawlSnapshots.runId, runs.id)).where(and55(
|
|
83686
83712
|
...completeSnapshotFilters,
|
|
83687
|
-
|
|
83688
|
-
|
|
83713
|
+
or15(
|
|
83714
|
+
lt11(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
83689
83715
|
and55(
|
|
83690
83716
|
eq65(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
83691
|
-
|
|
83717
|
+
lt11(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
|
|
83692
83718
|
)
|
|
83693
83719
|
)
|
|
83694
83720
|
)).orderBy(desc28(siteCrawlSnapshots.createdAt), desc28(siteCrawlSnapshots.runId)).limit(1).get() : void 0;
|
|
@@ -84281,8 +84307,8 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84281
84307
|
eq65(siteCrawlEdges.internal, true),
|
|
84282
84308
|
linkKindFilter(linkKind)
|
|
84283
84309
|
];
|
|
84284
|
-
const inboundMatch = request.query.nodeKey && request.query.url ?
|
|
84285
|
-
const outboundMatch = request.query.nodeKey && request.query.url ?
|
|
84310
|
+
const inboundMatch = request.query.nodeKey && request.query.url ? or15(eq65(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq65(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq65(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq65(siteCrawlEdges.targetUrl, request.query.url);
|
|
84311
|
+
const outboundMatch = request.query.nodeKey && request.query.url ? or15(eq65(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq65(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq65(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq65(siteCrawlEdges.sourceUrl, request.query.url);
|
|
84286
84312
|
const limit = parseBoundedLimit(request.query.limit, 50, 100);
|
|
84287
84313
|
const inboundRows = app.db.select().from(siteCrawlEdges).where(and55(...scope, inboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
84288
84314
|
const outboundRows = app.db.select().from(siteCrawlEdges).where(and55(...scope, outboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
@@ -84484,7 +84510,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84484
84510
|
}).from(siteCrawlPages).where(and55(
|
|
84485
84511
|
auditedWhere,
|
|
84486
84512
|
isNotNull5(siteCrawlPages.auditScore),
|
|
84487
|
-
|
|
84513
|
+
lt11(siteCrawlPages.auditScore, 70)
|
|
84488
84514
|
)).orderBy(asc11(siteCrawlPages.auditScore), asc11(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
|
|
84489
84515
|
const examples = candidates.flatMap((candidate) => {
|
|
84490
84516
|
if (candidate.auditScore == null) return [];
|
|
@@ -84577,7 +84603,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84577
84603
|
// ../api-routes/src/research.ts
|
|
84578
84604
|
import crypto49 from "crypto";
|
|
84579
84605
|
import { z as z5 } from "zod";
|
|
84580
|
-
import { and as and56, count as count3, desc as desc29, eq as eq66, gte as gte17, lt as
|
|
84606
|
+
import { and as and56, count as count3, desc as desc29, eq as eq66, gte as gte17, lt as lt12, or as or16, sql as sql26 } from "drizzle-orm";
|
|
84581
84607
|
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
84582
84608
|
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
84583
84609
|
async function researchRoutes(app, opts) {
|
|
@@ -84638,7 +84664,7 @@ async function researchRoutes(app, opts) {
|
|
|
84638
84664
|
const used = tx.select({ value: count3() }).from(researchRuns).where(and56(
|
|
84639
84665
|
eq66(researchRuns.projectId, project.id),
|
|
84640
84666
|
gte17(researchRuns.createdAt, start),
|
|
84641
|
-
|
|
84667
|
+
lt12(researchRuns.createdAt, end),
|
|
84642
84668
|
sql26`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
84643
84669
|
)).get()?.value ?? 0;
|
|
84644
84670
|
if (used >= viewerDailyRunLimit) {
|
|
@@ -84693,7 +84719,7 @@ async function researchRoutes(app, opts) {
|
|
|
84693
84719
|
const used = tx.select({ value: count3() }).from(researchRuns).where(and56(
|
|
84694
84720
|
eq66(researchRuns.projectId, project.id),
|
|
84695
84721
|
gte17(researchRuns.createdAt, start),
|
|
84696
|
-
|
|
84722
|
+
lt12(researchRuns.createdAt, end),
|
|
84697
84723
|
sql26`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
84698
84724
|
)).get()?.value ?? 0;
|
|
84699
84725
|
if (used + prepared.length > viewerDailyRunLimit) throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
@@ -84747,7 +84773,7 @@ async function researchRoutes(app, opts) {
|
|
|
84747
84773
|
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
84748
84774
|
const rows = app.db.select().from(researchRuns).where(and56(
|
|
84749
84775
|
eq66(researchRuns.projectId, project.id),
|
|
84750
|
-
cursor ?
|
|
84776
|
+
cursor ? or16(lt12(researchRuns.createdAt, cursor.createdAt), and56(eq66(researchRuns.createdAt, cursor.createdAt), lt12(researchRuns.id, cursor.id))) : void 0
|
|
84751
84777
|
)).orderBy(desc29(researchRuns.createdAt), desc29(researchRuns.id)).limit(limit + 1).all();
|
|
84752
84778
|
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
84753
84779
|
const last = runs2.at(-1);
|
|
@@ -85924,11 +85950,11 @@ function analyzableRunPredicates(projectId) {
|
|
|
85924
85950
|
eq68(runs.projectId, projectId),
|
|
85925
85951
|
// Only this kind writes query_snapshots.
|
|
85926
85952
|
eq68(runs.kind, RunKinds["answer-visibility"]),
|
|
85927
|
-
|
|
85953
|
+
or17(eq68(runs.status, RunStatuses.completed), eq68(runs.status, RunStatuses.partial)),
|
|
85928
85954
|
// Defensive: RunCoordinator already skips probes before analyzeAndPersist
|
|
85929
85955
|
// is called, but a future call site invoking it directly for a probe must
|
|
85930
85956
|
// still not pollute the intelligence window.
|
|
85931
|
-
|
|
85957
|
+
ne10(runs.trigger, RunTriggers.probe)
|
|
85932
85958
|
];
|
|
85933
85959
|
}
|
|
85934
85960
|
function readStoredGroundingSources(rawResponse) {
|
|
@@ -86514,9 +86540,9 @@ var IntelligenceService = class {
|
|
|
86514
86540
|
and58(
|
|
86515
86541
|
eq68(runs.projectId, projectId),
|
|
86516
86542
|
eq68(runs.kind, RunKinds["answer-visibility"]),
|
|
86517
|
-
|
|
86543
|
+
or17(eq68(runs.status, "completed"), eq68(runs.status, "partial")),
|
|
86518
86544
|
// Defensive — see top of file.
|
|
86519
|
-
|
|
86545
|
+
ne10(runs.trigger, RunTriggers.probe)
|
|
86520
86546
|
)
|
|
86521
86547
|
).orderBy(desc30(runs.createdAt), desc30(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
86522
86548
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|