@ainyc/canonry 5.5.0 → 5.8.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/SKILL.md +18 -0
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +1 -1
- package/assets/agent-workspace/skills/aero/soul.md +2 -0
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +10 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +5 -4
- package/assets/assets/{AuditHistoryPanel-BMGGxHpw.js → AuditHistoryPanel-DumM4n44.js} +1 -1
- package/assets/assets/{BacklinksPage-CS3wJSWe.js → BacklinksPage-DHW-aDYf.js} +1 -1
- package/assets/assets/ChartPrimitives-BQC2VH0O.js +1 -0
- package/assets/assets/{HistoryPage-Ba9ezeuP.js → HistoryPage-DJgCpcmY.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Y9eseEv0.js → MeasurementPropertyPage-CblC7DUV.js} +1 -1
- package/assets/assets/ProjectPage-BKZyG1S3.js +8 -0
- package/assets/assets/{RunRow-DbUtgNUI.js → RunRow-plH2LI7o.js} +1 -1
- package/assets/assets/{RunsPage-BBuFIYuS.js → RunsPage-JCSy7P9I.js} +1 -1
- package/assets/assets/{SettingsPage-DNnR33D-.js → SettingsPage-B8KXT4YK.js} +1 -1
- package/assets/assets/{SiteHealthSection-Cf4KHWp0.js → SiteHealthSection-06pNjngi.js} +3 -3
- package/assets/assets/{TrafficPage-DSQjaZM9.js → TrafficPage-BYLDNQ1A.js} +5 -5
- package/assets/assets/{TrafficSourceDetailPage-C-Pg69TW.js → TrafficSourceDetailPage-CxseBpYr.js} +1 -1
- package/assets/assets/{extract-error-message-DNLmL0ID.js → extract-error-message-Dv5fwiqy.js} +1 -1
- package/assets/assets/index-2Ldc0Hq4.js +86 -0
- package/assets/assets/index-neNKCg78.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-DkItaBPY.js → react-sigma_core.esm.min-C8381RhL.js} +1 -1
- package/assets/assets/v2-overview-adapter-BQD1Ap8K.js +1 -0
- package/assets/assets/vendor-markdown-B4giJcws.js +29 -0
- package/assets/assets/{vendor-recharts-D-cUuLZo.js → vendor-recharts-llkUFBGR.js} +1 -1
- package/assets/index.html +4 -4
- package/dist/{chunk-BL3FMPSM.js → chunk-5E4ASNU4.js} +857 -784
- package/dist/{chunk-75ECQ77U.js → chunk-BO53QARL.js} +49 -16
- package/dist/{chunk-REQEWYNF.js → chunk-D3DKDTXP.js} +1027 -70
- package/dist/{chunk-DX4CZNNU.js → chunk-LR7BVM53.js} +2 -2
- package/dist/{chunk-LQAOGFFK.js → chunk-R4COV5KR.js} +2 -2
- package/dist/{chunk-KKDXJWDK.js → chunk-S4DBS5DP.js} +4 -4
- package/dist/cli.js +10 -9
- package/dist/{demo-server-BSH6SSWD.js → demo-server-WRVVNKLS.js} +7 -3
- package/dist/index.d.ts +20 -8
- package/dist/index.js +5 -5
- package/dist/{intelligence-service-CU7W5AVC.js → intelligence-service-4XMMO3JK.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
- package/assets/assets/vendor-markdown-B-Oq2NYq.js +0 -14
|
@@ -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";
|
|
@@ -7346,6 +7406,26 @@ var MIGRATION_VERSIONS = [
|
|
|
7346
7406
|
statements: [
|
|
7347
7407
|
`ALTER TABLE doctor_health_state ADD COLUMN failing_signature TEXT`
|
|
7348
7408
|
]
|
|
7409
|
+
},
|
|
7410
|
+
{
|
|
7411
|
+
version: 158,
|
|
7412
|
+
name: "deepinfra-agent-default-model",
|
|
7413
|
+
// Move the previous agent default once. A later explicit GLM selection
|
|
7414
|
+
// must survive subsequent boots and session hydration.
|
|
7415
|
+
//
|
|
7416
|
+
// A run hook rather than a statement for two reasons. The statement
|
|
7417
|
+
// allowlist is schema-shaped and this rewrites VALUES, and a supported
|
|
7418
|
+
// legacy database can reach this version with no `agent_sessions` at
|
|
7419
|
+
// all: one stamped in `_migrations` at a version above 38 resumes past
|
|
7420
|
+
// the migration that introduces the table, so an unguarded UPDATE dies
|
|
7421
|
+
// with `no such table`.
|
|
7422
|
+
statements: [],
|
|
7423
|
+
run: (tx) => {
|
|
7424
|
+
if (!tableExists(tx, "agent_sessions")) return;
|
|
7425
|
+
tx.run(sql2.raw(
|
|
7426
|
+
`UPDATE agent_sessions SET model_id = 'deepseek-ai/DeepSeek-V4-Flash' WHERE model_provider = 'deepinfra' AND model_id = 'zai-org/GLM-5.2'`
|
|
7427
|
+
));
|
|
7428
|
+
}
|
|
7349
7429
|
}
|
|
7350
7430
|
];
|
|
7351
7431
|
function addRunsMeasurementPlanVersionForeignKey(tx) {
|
|
@@ -11285,6 +11365,22 @@ function requireAdminSession(request) {
|
|
|
11285
11365
|
if (!role || role === UserRoles.admin) return;
|
|
11286
11366
|
throw forbidden(ADMIN_ONLY_MESSAGE);
|
|
11287
11367
|
}
|
|
11368
|
+
var NARROW_KEY_DENIED_MESSAGE = "This API key is narrower than the install, so it cannot use an administrator surface.";
|
|
11369
|
+
function isInstanceAdministrator(request) {
|
|
11370
|
+
const principal = request.principal;
|
|
11371
|
+
if (!principal) return true;
|
|
11372
|
+
const role = principal.kind === "user" ? principal.role : principal.delegatedUser?.role;
|
|
11373
|
+
if (role && role !== UserRoles.admin) return false;
|
|
11374
|
+
return !principal.projectId && principal.scopes.includes(WILDCARD_SCOPE);
|
|
11375
|
+
}
|
|
11376
|
+
function requireInstanceAdministrator(request) {
|
|
11377
|
+
const principal = request.principal;
|
|
11378
|
+
if (!principal) return;
|
|
11379
|
+
const role = principal.kind === "user" ? principal.role : principal.delegatedUser?.role;
|
|
11380
|
+
if (role && role !== UserRoles.admin) throw forbidden(ADMIN_ONLY_MESSAGE);
|
|
11381
|
+
if (isInstanceAdministrator(request)) return;
|
|
11382
|
+
throw forbidden(NARROW_KEY_DENIED_MESSAGE);
|
|
11383
|
+
}
|
|
11288
11384
|
function requireOperator(request) {
|
|
11289
11385
|
const principal = request.principal;
|
|
11290
11386
|
if (request.operatorAccess === true && principal?.kind === "api-key" && !principal.viaCookie && !principal.projectId && !principal.delegatedUser) return;
|
|
@@ -22786,7 +22882,59 @@ async function measurementPortfolioReadRoutes(app) {
|
|
|
22786
22882
|
|
|
22787
22883
|
// ../api-routes/src/visibility-report.ts
|
|
22788
22884
|
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,
|
|
22885
|
+
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";
|
|
22886
|
+
|
|
22887
|
+
// ../api-routes/src/measurement-scope-options.ts
|
|
22888
|
+
function marketLinks(keys, settings) {
|
|
22889
|
+
if (!settings.marketLinks) return {};
|
|
22890
|
+
const marketKeys = [...new Set(keys ?? [])].sort();
|
|
22891
|
+
return marketKeys.length === 0 ? {} : { marketKeys };
|
|
22892
|
+
}
|
|
22893
|
+
function planScopeOptions(plan, settings) {
|
|
22894
|
+
const groupKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
22895
|
+
for (const group of plan.groups) {
|
|
22896
|
+
for (const targetKey of group.targetKeys) groupKeysForTarget.get(targetKey)?.push(group.stableKey);
|
|
22897
|
+
}
|
|
22898
|
+
const marketKeysForGroup = /* @__PURE__ */ new Map();
|
|
22899
|
+
const marketKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
|
|
22900
|
+
for (const market of plan.reportingScopes ?? []) {
|
|
22901
|
+
if (market.groupKey !== void 0) {
|
|
22902
|
+
const keys = marketKeysForGroup.get(market.groupKey) ?? [];
|
|
22903
|
+
keys.push(market.stableKey);
|
|
22904
|
+
marketKeysForGroup.set(market.groupKey, keys);
|
|
22905
|
+
}
|
|
22906
|
+
for (const edge of market.usageEdges) marketKeysForTarget.get(edge.targetKey)?.push(market.stableKey);
|
|
22907
|
+
}
|
|
22908
|
+
return [
|
|
22909
|
+
{ id: "project", label: "Project", kind: "project", targetCount: plan.targets.length },
|
|
22910
|
+
...plan.groups.map((group) => ({
|
|
22911
|
+
id: group.stableKey,
|
|
22912
|
+
label: group.label,
|
|
22913
|
+
kind: "group",
|
|
22914
|
+
targetCount: group.targetKeys.length,
|
|
22915
|
+
...group.parentGroupKey === void 0 ? {} : { parentGroupIds: [group.parentGroupKey] },
|
|
22916
|
+
...marketLinks(marketKeysForGroup.get(group.stableKey), settings)
|
|
22917
|
+
})),
|
|
22918
|
+
...(plan.reportingScopes ?? []).map((market) => ({
|
|
22919
|
+
id: market.stableKey,
|
|
22920
|
+
label: market.label,
|
|
22921
|
+
kind: "market",
|
|
22922
|
+
targetCount: new Set(market.usageEdges.map((edge) => edge.targetKey)).size,
|
|
22923
|
+
...market.groupKey === void 0 ? {} : { parentGroupIds: [market.groupKey] }
|
|
22924
|
+
})),
|
|
22925
|
+
...plan.targets.map((target) => ({
|
|
22926
|
+
id: target.stableKey,
|
|
22927
|
+
label: target.label,
|
|
22928
|
+
kind: "property",
|
|
22929
|
+
targetCount: 1,
|
|
22930
|
+
parentGroupIds: [...new Set(groupKeysForTarget.get(target.stableKey) ?? [])].sort(),
|
|
22931
|
+
...marketLinks(marketKeysForTarget.get(target.stableKey), settings)
|
|
22932
|
+
}))
|
|
22933
|
+
];
|
|
22934
|
+
}
|
|
22935
|
+
function simpleScopeOptions() {
|
|
22936
|
+
return [{ id: "project", label: "Project", kind: "project", targetCount: 1 }];
|
|
22937
|
+
}
|
|
22790
22938
|
|
|
22791
22939
|
// ../api-routes/src/visibility-report-reader.ts
|
|
22792
22940
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -22797,9 +22945,12 @@ var VisibilityReportCursorError = class extends Error {
|
|
|
22797
22945
|
}
|
|
22798
22946
|
};
|
|
22799
22947
|
var VisibilityReportScopeError = class extends Error {
|
|
22800
|
-
|
|
22948
|
+
/** Set only when the selected scope or market key is absent from the frozen definition. */
|
|
22949
|
+
details;
|
|
22950
|
+
constructor(message, details) {
|
|
22801
22951
|
super(message);
|
|
22802
22952
|
this.name = "VisibilityReportScopeError";
|
|
22953
|
+
this.details = details;
|
|
22803
22954
|
}
|
|
22804
22955
|
};
|
|
22805
22956
|
var ALL_CLASSES = ["branded", "non-brand", "unknown"];
|
|
@@ -22840,13 +22991,13 @@ function scopeResolution(definition, selection) {
|
|
|
22840
22991
|
const key = selection.scopeKey;
|
|
22841
22992
|
if (!key) throw new VisibilityReportScopeError(`${selection.scope} scope requires a scope key.`);
|
|
22842
22993
|
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
|
|
22994
|
+
if (!scoped) throw new VisibilityReportScopeError(`${selection.scope} scope "${key}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: selection.scope, key });
|
|
22844
22995
|
option = scoped;
|
|
22845
22996
|
if (selection.scope === "property") {
|
|
22846
22997
|
edgeIds = new Set(definition.edges.filter((edge) => edge.targetKey === key).map((edge) => edge.id));
|
|
22847
22998
|
} else if (selection.scope === "group") {
|
|
22848
22999
|
const group = definition.groups.find((candidate) => candidate.id === key);
|
|
22849
|
-
if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition
|
|
23000
|
+
if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-scope"], kind: "group", key });
|
|
22850
23001
|
const targetKeys = new Set(group.targetKeys);
|
|
22851
23002
|
edgeIds = new Set(definition.edges.filter((edge) => targetKeys.has(edge.targetKey)).map((edge) => edge.id));
|
|
22852
23003
|
} else {
|
|
@@ -22859,7 +23010,7 @@ function scopeResolution(definition, selection) {
|
|
|
22859
23010
|
const marketKey = selection.scope === "market" ? selection.scopeKey : selection.marketKey;
|
|
22860
23011
|
if (marketKey === void 0) return { option, edgeIds };
|
|
22861
23012
|
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
|
|
23013
|
+
if (!market) throw new VisibilityReportScopeError(`Market "${marketKey}" is not in this frozen definition.`, { reason: VisibilityReportScopeErrorReasons["retired-market"], kind: "market", key: marketKey });
|
|
22863
23014
|
return {
|
|
22864
23015
|
option,
|
|
22865
23016
|
market,
|
|
@@ -22873,7 +23024,7 @@ function scopeTargetKeys(definition, selection) {
|
|
|
22873
23024
|
if (selection.scope === "property") return [resolution.option.id];
|
|
22874
23025
|
if (selection.scope === "group") {
|
|
22875
23026
|
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
|
|
23027
|
+
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
23028
|
return [...group.targetKeys].sort(compareText14);
|
|
22878
23029
|
}
|
|
22879
23030
|
return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
|
|
@@ -23254,17 +23405,69 @@ function filterOptions(definition, selection, run) {
|
|
|
23254
23405
|
}
|
|
23255
23406
|
return { providers, models, locations };
|
|
23256
23407
|
}
|
|
23257
|
-
function
|
|
23258
|
-
if (!previous) return { state: "first", comparedRunId: null };
|
|
23408
|
+
function pairContinuity(previous, current) {
|
|
23259
23409
|
const currentLegacy = current.definition.provenance.kind === "legacy-simple";
|
|
23260
23410
|
const previousLegacy = previous.definition.provenance.kind === "legacy-simple";
|
|
23261
|
-
if (currentLegacy || previousLegacy) return
|
|
23411
|
+
if (currentLegacy || previousLegacy) return "legacy-unknown";
|
|
23262
23412
|
const sameDefinition = current.definitionId !== null && previous.definitionId !== null && (current.definitionId === previous.definitionId || current.comparableDefinitionIds.includes(previous.definitionId));
|
|
23263
|
-
if (!sameDefinition) return
|
|
23413
|
+
if (!sameDefinition) return "definition-changed";
|
|
23264
23414
|
if (current.modelFingerprint === null || previous.modelFingerprint === null || current.modelFingerprint !== previous.modelFingerprint) {
|
|
23265
|
-
return
|
|
23415
|
+
return "model-changed";
|
|
23416
|
+
}
|
|
23417
|
+
return "comparable";
|
|
23418
|
+
}
|
|
23419
|
+
function continuity(previous, current) {
|
|
23420
|
+
if (!previous) return { state: "first", comparedRunId: null };
|
|
23421
|
+
return { state: pairContinuity(previous, current), comparedRunId: previous.id };
|
|
23422
|
+
}
|
|
23423
|
+
function comparedRun(run) {
|
|
23424
|
+
return { id: run.id, createdAt: run.createdAt, completedAt: run.completedAt };
|
|
23425
|
+
}
|
|
23426
|
+
function previousSummaries(run, selection, classes) {
|
|
23427
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
23428
|
+
for (const queryClass of classes) {
|
|
23429
|
+
try {
|
|
23430
|
+
const { candidates } = candidatesFor(run, selection, queryClass);
|
|
23431
|
+
summaries.set(queryClass, summary(candidates, run.definition, scopeTargetKeys(run.definition, selection)));
|
|
23432
|
+
} catch (error) {
|
|
23433
|
+
if (!(error instanceof VisibilityReportScopeError)) return void 0;
|
|
23434
|
+
summaries.set(queryClass, null);
|
|
23435
|
+
}
|
|
23436
|
+
}
|
|
23437
|
+
return summaries;
|
|
23438
|
+
}
|
|
23439
|
+
function comparisonBasis(selectedRun, previous, selection, classes) {
|
|
23440
|
+
if (previous !== null && "unreadable" in previous) return void 0;
|
|
23441
|
+
if (!selectedRun) return { state: "unavailable", reason: "no-selected-run", previousRun: null };
|
|
23442
|
+
if (selectedRun.scoped) return { state: "unavailable", reason: "scoped-run", previousRun: null };
|
|
23443
|
+
if (previous === null) return { state: "unavailable", reason: "no-previous-run", previousRun: null };
|
|
23444
|
+
if ("incomparable" in previous) return { state: "unavailable", reason: "definition-changed", previousRun: previous.incomparable };
|
|
23445
|
+
const pair = pairContinuity(previous.run, selectedRun);
|
|
23446
|
+
if (pair !== "comparable") return { state: "unavailable", reason: pair, previousRun: comparedRun(previous.run) };
|
|
23447
|
+
if (selectedRun.state === "partial" || previous.run.state === "partial") {
|
|
23448
|
+
return { state: "unavailable", reason: "partial-run", previousRun: comparedRun(previous.run) };
|
|
23449
|
+
}
|
|
23450
|
+
const summaries = previousSummaries(previous.run, selection, classes);
|
|
23451
|
+
return summaries === void 0 ? void 0 : { state: "available", previous: previous.run, summaries };
|
|
23452
|
+
}
|
|
23453
|
+
function rateChange(current, previous) {
|
|
23454
|
+
if (current.rate === null) {
|
|
23455
|
+
return { state: "unavailable", reason: current.reason === "not-applicable" ? "not-applicable" : "current-unavailable" };
|
|
23266
23456
|
}
|
|
23267
|
-
return { state: "
|
|
23457
|
+
if (previous === null || previous.rate === null) return { state: "unavailable", reason: "previous-unavailable" };
|
|
23458
|
+
return { state: "available", previous, delta: current.rate - previous.rate };
|
|
23459
|
+
}
|
|
23460
|
+
function populationComparison(basis, current, selection, queryClass) {
|
|
23461
|
+
if (basis.state === "unavailable") return { state: "unavailable", reason: basis.reason, previousRun: basis.previousRun };
|
|
23462
|
+
const previous = basis.summaries.get(queryClass) ?? null;
|
|
23463
|
+
const currentReach = selection.scope === "property" ? unavailable4("not-applicable") : current.propertyReach;
|
|
23464
|
+
return {
|
|
23465
|
+
state: "available",
|
|
23466
|
+
previousRun: comparedRun(basis.previous),
|
|
23467
|
+
mentionCoverage: rateChange(current.mentionCoverage, previous?.mentionCoverage ?? null),
|
|
23468
|
+
citationCoverage: rateChange(current.citationCoverage, previous?.citationCoverage ?? null),
|
|
23469
|
+
propertyReach: rateChange(currentReach, previous?.propertyReach ?? null)
|
|
23470
|
+
};
|
|
23268
23471
|
}
|
|
23269
23472
|
function inTime(run, selection) {
|
|
23270
23473
|
return (selection.from === void 0 || run.createdAt >= selection.from) && (selection.to === void 0 || run.createdAt <= selection.to);
|
|
@@ -23305,8 +23508,12 @@ function buildVisibilityReport(input) {
|
|
|
23305
23508
|
}
|
|
23306
23509
|
runCandidates.set(run.id, byClass);
|
|
23307
23510
|
}
|
|
23511
|
+
const basis = input.previous === void 0 ? void 0 : comparisonBasis(selectedRun, input.previous, input.selection, classes);
|
|
23308
23512
|
const populations = classes.map((queryClass) => {
|
|
23309
|
-
if (!selectedRun)
|
|
23513
|
+
if (!selectedRun) {
|
|
23514
|
+
const empty = emptyPopulation(queryClass, definition, input);
|
|
23515
|
+
return basis === void 0 ? empty : { ...empty, comparison: populationComparison(basis, empty.summary, input.selection, queryClass) };
|
|
23516
|
+
}
|
|
23310
23517
|
const selected = runCandidates.get(selectedRun.id)?.get(queryClass) ?? [];
|
|
23311
23518
|
const trend = runs2.map((run, index2) => {
|
|
23312
23519
|
const candidates = runCandidates.get(run.id)?.get(queryClass) ?? [];
|
|
@@ -23325,10 +23532,12 @@ function buildVisibilityReport(input) {
|
|
|
23325
23532
|
});
|
|
23326
23533
|
const rows = queryRows(selected, definition, input.selection);
|
|
23327
23534
|
const evidence = evidenceRows(selected, definition, input.selection);
|
|
23535
|
+
const selectedSummary = summary(selected, definition, populationTargetKeys);
|
|
23328
23536
|
return {
|
|
23329
23537
|
queryClass,
|
|
23330
|
-
summary:
|
|
23538
|
+
summary: selectedSummary,
|
|
23331
23539
|
trend,
|
|
23540
|
+
...basis === void 0 ? {} : { comparison: populationComparison(basis, selectedSummary, input.selection, queryClass) },
|
|
23332
23541
|
queries: page2(rows, (row) => [row.queryKey, row.provider, row.model ?? "", row.location ?? ""].join("\0"), "queries", queryClass, input, selectedRun),
|
|
23333
23542
|
evidence: page2(evidence, (row) => [row.answerId, row.runId].join("\0"), "evidence", queryClass, input, selectedRun),
|
|
23334
23543
|
competitorAvailability: definition.competitorAvailability,
|
|
@@ -23524,56 +23733,10 @@ function v2Definition(plan, revision, manifest, run) {
|
|
|
23524
23733
|
location: slot.context?.label ?? null
|
|
23525
23734
|
};
|
|
23526
23735
|
});
|
|
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
23736
|
return {
|
|
23574
23737
|
revision,
|
|
23575
23738
|
provenance: { kind: "frozen-advanced", definitionRevision: revision },
|
|
23576
|
-
scopeOptions,
|
|
23739
|
+
scopeOptions: planScopeOptions(plan, { marketLinks: true }),
|
|
23577
23740
|
targets: plan.targets.map((target) => ({ id: target.stableKey, label: target.label, mentionEligible: !target.mentionNotApplicable })),
|
|
23578
23741
|
groups: plan.groups.map((group) => ({ id: group.stableKey, label: group.label, targetKeys: group.targetKeys })),
|
|
23579
23742
|
competitorAvailability: { state: "available" },
|
|
@@ -23623,6 +23786,7 @@ function advancedRun(run, version, plan, snapshots, comparableDefinitionIds, inc
|
|
|
23623
23786
|
completedAt: run.finishedAt,
|
|
23624
23787
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23625
23788
|
probe: false,
|
|
23789
|
+
scoped: run.measurementScope !== null,
|
|
23626
23790
|
definition: v2Definition(plan, version.revision, populated.manifest, run),
|
|
23627
23791
|
definitionId: version.id,
|
|
23628
23792
|
comparableDefinitionIds,
|
|
@@ -23634,7 +23798,7 @@ function simpleScope(label, mentionEligible) {
|
|
|
23634
23798
|
return {
|
|
23635
23799
|
revision: null,
|
|
23636
23800
|
provenance: { kind: "legacy-simple", definitionRevision: null },
|
|
23637
|
-
scopeOptions:
|
|
23801
|
+
scopeOptions: simpleScopeOptions(),
|
|
23638
23802
|
targets: [{ id: "project", label, mentionEligible }],
|
|
23639
23803
|
groups: [],
|
|
23640
23804
|
competitorAvailability: { state: "unavailable", reason: "frozen-competitor-identity-missing" }
|
|
@@ -23734,6 +23898,7 @@ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
|
|
|
23734
23898
|
completedAt: run.finishedAt,
|
|
23735
23899
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23736
23900
|
probe: false,
|
|
23901
|
+
scoped: run.measurementScope !== null,
|
|
23737
23902
|
definition: {
|
|
23738
23903
|
...simpleScope(definition.identity.displayName, matcher.keys.size > 0),
|
|
23739
23904
|
provenance: { kind: "frozen-simple", definitionRevision: null },
|
|
@@ -23772,6 +23937,7 @@ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
|
|
|
23772
23937
|
completedAt: run.finishedAt,
|
|
23773
23938
|
state: run.status === RunStatuses.partial ? "partial" : "measured",
|
|
23774
23939
|
probe: false,
|
|
23940
|
+
scoped: run.measurementScope !== null,
|
|
23775
23941
|
definition: {
|
|
23776
23942
|
...simpleScope(project.displayName, true),
|
|
23777
23943
|
slots,
|
|
@@ -23874,7 +24040,7 @@ function completedVisibilityRuns(db, projectId, planless, query) {
|
|
|
23874
24040
|
eq24(runs.projectId, projectId),
|
|
23875
24041
|
eq24(runs.kind, RunKinds["answer-visibility"]),
|
|
23876
24042
|
inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
23877
|
-
|
|
24043
|
+
notProbeRun(),
|
|
23878
24044
|
planless ? isNull4(runs.measurementPlanVersionId) : isNotNull(runs.measurementPlanVersionId),
|
|
23879
24045
|
// A scoped spot check is never a whole-project sweep. It can be inspected
|
|
23880
24046
|
// by its exact id, but must not win the default latest-result selection.
|
|
@@ -23884,6 +24050,27 @@ function completedVisibilityRuns(db, projectId, planless, query) {
|
|
|
23884
24050
|
query.to === void 0 ? void 0 : lte4(runs.createdAt, query.to)
|
|
23885
24051
|
)).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(VISIBILITY_REPORT_MAX_RUNS).all();
|
|
23886
24052
|
}
|
|
24053
|
+
function previousEligibleVisibilityRun(db, projectId, planless, selected) {
|
|
24054
|
+
return db.select().from(runs).where(and19(
|
|
24055
|
+
eq24(runs.projectId, projectId),
|
|
24056
|
+
eq24(runs.kind, RunKinds["answer-visibility"]),
|
|
24057
|
+
inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24058
|
+
notProbeRun(),
|
|
24059
|
+
planless ? isNull4(runs.measurementPlanVersionId) : isNotNull(runs.measurementPlanVersionId),
|
|
24060
|
+
isNull4(runs.measurementScope),
|
|
24061
|
+
or4(
|
|
24062
|
+
lt4(runs.createdAt, selected.createdAt),
|
|
24063
|
+
and19(eq24(runs.createdAt, selected.createdAt), lt4(runs.id, selected.id))
|
|
24064
|
+
)
|
|
24065
|
+
)).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(1).get();
|
|
24066
|
+
}
|
|
24067
|
+
function readPreviousSweep(read) {
|
|
24068
|
+
try {
|
|
24069
|
+
return read();
|
|
24070
|
+
} catch {
|
|
24071
|
+
return { unreadable: true };
|
|
24072
|
+
}
|
|
24073
|
+
}
|
|
23887
24074
|
function parseV2Versions(rows) {
|
|
23888
24075
|
const versions = /* @__PURE__ */ new Map();
|
|
23889
24076
|
for (const row of rows) {
|
|
@@ -23924,7 +24111,7 @@ function pendingAssignments(activePlan, measuredPlan, measuredDefinition) {
|
|
|
23924
24111
|
const measuredSignatures = new Set(measuredPlan.assignments.filter((assignment) => measuredEdges.has([assignment.executionNodeKey, assignment.targetKey, assignment.queryId].join("\0"))).map((assignment) => assignmentSignature(measuredPlan, assignment)));
|
|
23925
24112
|
return activePlan.assignments.filter((assignment) => !measuredSignatures.has(assignmentSignature(activePlan, assignment))).length;
|
|
23926
24113
|
}
|
|
23927
|
-
function advancedReaderInput(db, projectId, active, query) {
|
|
24114
|
+
function advancedReaderInput(db, projectId, active, query, includeComparison) {
|
|
23928
24115
|
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) throw new Error("Expected a v2 active plan");
|
|
23929
24116
|
const allVersionRows = db.select().from(measurementPlanVersions).where(eq24(measurementPlanVersions.projectId, projectId)).all();
|
|
23930
24117
|
const allVersionRowsById = new Map(allVersionRows.map((row) => [row.id, row]));
|
|
@@ -23954,7 +24141,7 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23954
24141
|
const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
|
|
23955
24142
|
const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
|
|
23956
24143
|
const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
|
|
23957
|
-
const
|
|
24144
|
+
const materialize = (candidate, includeEvidence) => {
|
|
23958
24145
|
const usePresentation = presentationComparableIds.has(candidate.source.row.id);
|
|
23959
24146
|
const version = usePresentation ? presentationVersion : candidate.source.row;
|
|
23960
24147
|
const plan = usePresentation ? presentationPlan : candidate.source.plan;
|
|
@@ -23965,10 +24152,25 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23965
24152
|
plan,
|
|
23966
24153
|
snapshots,
|
|
23967
24154
|
usePresentation ? [...presentationComparableIds] : comparableVersionIds(allVersionRowsById, candidate.source.row.id),
|
|
23968
|
-
|
|
24155
|
+
includeEvidence
|
|
23969
24156
|
);
|
|
23970
|
-
}
|
|
24157
|
+
};
|
|
24158
|
+
const candidates = sourceCandidates.map((candidate) => materialize(candidate, candidate.run.id === selectedSource?.run.id));
|
|
23971
24159
|
const selectedDefinition = candidates.find((candidate) => candidate.id === selectedSource?.run.id)?.definition;
|
|
24160
|
+
const previousInput = () => {
|
|
24161
|
+
if (selectedSource === void 0 || selectedSource.run.measurementScope !== null) return null;
|
|
24162
|
+
return readPreviousSweep(() => {
|
|
24163
|
+
const predecessor = previousEligibleVisibilityRun(db, projectId, false, selectedSource.run);
|
|
24164
|
+
if (predecessor === void 0) return null;
|
|
24165
|
+
const loaded = candidates.find((candidate) => candidate.id === predecessor.id);
|
|
24166
|
+
if (loaded !== void 0) return { run: loaded };
|
|
24167
|
+
const source = predecessor.measurementPlanVersionId === null ? void 0 : versions.get(predecessor.measurementPlanVersionId);
|
|
24168
|
+
if (source === void 0) {
|
|
24169
|
+
return { incomparable: { id: predecessor.id, createdAt: predecessor.createdAt, completedAt: predecessor.finishedAt } };
|
|
24170
|
+
}
|
|
24171
|
+
return { run: materialize({ run: predecessor, source }, false) };
|
|
24172
|
+
});
|
|
24173
|
+
};
|
|
23972
24174
|
return {
|
|
23973
24175
|
mode: "advanced",
|
|
23974
24176
|
activeRevision: active.version.revision,
|
|
@@ -23976,35 +24178,52 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23976
24178
|
selection: query,
|
|
23977
24179
|
activeDefinition: activeV2Definition(presentationPlan, presentationVersion.revision),
|
|
23978
24180
|
...preferredSource === void 0 ? {} : { preferredRunId: preferredSource.run.id },
|
|
23979
|
-
runs: candidates
|
|
24181
|
+
runs: candidates,
|
|
24182
|
+
...includeComparison ? { previous: previousInput() } : {}
|
|
23980
24183
|
};
|
|
23981
24184
|
}
|
|
23982
|
-
function
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
eq24(simpleMeasurementDefinitions.projectId,
|
|
23986
|
-
inArray9(simpleMeasurementDefinitions.runId,
|
|
24185
|
+
function frozenSimpleDefinitions(db, projectId, runIds) {
|
|
24186
|
+
if (runIds.length === 0) return /* @__PURE__ */ new Map();
|
|
24187
|
+
return new Map(db.select().from(simpleMeasurementDefinitions).where(and19(
|
|
24188
|
+
eq24(simpleMeasurementDefinitions.projectId, projectId),
|
|
24189
|
+
inArray9(simpleMeasurementDefinitions.runId, [...runIds])
|
|
23987
24190
|
)).all().map((row) => [row.runId, row.definition]));
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
23992
|
-
|
|
23993
|
-
|
|
23994
|
-
|
|
24191
|
+
}
|
|
24192
|
+
function simpleRunInput(db, project, run, definition, includeEvidence) {
|
|
24193
|
+
const snapshots = loadVisibilitySnapshots(db, [run.id]).get(run.id) ?? [];
|
|
24194
|
+
return definition ? frozenSimpleRun(run, definition, snapshots, includeEvidence) : legacySimpleRun(project, run, snapshots, includeEvidence);
|
|
24195
|
+
}
|
|
24196
|
+
function simpleReaderInput(db, project, query, includeComparison) {
|
|
24197
|
+
const sourceRuns = completedVisibilityRuns(db, project.id, true, query);
|
|
24198
|
+
const frozen = frozenSimpleDefinitions(db, project.id, sourceRuns.map((run) => run.id));
|
|
24199
|
+
const selectedRun = sourceRuns.at(0);
|
|
24200
|
+
const candidates = sourceRuns.map((run) => simpleRunInput(db, project, run, frozen.get(run.id), run.id === selectedRun?.id));
|
|
23995
24201
|
if (query.runId !== void 0 && !candidates.some((run) => run.id === query.runId)) {
|
|
23996
24202
|
throw validationError(`Measurement run "${query.runId}" is not an eligible simple result.`);
|
|
23997
24203
|
}
|
|
24204
|
+
const previousInput = () => {
|
|
24205
|
+
if (selectedRun === void 0 || selectedRun.measurementScope !== null) return null;
|
|
24206
|
+
return readPreviousSweep(() => {
|
|
24207
|
+
const predecessor = previousEligibleVisibilityRun(db, project.id, true, selectedRun);
|
|
24208
|
+
if (predecessor === void 0) return null;
|
|
24209
|
+
const loaded = candidates.find((candidate) => candidate.id === predecessor.id);
|
|
24210
|
+
if (loaded !== void 0) return { run: loaded };
|
|
24211
|
+
const definition = frozenSimpleDefinitions(db, project.id, [predecessor.id]).get(predecessor.id);
|
|
24212
|
+
return { run: simpleRunInput(db, project, predecessor, definition, false) };
|
|
24213
|
+
});
|
|
24214
|
+
};
|
|
23998
24215
|
return {
|
|
23999
24216
|
mode: "simple",
|
|
24000
24217
|
activeRevision: null,
|
|
24001
24218
|
pendingAssignmentCount: 0,
|
|
24002
24219
|
selection: query,
|
|
24003
24220
|
activeDefinition: simpleActiveDefinition(project),
|
|
24004
|
-
runs: candidates
|
|
24221
|
+
runs: candidates,
|
|
24222
|
+
...includeComparison ? { previous: previousInput() } : {}
|
|
24005
24223
|
};
|
|
24006
24224
|
}
|
|
24007
|
-
function readVisibilityReport(db, project, rawQuery) {
|
|
24225
|
+
function readVisibilityReport(db, project, rawQuery, options = {}) {
|
|
24226
|
+
const includeComparison = options.includeComparison ?? true;
|
|
24008
24227
|
const query = parseQuery(rawQuery);
|
|
24009
24228
|
const active = activeMeasurementPlan(db, project.id);
|
|
24010
24229
|
const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
|
|
@@ -24012,13 +24231,12 @@ function readVisibilityReport(db, project, rawQuery) {
|
|
|
24012
24231
|
if (mode === "advanced") {
|
|
24013
24232
|
if (active === null) throw validationError("This project has no advanced measurement plan.");
|
|
24014
24233
|
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) return unsupportedAdvancedResponse(query, active);
|
|
24015
|
-
return buildVisibilityReport(advancedReaderInput(db, project.id, active, query));
|
|
24234
|
+
return buildVisibilityReport(advancedReaderInput(db, project.id, active, query, includeComparison));
|
|
24016
24235
|
}
|
|
24017
|
-
return buildVisibilityReport(simpleReaderInput(db, project, query));
|
|
24236
|
+
return buildVisibilityReport(simpleReaderInput(db, project, query, includeComparison));
|
|
24018
24237
|
} catch (error) {
|
|
24019
|
-
if (error instanceof
|
|
24020
|
-
|
|
24021
|
-
}
|
|
24238
|
+
if (error instanceof VisibilityReportScopeError) throw validationError(error.message, error.details);
|
|
24239
|
+
if (error instanceof VisibilityReportCursorError) throw validationError(error.message);
|
|
24022
24240
|
throw error;
|
|
24023
24241
|
}
|
|
24024
24242
|
}
|
|
@@ -24031,7 +24249,7 @@ async function visibilityReportRoutes(app) {
|
|
|
24031
24249
|
|
|
24032
24250
|
// ../api-routes/src/query-tracking.ts
|
|
24033
24251
|
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
|
|
24252
|
+
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
24253
|
var SOURCE_LIMIT = 100;
|
|
24036
24254
|
var MAX_REVIEW_AGE_MS = 15 * 60 * 1e3;
|
|
24037
24255
|
var MAX_REVIEW_FUTURE_SKEW_MS = 60 * 1e3;
|
|
@@ -24214,7 +24432,7 @@ function simpleMeasuredAtByQuery(db, project, queryRows2, opts) {
|
|
|
24214
24432
|
eq25(runs.projectId, project.id),
|
|
24215
24433
|
eq25(runs.kind, RunKinds["answer-visibility"]),
|
|
24216
24434
|
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24217
|
-
|
|
24435
|
+
ne3(runs.trigger, RunTriggers.probe),
|
|
24218
24436
|
isNull5(runs.measurementScope),
|
|
24219
24437
|
isNull5(runs.measurementPlanVersionId)
|
|
24220
24438
|
)).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(SOURCE_LIMIT).all();
|
|
@@ -24336,7 +24554,7 @@ function measuredAtByQuery(db, state, opts) {
|
|
|
24336
24554
|
eq25(runs.projectId, project.id),
|
|
24337
24555
|
eq25(runs.kind, RunKinds["answer-visibility"]),
|
|
24338
24556
|
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
24339
|
-
|
|
24557
|
+
ne3(runs.trigger, RunTriggers.probe),
|
|
24340
24558
|
isNull5(runs.measurementScope),
|
|
24341
24559
|
isNotNull2(runs.measurementPlanVersionId)
|
|
24342
24560
|
)).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(SOURCE_LIMIT).all();
|
|
@@ -24553,6 +24771,8 @@ function workspaceDto(db, state, opts) {
|
|
|
24553
24771
|
...group.parentGroupKey ? { parentGroupKey: group.parentGroupKey } : {}
|
|
24554
24772
|
})) ?? [],
|
|
24555
24773
|
markets: plan?.reportingScopes?.map((scope) => ({ stableKey: scope.stableKey, label: scope.label, usageEdges: scope.usageEdges, ...scope.groupKey ? { groupKey: scope.groupKey } : {} })) ?? [],
|
|
24774
|
+
// Tracked assignments have no market intersection, so Groups and Properties carry no market links.
|
|
24775
|
+
scopeOptions: plan ? planScopeOptions(plan, { marketLinks: false }) : simpleScopeOptions(),
|
|
24556
24776
|
tracked: trackedRows(db, state, rows, plan, opts),
|
|
24557
24777
|
savedSources: savedSources(db, state.project.id)
|
|
24558
24778
|
});
|
|
@@ -26184,7 +26404,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
26184
26404
|
}
|
|
26185
26405
|
|
|
26186
26406
|
// ../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
|
|
26407
|
+
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
26408
|
|
|
26189
26409
|
// ../api-routes/src/notification-redaction.ts
|
|
26190
26410
|
var REDACTED_URL = {
|
|
@@ -26263,7 +26483,7 @@ async function historyRoutes(app) {
|
|
|
26263
26483
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
26264
26484
|
const project = resolveProject(app.db, request.params.name);
|
|
26265
26485
|
const filters = [eq27(auditLog.projectId, project.id)];
|
|
26266
|
-
if (request.operatorAccess !== true) filters.push(
|
|
26486
|
+
if (request.operatorAccess !== true) filters.push(ne4(auditLog.entityType, "telemetry"));
|
|
26267
26487
|
addAuditHistoryFilters(filters, request.query);
|
|
26268
26488
|
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
26489
|
return reply.send(rows.map(formatAuditEntry));
|
|
@@ -26271,7 +26491,7 @@ async function historyRoutes(app) {
|
|
|
26271
26491
|
app.get("/history", async (request, reply) => {
|
|
26272
26492
|
const scopedProjectId = request.apiKey?.projectId;
|
|
26273
26493
|
const filters = scopedProjectId ? [eq27(auditLog.projectId, scopedProjectId)] : [];
|
|
26274
|
-
if (request.operatorAccess !== true) filters.push(
|
|
26494
|
+
if (request.operatorAccess !== true) filters.push(ne4(auditLog.entityType, "telemetry"));
|
|
26275
26495
|
addAuditHistoryFilters(filters, request.query);
|
|
26276
26496
|
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
26497
|
return reply.send(rows.map(formatAuditEntry));
|
|
@@ -26572,7 +26792,7 @@ function formatAuditEntry(row) {
|
|
|
26572
26792
|
function addAuditHistoryFilters(filters, query) {
|
|
26573
26793
|
if (query.since && !Number.isNaN(Date.parse(query.since))) filters.push(gte5(auditLog.createdAt, query.since));
|
|
26574
26794
|
if (query.action) filters.push(eq27(auditLog.action, query.action));
|
|
26575
|
-
if (query.actor === "api") filters.push(
|
|
26795
|
+
if (query.actor === "api") filters.push(or5(
|
|
26576
26796
|
eq27(auditLog.actor, "api"),
|
|
26577
26797
|
like(auditLog.actor, "api-key:%"),
|
|
26578
26798
|
like(auditLog.actor, "user:%")
|
|
@@ -26593,7 +26813,7 @@ function parseOptionalPositiveInt(value, max2) {
|
|
|
26593
26813
|
}
|
|
26594
26814
|
|
|
26595
26815
|
// ../api-routes/src/analytics.ts
|
|
26596
|
-
import { and as and23, desc as desc10, eq as eq28, gte as gte6, inArray as inArray12, lt as
|
|
26816
|
+
import { and as and23, desc as desc10, eq as eq28, gte as gte6, inArray as inArray12, lt as lt5 } from "drizzle-orm";
|
|
26597
26817
|
|
|
26598
26818
|
// ../api-routes/src/analytics-model-attribution.ts
|
|
26599
26819
|
function logicalSweeps(observations) {
|
|
@@ -26785,7 +27005,7 @@ async function analyticsRoutes(app) {
|
|
|
26785
27005
|
eq28(runs.kind, RunKinds["answer-visibility"]),
|
|
26786
27006
|
inArray12(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
26787
27007
|
notProbeRun(),
|
|
26788
|
-
|
|
27008
|
+
lt5(runs.createdAt, cutoff)
|
|
26789
27009
|
);
|
|
26790
27010
|
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
27011
|
const anchorScanTruncated = anchorSweepTimes.length > ANCHOR_SWEEP_SCAN_LIMIT;
|
|
@@ -27619,7 +27839,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
27619
27839
|
}
|
|
27620
27840
|
|
|
27621
27841
|
// ../api-routes/src/report.ts
|
|
27622
|
-
import { and as and29, desc as desc14, eq as eq33, gte as gte8, inArray as inArray15, lt as
|
|
27842
|
+
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
27843
|
|
|
27624
27844
|
// ../api-routes/src/content.ts
|
|
27625
27845
|
import crypto23 from "crypto";
|
|
@@ -28590,141 +28810,22 @@ function escapeHtml2(value) {
|
|
|
28590
28810
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
28591
28811
|
}
|
|
28592
28812
|
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;
|
|
28813
|
+
return escapeHtml2(safeLinkHref(value));
|
|
28625
28814
|
}
|
|
28626
28815
|
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` : ""}`;
|
|
28816
|
+
const page3 = describeLandingPage(raw);
|
|
28817
|
+
const pathHtml = `<span class="page-path">${escapeHtml2(page3.path)}</span>`;
|
|
28818
|
+
if (!page3.querySummary) return pathHtml;
|
|
28819
|
+
return `${pathHtml}<span class="page-query" title="${escapeHtml2(page3.raw)}">${escapeHtml2(page3.querySummary)}</span>`;
|
|
28701
28820
|
}
|
|
28702
28821
|
function renderProofChips(items, limit = 3) {
|
|
28703
28822
|
if (items.length === 0) return "";
|
|
28704
28823
|
const visible = items.slice(0, limit);
|
|
28705
28824
|
const more = items.length - visible.length;
|
|
28706
28825
|
const chips = visible.map((item2) => `<span class="proof-chip">${escapeHtml2(item2)}</span>`);
|
|
28707
|
-
if (more > 0) chips.push(`<span class="proof-chip"
|
|
28826
|
+
if (more > 0) chips.push(`<span class="proof-chip">${reportMoreChipLabel(more)}</span>`);
|
|
28708
28827
|
return `<div class="proof-chips">${chips.join("")}</div>`;
|
|
28709
28828
|
}
|
|
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
28829
|
var STYLE = `
|
|
28729
28830
|
:root {
|
|
28730
28831
|
color-scheme: dark;
|
|
@@ -28942,6 +29043,7 @@ section.report-section .section-intro {
|
|
|
28942
29043
|
.source-bar-pct { color: ${COLORS.textFaint}; font-size: 11px; }
|
|
28943
29044
|
.driver-list { margin: 0; padding-left: 16px; font-size: 12px; color: ${COLORS.textMuted}; }
|
|
28944
29045
|
.driver-list li { margin: 2px 0; }
|
|
29046
|
+
.table-scroll { overflow-x: auto; }
|
|
28945
29047
|
table.report-table {
|
|
28946
29048
|
width: 100%;
|
|
28947
29049
|
border-collapse: collapse;
|
|
@@ -28964,7 +29066,7 @@ table.report-table th {
|
|
|
28964
29066
|
}
|
|
28965
29067
|
table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
28966
29068
|
table.report-table td.page-cell { max-width: 0; }
|
|
28967
|
-
table.insights-table { table-layout: fixed; }
|
|
29069
|
+
table.insights-table { table-layout: fixed; min-width: 680px; }
|
|
28968
29070
|
table.insights-table th.col-severity, table.insights-table td.col-severity { width: 96px; }
|
|
28969
29071
|
table.insights-table th.col-query, table.insights-table td.col-query { width: 18%; }
|
|
28970
29072
|
table.insights-table th.col-provider, table.insights-table td.col-provider { width: 88px; }
|
|
@@ -28995,6 +29097,7 @@ table.report-table td .badge {
|
|
|
28995
29097
|
font-weight: 600;
|
|
28996
29098
|
border: 1px solid;
|
|
28997
29099
|
}
|
|
29100
|
+
table.report-table td p.muted { margin: 2px 0 0; font-size: 12px; color: ${COLORS.textMuted}; }
|
|
28998
29101
|
.cell-cited { color: ${COLORS.positive}; font-weight: 600; }
|
|
28999
29102
|
.cell-not-cited { color: ${COLORS.textFaint}; }
|
|
29000
29103
|
.cell-pending { color: ${COLORS.textFaint}; font-style: italic; }
|
|
@@ -29020,7 +29123,7 @@ table.report-table td .badge {
|
|
|
29020
29123
|
}
|
|
29021
29124
|
.chart-grid {
|
|
29022
29125
|
display: grid;
|
|
29023
|
-
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
29126
|
+
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
|
29024
29127
|
gap: 16px;
|
|
29025
29128
|
}
|
|
29026
29129
|
.mention-branded-block {
|
|
@@ -29374,7 +29477,7 @@ table.report-table td .badge {
|
|
|
29374
29477
|
.client-progress-fill.tone-negative { background: ${COLORS.negative}b3; }
|
|
29375
29478
|
.client-evidence-grid {
|
|
29376
29479
|
display: grid;
|
|
29377
|
-
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
|
29480
|
+
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
|
29378
29481
|
gap: 16px;
|
|
29379
29482
|
}
|
|
29380
29483
|
.client-opportunity-list {
|
|
@@ -29468,6 +29571,7 @@ table.report-table td .badge {
|
|
|
29468
29571
|
print-color-adjust: exact;
|
|
29469
29572
|
}
|
|
29470
29573
|
.container { max-width: none; padding: 0; }
|
|
29574
|
+
.table-scroll { overflow: visible; }
|
|
29471
29575
|
section.report-section,
|
|
29472
29576
|
.executive-hero,
|
|
29473
29577
|
.headline-card,
|
|
@@ -29499,46 +29603,34 @@ function section(opts, body) {
|
|
|
29499
29603
|
function renderEmpty(message) {
|
|
29500
29604
|
return `<div class="empty-state">${escapeHtml2(message)}</div>`;
|
|
29501
29605
|
}
|
|
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
29606
|
function renderHeaderLocationFragment(location) {
|
|
29508
|
-
|
|
29509
|
-
return ` \xB7 Market: ${escapeHtml2(locationDisplay(location))}`;
|
|
29607
|
+
return ` \xB7 ${escapeHtml2(reportHeaderMarketLabel(location))}`;
|
|
29510
29608
|
}
|
|
29511
29609
|
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.
|
|
29610
|
+
const scope = reportMarketScope(report);
|
|
29611
|
+
if (!scope) return "";
|
|
29612
|
+
const copy = REPORT_SECTION_COPY["executive-summary"].marketScope;
|
|
29613
|
+
const warning = scope.weakProviders !== null ? `<div class="scope-warning">
|
|
29614
|
+
<strong>${copy.warningTitle}</strong>
|
|
29615
|
+
${escapeHtml2(scope.weakProviders)} ${copy.warningDetail}
|
|
29524
29616
|
</div>` : "";
|
|
29525
29617
|
return `<div class="chart-card market-scope-card">
|
|
29526
|
-
<h3
|
|
29618
|
+
<h3>${copy.heading}</h3>
|
|
29527
29619
|
<div class="market-scope-grid">
|
|
29528
29620
|
<div class="scope-tile">
|
|
29529
|
-
<div class="scope-label"
|
|
29530
|
-
<div class="scope-value">${escapeHtml2(
|
|
29531
|
-
<div class="scope-copy"
|
|
29621
|
+
<div class="scope-label">${copy.currentLabel}</div>
|
|
29622
|
+
<div class="scope-value">${escapeHtml2(scope.currentValue)}</div>
|
|
29623
|
+
<div class="scope-copy">${copy.currentCopy}</div>
|
|
29532
29624
|
</div>
|
|
29533
29625
|
<div class="scope-tile">
|
|
29534
|
-
<div class="scope-label"
|
|
29535
|
-
<div class="scope-value">${escapeHtml2(
|
|
29536
|
-
<div class="scope-copy">${escapeHtml2(
|
|
29626
|
+
<div class="scope-label">${copy.notIncludedLabel}</div>
|
|
29627
|
+
<div class="scope-value">${escapeHtml2(scope.notIncludedValue)}</div>
|
|
29628
|
+
<div class="scope-copy">${escapeHtml2(scope.notIncludedCopy)}</div>
|
|
29537
29629
|
</div>
|
|
29538
29630
|
<div class="scope-tile">
|
|
29539
|
-
<div class="scope-label"
|
|
29540
|
-
<div class="scope-value">${
|
|
29541
|
-
<div class="scope-copy">${escapeHtml2(providerCopy)}</div>
|
|
29631
|
+
<div class="scope-label">${copy.providerLabel}</div>
|
|
29632
|
+
<div class="scope-value">${scope.providerValue}</div>
|
|
29633
|
+
<div class="scope-copy">${escapeHtml2(scope.providerCopy)}</div>
|
|
29542
29634
|
</div>
|
|
29543
29635
|
</div>
|
|
29544
29636
|
${warning}
|
|
@@ -29546,71 +29638,65 @@ function renderLocationCard(report) {
|
|
|
29546
29638
|
}
|
|
29547
29639
|
function renderExecutiveSummary(report) {
|
|
29548
29640
|
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;
|
|
29641
|
+
const copy = REPORT_SECTION_COPY["executive-summary"];
|
|
29642
|
+
const headline = reportExecutiveHeadline(report);
|
|
29558
29643
|
const heroHtml = `<div class="executive-hero">
|
|
29559
29644
|
<div class="headline-card">
|
|
29560
29645
|
<div>
|
|
29561
|
-
<div class="hero-kicker"
|
|
29562
|
-
<div class="hero-title">${escapeHtml2(
|
|
29646
|
+
<div class="hero-kicker">${copy.heroKicker}</div>
|
|
29647
|
+
<div class="hero-title">${escapeHtml2(headline.title)}</div>
|
|
29563
29648
|
</div>
|
|
29564
|
-
<div class="hero-subtitle">${escapeHtml2(
|
|
29649
|
+
<div class="hero-subtitle">${escapeHtml2(headline.subtitle)}</div>
|
|
29565
29650
|
</div>
|
|
29566
29651
|
<div class="hero-proof-grid">
|
|
29567
29652
|
<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>
|
|
29653
|
+
<div class="mini-label">${copy.proofTiles.citationTrend}</div>
|
|
29654
|
+
<div class="mini-value tone-${headline.trendTone}">${escapeHtml2(headline.trendLabel)}</div>
|
|
29655
|
+
<div class="mini-copy">${escapeHtml2(headline.citedFragment)}</div>
|
|
29571
29656
|
</div>
|
|
29572
29657
|
<div class="hero-proof">
|
|
29573
|
-
<div class="mini-label"
|
|
29658
|
+
<div class="mini-label">${copy.proofTiles.mentionCoverage}</div>
|
|
29574
29659
|
<div class="mini-value">${s.mentionRate}%</div>
|
|
29575
|
-
<div class="mini-copy">${escapeHtml2(mentionedFragment)}</div>
|
|
29660
|
+
<div class="mini-copy">${escapeHtml2(headline.mentionedFragment)}</div>
|
|
29576
29661
|
</div>
|
|
29577
29662
|
<div class="hero-proof">
|
|
29578
|
-
<div class="mini-label"
|
|
29579
|
-
<div class="mini-value">${formatNumber(
|
|
29580
|
-
<div class="mini-copy"
|
|
29663
|
+
<div class="mini-label">${copy.proofTiles.prioritizedActions}</div>
|
|
29664
|
+
<div class="mini-value">${formatNumber(headline.prioritizedActionCount)}</div>
|
|
29665
|
+
<div class="mini-copy">${copy.prioritizedActionsCopy}</div>
|
|
29581
29666
|
</div>
|
|
29582
29667
|
</div>
|
|
29583
29668
|
</div>`;
|
|
29584
29669
|
const metrics = [
|
|
29585
29670
|
{
|
|
29586
|
-
label:
|
|
29671
|
+
label: copy.tiles.citationRate,
|
|
29587
29672
|
value: `${s.citationRate}%`,
|
|
29588
|
-
delta: `<span class="tone-${trendTone}">${trendLabel}</span> \xB7 ${citedFragment} \xB7 ${
|
|
29673
|
+
delta: `<span class="tone-${headline.trendTone}">${headline.trendLabel}</span> \xB7 ${headline.citedFragment} \xB7 ${headline.providerCountLabel}`
|
|
29589
29674
|
},
|
|
29590
29675
|
{
|
|
29591
|
-
label:
|
|
29676
|
+
label: copy.tiles.mentionRate,
|
|
29592
29677
|
value: `${s.mentionRate}%`,
|
|
29593
|
-
delta: mentionedFragment
|
|
29678
|
+
delta: headline.mentionedFragment
|
|
29594
29679
|
},
|
|
29595
29680
|
{
|
|
29596
|
-
label:
|
|
29681
|
+
label: copy.tiles.queriesTracked,
|
|
29597
29682
|
value: formatNumber(s.queryCount),
|
|
29598
|
-
delta:
|
|
29683
|
+
delta: headline.competitorCountLabel
|
|
29599
29684
|
}
|
|
29600
29685
|
];
|
|
29601
|
-
if (s.gsc) {
|
|
29602
|
-
const dateRange = gscDateRange(report);
|
|
29686
|
+
if (s.gsc && headline.gscDelta !== null) {
|
|
29603
29687
|
metrics.push({
|
|
29604
|
-
label:
|
|
29688
|
+
label: copy.tiles.gscClicks,
|
|
29605
29689
|
value: formatNumber(s.gsc.clicks),
|
|
29606
|
-
|
|
29690
|
+
// Only the date range can hold markup; the counts and separators cannot,
|
|
29691
|
+
// so escaping the whole line matches escaping the range alone.
|
|
29692
|
+
delta: escapeHtml2(headline.gscDelta)
|
|
29607
29693
|
});
|
|
29608
29694
|
}
|
|
29609
|
-
if (s.ga) {
|
|
29695
|
+
if (s.ga && headline.gaDelta !== null) {
|
|
29610
29696
|
metrics.push({
|
|
29611
|
-
label:
|
|
29697
|
+
label: copy.tiles.gaSessions,
|
|
29612
29698
|
value: formatNumber(s.ga.sessions),
|
|
29613
|
-
delta:
|
|
29699
|
+
delta: headline.gaDelta
|
|
29614
29700
|
});
|
|
29615
29701
|
}
|
|
29616
29702
|
const metricsHtml = `<div class="metric-grid">
|
|
@@ -29628,44 +29714,38 @@ function renderExecutiveSummary(report) {
|
|
|
29628
29714
|
const locationHtml = renderLocationCard(report);
|
|
29629
29715
|
return section(
|
|
29630
29716
|
{
|
|
29631
|
-
id: "executive-summary",
|
|
29632
|
-
eyebrow:
|
|
29633
|
-
title:
|
|
29634
|
-
intro:
|
|
29717
|
+
id: ReportSectionIds["executive-summary"],
|
|
29718
|
+
eyebrow: copy.eyebrow,
|
|
29719
|
+
title: copy.title,
|
|
29720
|
+
intro: copy.intro
|
|
29635
29721
|
},
|
|
29636
29722
|
heroHtml + metricsHtml + findingsHtml + locationHtml
|
|
29637
29723
|
);
|
|
29638
29724
|
}
|
|
29639
29725
|
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";
|
|
29726
|
+
const tone = reportDirectionTone(direction);
|
|
29727
|
+
return tone === "neutral" ? "" : `tone-${tone}`;
|
|
29648
29728
|
}
|
|
29649
29729
|
function renderRateDeltaTile(label, delta, unit) {
|
|
29650
29730
|
if (!delta) {
|
|
29651
|
-
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta"
|
|
29731
|
+
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
29732
|
}
|
|
29653
29733
|
const valueSuffix = unit === "%" ? "%" : "";
|
|
29654
|
-
const deltaText =
|
|
29734
|
+
const deltaText = reportRateDeltaCopy(delta, unit);
|
|
29655
29735
|
return `<div class="metric">
|
|
29656
29736
|
<div class="label">${escapeHtml2(label)}</div>
|
|
29657
|
-
<div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${
|
|
29737
|
+
<div class="value ${deltaToneClass(delta.direction)}">${delta.current}${valueSuffix} <span style="font-size:14px;font-weight:500;">${reportDeltaArrow(delta.direction)}</span></div>
|
|
29658
29738
|
<div class="delta">${deltaText}</div>
|
|
29659
29739
|
</div>`;
|
|
29660
29740
|
}
|
|
29661
29741
|
function renderTrafficDeltaTile(label, delta, countLabel, comparisonWindowDays) {
|
|
29662
29742
|
if (!delta) {
|
|
29663
|
-
return `<div class="metric"><div class="label">${escapeHtml2(label)}</div><div class="value">\u2014</div><div class="delta"
|
|
29743
|
+
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
29744
|
}
|
|
29665
|
-
const deltaText = formatWindowCountDelta(delta, countLabel,
|
|
29745
|
+
const deltaText = formatWindowCountDelta(delta, countLabel, reportPriorWindowLabel(comparisonWindowDays));
|
|
29666
29746
|
return `<div class="metric">
|
|
29667
29747
|
<div class="label">${escapeHtml2(label)}</div>
|
|
29668
|
-
<div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${
|
|
29748
|
+
<div class="value ${deltaToneClass(delta.direction)}">${formatNumber(delta.current)} <span style="font-size:14px;font-weight:500;">${reportDeltaArrow(delta.direction)}</span></div>
|
|
29669
29749
|
<div class="delta">${deltaText}</div>
|
|
29670
29750
|
</div>`;
|
|
29671
29751
|
}
|
|
@@ -29673,24 +29753,21 @@ function renderProviderMovements(movements, audience) {
|
|
|
29673
29753
|
const meaningful = movements.filter((m) => m.direction !== "flat");
|
|
29674
29754
|
if (meaningful.length === 0) return "";
|
|
29675
29755
|
const isClient = audience === "client";
|
|
29756
|
+
const copy = isClient ? REPORT_SECTION_COPY["whats-changed"].client : REPORT_SECTION_COPY["whats-changed"].agency;
|
|
29676
29757
|
const rows = meaningful.map((m) => {
|
|
29677
|
-
const sign = m.deltaAbs > 0 ? "+" : "";
|
|
29678
29758
|
return `<tr>
|
|
29679
|
-
<td>${escapeHtml2(isClient ?
|
|
29759
|
+
<td>${escapeHtml2(isClient ? reportProviderDisplayName(m.provider) : m.provider)}</td>
|
|
29680
29760
|
<td class="numeric">${m.prior}%</td>
|
|
29681
29761
|
<td class="numeric">${m.current}%</td>
|
|
29682
|
-
<td class="numeric ${deltaToneClass(m.direction)}">${
|
|
29762
|
+
<td class="numeric ${deltaToneClass(m.direction)}">${reportMovementChangeCopy(m)}</td>
|
|
29683
29763
|
</tr>`;
|
|
29684
29764
|
}).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>
|
|
29765
|
+
const [engineHeader, priorHeader, currentHeader, changeHeader] = copy.movementHeaders;
|
|
29766
|
+
return `<div class="chart-card"><h3>${copy.movementsHeading}</h3>
|
|
29767
|
+
<div class="table-scroll"><table class="report-table">
|
|
29768
|
+
<thead><tr><th>${engineHeader}</th><th class="numeric">${priorHeader}</th><th class="numeric">${currentHeader}</th><th class="numeric">${changeHeader}</th></tr></thead>
|
|
29692
29769
|
<tbody>${rows}</tbody>
|
|
29693
|
-
</table>
|
|
29770
|
+
</table></div>
|
|
29694
29771
|
</div>`;
|
|
29695
29772
|
}
|
|
29696
29773
|
function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
@@ -29701,52 +29778,45 @@ function renderWinsLosses(insights2, heading, emptyMessage, audience) {
|
|
|
29701
29778
|
}
|
|
29702
29779
|
const isClient = audience === "client";
|
|
29703
29780
|
const rows = insights2.map((i) => {
|
|
29704
|
-
const tone =
|
|
29705
|
-
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral"
|
|
29781
|
+
const tone = reportInsightTone(i);
|
|
29782
|
+
const countChip = i.instanceCount > 1 ? ` <span class="badge tone-neutral">${reportInstanceCountLabel(i.instanceCount)}</span>` : "";
|
|
29706
29783
|
const severityCell = isClient ? "" : `<td><span class="badge tone-${tone}">${escapeHtml2(reportSeverityLabel(i.severity))}</span></td>`;
|
|
29707
29784
|
return `<tr>
|
|
29708
29785
|
${severityCell}
|
|
29709
29786
|
<td>${escapeHtml2(i.title)}${countChip}</td>
|
|
29710
29787
|
<td>${escapeHtml2(i.query)}</td>
|
|
29711
|
-
<td>${escapeHtml2(isClient ?
|
|
29788
|
+
<td>${escapeHtml2(isClient ? reportProviderDisplayName(i.provider) : i.provider)}</td>
|
|
29712
29789
|
</tr>`;
|
|
29713
29790
|
}).join("");
|
|
29714
|
-
const
|
|
29791
|
+
const headerCells = (isClient ? REPORT_SECTION_COPY["whats-changed"].client : REPORT_SECTION_COPY["whats-changed"].agency).insightHeaders;
|
|
29792
|
+
const headers = `<tr>${headerCells.map((header) => `<th>${header}</th>`).join("")}</tr>`;
|
|
29715
29793
|
return `<div class="chart-card"><h3>${escapeHtml2(heading)}</h3>
|
|
29716
|
-
<table class="report-table">
|
|
29794
|
+
<div class="table-scroll"><table class="report-table">
|
|
29717
29795
|
<thead>${headers}</thead>
|
|
29718
29796
|
<tbody>${rows}</tbody>
|
|
29719
|
-
</table>
|
|
29797
|
+
</table></div>
|
|
29720
29798
|
</div>`;
|
|
29721
29799
|
}
|
|
29722
29800
|
function renderWhatsChanged(report, audience) {
|
|
29723
29801
|
const w = report.whatsChanged;
|
|
29724
29802
|
const isClient = audience === "client";
|
|
29725
|
-
const
|
|
29726
|
-
const
|
|
29727
|
-
const
|
|
29803
|
+
const { client, agency } = REPORT_SECTION_COPY["whats-changed"];
|
|
29804
|
+
const copy = isClient ? client : agency;
|
|
29805
|
+
const heading = { id: ReportSectionIds["whats-changed"], eyebrow: copy.eyebrow, title: copy.title, intro: isClient ? "" : w.headline };
|
|
29728
29806
|
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
|
-
);
|
|
29807
|
+
return section(heading, renderEmpty(copy.empty));
|
|
29733
29808
|
}
|
|
29734
29809
|
const rateTiles = `<div class="metric-grid">
|
|
29735
|
-
${renderRateDeltaTile(isClient ?
|
|
29736
|
-
${renderRateDeltaTile(isClient ?
|
|
29737
|
-
${renderRateDeltaTile(isClient ?
|
|
29738
|
-
${renderTrafficDeltaTile(
|
|
29739
|
-
${renderTrafficDeltaTile(
|
|
29810
|
+
${renderRateDeltaTile(isClient ? client.tiles.mentionRate : agency.tiles.citationRate, isClient ? w.mentionRate : w.citationRate, "%")}
|
|
29811
|
+
${renderRateDeltaTile(isClient ? client.tiles.citationRate : agency.tiles.mentionRate, isClient ? w.citationRate : w.mentionRate, "%")}
|
|
29812
|
+
${renderRateDeltaTile(isClient ? client.tiles.mentionedQueryCount : agency.tiles.citedQueryCount, isClient ? w.mentionedQueryCount : w.citedQueryCount, "count")}
|
|
29813
|
+
${renderTrafficDeltaTile(copy.tiles.gscClicks, w.gscClicksDelta, copy.gscCountLabel, w.comparisonWindowDays)}
|
|
29814
|
+
${renderTrafficDeltaTile(copy.tiles.aiReferrals, w.aiReferralsDelta, copy.aiReferralsCountLabel, w.comparisonWindowDays)}
|
|
29740
29815
|
</div>`;
|
|
29741
29816
|
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
|
-
);
|
|
29817
|
+
const wins = renderWinsLosses(w.wins, copy.winsHeading, copy.winsEmpty, audience);
|
|
29818
|
+
const regressions = renderWinsLosses(w.regressions, copy.regressionsHeading, copy.regressionsEmpty, audience);
|
|
29819
|
+
return section(heading, `${rateTiles}${movements}${wins}${regressions}`);
|
|
29750
29820
|
}
|
|
29751
29821
|
function renderProviderBars(rates) {
|
|
29752
29822
|
if (rates.length === 0) return "";
|
|
@@ -29765,45 +29835,50 @@ function renderProviderBars(rates) {
|
|
|
29765
29835
|
<text x="${labelWidth - 8}" y="${y + 16}" fill="${COLORS.textMuted}" font-size="11" text-anchor="end">${escapeHtml2(r.provider)}</text>
|
|
29766
29836
|
<rect x="${labelWidth}" y="${y}" width="${barWidth}" height="${barHeight}" fill="${COLORS.border}" opacity="0.4" rx="3" />
|
|
29767
29837
|
<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">${
|
|
29838
|
+
<text x="${labelWidth + w + 6}" y="${y + 16}" fill="${COLORS.text}" font-size="11">${reportProviderRateLabel(r)}</text>`;
|
|
29769
29839
|
}).join("");
|
|
29840
|
+
const title = REPORT_SECTION_COPY["citation-scorecard"].providerChartTitle;
|
|
29770
29841
|
return `<div class="chart-card">
|
|
29771
|
-
<h3
|
|
29772
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="
|
|
29842
|
+
<h3>${title}</h3>
|
|
29843
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${reportBarChartLabel(title)}">
|
|
29773
29844
|
${bars}
|
|
29774
29845
|
</svg>
|
|
29775
29846
|
</div>`;
|
|
29776
29847
|
}
|
|
29777
29848
|
function renderCitationMatrix(scorecard) {
|
|
29849
|
+
const copy = REPORT_SECTION_COPY["citation-scorecard"];
|
|
29850
|
+
const glyphs = copy.glyphs;
|
|
29778
29851
|
if (scorecard.queries.length === 0 || scorecard.providers.length === 0) {
|
|
29779
|
-
return renderEmpty(
|
|
29852
|
+
return renderEmpty(copy.empty);
|
|
29780
29853
|
}
|
|
29781
29854
|
const headers = scorecard.providers.map((p) => `<th>${escapeHtml2(p)}</th>`).join("");
|
|
29782
29855
|
const rows = scorecard.queries.map((q, qi) => {
|
|
29783
29856
|
const cells = scorecard.providers.map((_, pi) => {
|
|
29784
29857
|
const cell = scorecard.matrix[qi]?.[pi];
|
|
29785
29858
|
if (!cell) {
|
|
29786
|
-
return
|
|
29859
|
+
return `<td><span class="cell-pending">${glyphs.missingCell}</span></td>`;
|
|
29787
29860
|
}
|
|
29788
|
-
const citedGlyph = cell.citationState === CitationStates.cited ?
|
|
29789
|
-
const mentionedGlyph = cell.answerMentioned === true ?
|
|
29861
|
+
const citedGlyph = cell.citationState === CitationStates.cited ? `<span class="cell-cited">${glyphs.cited}</span>` : `<span class="cell-not-cited">${glyphs.notCited}</span>`;
|
|
29862
|
+
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
29863
|
return `<td>${citedGlyph} ${mentionedGlyph}</td>`;
|
|
29791
29864
|
}).join("");
|
|
29792
29865
|
return `<tr><td>${escapeHtml2(q)}</td>${cells}</tr>`;
|
|
29793
29866
|
}).join("");
|
|
29794
|
-
const
|
|
29795
|
-
|
|
29796
|
-
|
|
29867
|
+
const legendCopy = copy.legend;
|
|
29868
|
+
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>`;
|
|
29869
|
+
return `${legend}<div class="table-scroll"><table class="report-table">
|
|
29870
|
+
<thead><tr><th>${copy.queryHeader}</th>${headers}</tr></thead>
|
|
29797
29871
|
<tbody>${rows}</tbody>
|
|
29798
|
-
</table>`;
|
|
29872
|
+
</table></div>`;
|
|
29799
29873
|
}
|
|
29800
29874
|
function renderCitationScorecard(report) {
|
|
29801
29875
|
const body = `
|
|
29802
29876
|
${renderProviderBars(report.citationScorecard.providerRates)}
|
|
29803
29877
|
${renderCitationMatrix(report.citationScorecard)}
|
|
29804
29878
|
`;
|
|
29879
|
+
const copy = REPORT_SECTION_COPY["citation-scorecard"];
|
|
29805
29880
|
return section(
|
|
29806
|
-
{ id: "citation-scorecard", eyebrow:
|
|
29881
|
+
{ id: ReportSectionIds["citation-scorecard"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
29807
29882
|
body
|
|
29808
29883
|
);
|
|
29809
29884
|
}
|
|
@@ -29835,19 +29910,16 @@ function renderCompetitorBars(landscape, canonical) {
|
|
|
29835
29910
|
{ label: canonical, count: landscape.projectCitationCount, isProject: true },
|
|
29836
29911
|
...landscape.competitors.map((c) => ({ label: c.domain, count: c.citationCount, isProject: false }))
|
|
29837
29912
|
];
|
|
29838
|
-
|
|
29913
|
+
const title = REPORT_SECTION_COPY["competitor-landscape"].citationsChartTitle;
|
|
29914
|
+
return renderLandscapeBars(data, title, reportBarChartLabel(title));
|
|
29839
29915
|
}
|
|
29840
29916
|
function renderMentionBars(section2, canonical, title) {
|
|
29841
29917
|
const data = [
|
|
29842
29918
|
{ label: canonical, count: section2.projectMentionCount, isProject: true },
|
|
29843
29919
|
...section2.competitors.map((c) => ({ label: c.domain, count: c.mentionCount, isProject: false }))
|
|
29844
29920
|
];
|
|
29845
|
-
return renderLandscapeBars(data, title,
|
|
29921
|
+
return renderLandscapeBars(data, title, reportBarChartLabel(title));
|
|
29846
29922
|
}
|
|
29847
|
-
var MENTION_SCOPE_LABEL = {
|
|
29848
|
-
"non-brand": "non-brand queries",
|
|
29849
|
-
pooled: "pooled queries \xB7 classification unavailable"
|
|
29850
|
-
};
|
|
29851
29923
|
function renderReportShareOfVoice(report) {
|
|
29852
29924
|
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
29925
|
}
|
|
@@ -29858,19 +29930,20 @@ function renderCompetitorLandscape(report) {
|
|
|
29858
29930
|
const brandedMentions = mentionLandscape.branded;
|
|
29859
29931
|
const hasBrandedMentions = (brandedMentions?.totalAnswerSnapshots ?? 0) > 0;
|
|
29860
29932
|
const noMentionData = mentionLandscape.competitors.length === 0 && mentionLandscape.projectMentionCount === 0 && !hasBrandedMentions;
|
|
29933
|
+
const copy = REPORT_SECTION_COPY["competitor-landscape"];
|
|
29861
29934
|
if (noCitationData && noMentionData) {
|
|
29862
29935
|
return section(
|
|
29863
|
-
{ id: "competitor-landscape", eyebrow:
|
|
29864
|
-
renderEmpty(
|
|
29936
|
+
{ id: ReportSectionIds["competitor-landscape"], eyebrow: copy.eyebrow, title: copy.title },
|
|
29937
|
+
renderEmpty(copy.empty)
|
|
29865
29938
|
);
|
|
29866
29939
|
}
|
|
29867
29940
|
const mentionByDomain = new Map(mentionLandscape.competitors.map((m) => [m.domain, m]));
|
|
29868
29941
|
const rows = competitors2.map((c) => {
|
|
29869
|
-
const tone =
|
|
29942
|
+
const tone = reportPressureTone(c.pressureLabel);
|
|
29870
29943
|
const mention = mentionByDomain.get(c.domain);
|
|
29871
29944
|
const mentionCount = mention?.mentionCount ?? 0;
|
|
29872
29945
|
const mentionTotal = mention?.totalCount ?? mentionLandscape.totalAnswerSnapshots;
|
|
29873
|
-
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length}
|
|
29946
|
+
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${reportCitedUrlCount(c.theirCitedPages.length)}</summary>
|
|
29874
29947
|
<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
29948
|
</details>` : "";
|
|
29876
29949
|
return `<tr>
|
|
@@ -29879,52 +29952,39 @@ function renderCompetitorLandscape(report) {
|
|
|
29879
29952
|
<td class="numeric">${c.citationCount} / ${c.totalCount}</td>
|
|
29880
29953
|
<td class="numeric">${mentionCount} / ${mentionTotal}</td>
|
|
29881
29954
|
<td class="numeric">${c.sharePct}%</td>
|
|
29882
|
-
<td>${escapeHtml2(c.citedQueries
|
|
29955
|
+
<td>${escapeHtml2(reportTruncatedList(c.citedQueries, 5))}${pagesDisclosure}</td>
|
|
29883
29956
|
</tr>`;
|
|
29884
29957
|
}).join("");
|
|
29885
|
-
const
|
|
29886
|
-
const
|
|
29887
|
-
const table = competitors2.length > 0 ? `<table class="report-table">
|
|
29888
|
-
<thead><tr><th
|
|
29958
|
+
const mentionCopy = reportCompetitorMentionCopy(mentionLandscape);
|
|
29959
|
+
const headers = copy.headers;
|
|
29960
|
+
const table = competitors2.length > 0 ? `<div class="table-scroll"><table class="report-table">
|
|
29961
|
+
<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
29962
|
<tbody>${rows}</tbody>
|
|
29890
|
-
</table>` : renderEmpty(
|
|
29963
|
+
</table></div>` : renderEmpty(copy.noCompetitors);
|
|
29891
29964
|
const citationBars = renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain);
|
|
29892
29965
|
const mentionBars = renderMentionBars(
|
|
29893
29966
|
mentionLandscape,
|
|
29894
29967
|
report.meta.project.canonicalDomain,
|
|
29895
|
-
|
|
29968
|
+
mentionCopy.mentionsChartTitle
|
|
29896
29969
|
);
|
|
29897
29970
|
const charts = citationBars && mentionBars ? `<div class="chart-grid">${citationBars}${mentionBars}</div>` : `${citationBars}${mentionBars}`;
|
|
29898
29971
|
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,
|
|
29972
|
+
const mentionShareNote = mentionShareUnavailable ? `<p class="chart-note">${escapeHtml2(mentionCopy.mentionShareUnavailable)}</p>` : "";
|
|
29973
|
+
const brandedBars = hasBrandedMentions && brandedMentions ? renderMentionBars(brandedMentions, report.meta.project.canonicalDomain, copy.brandedChartTitle) : "";
|
|
29901
29974
|
const brandedBlock = brandedBars ? `<div class="mention-branded-block">
|
|
29902
|
-
<p class="chart-note"
|
|
29975
|
+
<p class="chart-note">${mentionCopy.brandedNote}</p>
|
|
29903
29976
|
${brandedBars}
|
|
29904
29977
|
</div>` : "";
|
|
29905
29978
|
return section(
|
|
29906
29979
|
{
|
|
29907
|
-
id: "competitor-landscape",
|
|
29908
|
-
eyebrow:
|
|
29909
|
-
title:
|
|
29910
|
-
intro:
|
|
29980
|
+
id: ReportSectionIds["competitor-landscape"],
|
|
29981
|
+
eyebrow: copy.eyebrow,
|
|
29982
|
+
title: copy.title,
|
|
29983
|
+
intro: copy.intro
|
|
29911
29984
|
},
|
|
29912
29985
|
`${mentionShareNote}${charts}${table}${brandedBlock}`
|
|
29913
29986
|
);
|
|
29914
29987
|
}
|
|
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
29988
|
function renderCategoryBars(buckets) {
|
|
29929
29989
|
if (buckets.length === 0) return "";
|
|
29930
29990
|
const total = buckets.reduce((s, b) => s + b.count, 0);
|
|
@@ -29932,7 +29992,7 @@ function renderCategoryBars(buckets) {
|
|
|
29932
29992
|
const max2 = Math.max(...buckets.map((b) => b.count), 1);
|
|
29933
29993
|
const rows = buckets.map((b) => {
|
|
29934
29994
|
const pct = b.count / max2 * 100;
|
|
29935
|
-
const tone =
|
|
29995
|
+
const tone = reportSourceCategoryTone(b.category);
|
|
29936
29996
|
const color = tone === "negative" ? COLORS.negative : tone === "caution" ? COLORS.caution : COLORS.accent;
|
|
29937
29997
|
return `
|
|
29938
29998
|
<div class="source-bar-row">
|
|
@@ -29940,11 +30000,11 @@ function renderCategoryBars(buckets) {
|
|
|
29940
30000
|
<div class="source-bar-track">
|
|
29941
30001
|
<div class="source-bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div>
|
|
29942
30002
|
</div>
|
|
29943
|
-
<div class="source-bar-value">${b.count} <span class="source-bar-pct"
|
|
30003
|
+
<div class="source-bar-value">${b.count} <span class="source-bar-pct">${reportSourceCategoryShareLabel(b.sharePct)}</span></div>
|
|
29944
30004
|
</div>`;
|
|
29945
30005
|
}).join("");
|
|
29946
30006
|
return `<div class="chart-card">
|
|
29947
|
-
<h3
|
|
30007
|
+
<h3>${REPORT_SECTION_COPY["ai-source-origin"].categoriesHeading}</h3>
|
|
29948
30008
|
<div class="source-bars">${rows}</div>
|
|
29949
30009
|
</div>`;
|
|
29950
30010
|
}
|
|
@@ -29960,7 +30020,7 @@ function renderShareBars(heading, rows, countLabel) {
|
|
|
29960
30020
|
<div class="source-bar-track">
|
|
29961
30021
|
<div class="source-bar-fill" style="width:${pct.toFixed(1)}%;background:${color}"></div>
|
|
29962
30022
|
</div>
|
|
29963
|
-
<div class="source-bar-value">${formatNumber(r.count)} <span class="source-bar-pct">${escapeHtml2(countLabel
|
|
30023
|
+
<div class="source-bar-value">${formatNumber(r.count)} <span class="source-bar-pct">${escapeHtml2(reportShareBarShareLabel(countLabel, r.sharePct))}</span></div>
|
|
29964
30024
|
</div>`;
|
|
29965
30025
|
}).join("");
|
|
29966
30026
|
return `<div class="chart-card">
|
|
@@ -29970,32 +30030,34 @@ function renderShareBars(heading, rows, countLabel) {
|
|
|
29970
30030
|
}
|
|
29971
30031
|
function renderAiSourceOrigin(report) {
|
|
29972
30032
|
const origin = report.aiSourceOrigin;
|
|
30033
|
+
const copy = REPORT_SECTION_COPY["ai-source-origin"];
|
|
29973
30034
|
if (origin.categories.length === 0 && origin.topDomains.length === 0) {
|
|
29974
30035
|
return section(
|
|
29975
|
-
{ id: "ai-source-origin", eyebrow:
|
|
29976
|
-
renderEmpty(
|
|
30036
|
+
{ id: ReportSectionIds["ai-source-origin"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30037
|
+
renderEmpty(copy.empty)
|
|
29977
30038
|
);
|
|
29978
30039
|
}
|
|
29979
|
-
const
|
|
29980
|
-
const headlineFragment =
|
|
30040
|
+
const headline = reportSourceOriginHeadline(origin.categories);
|
|
30041
|
+
const headlineFragment = headline ? `<p class="source-origin-headline"><strong>${headline.share}</strong> ${headline.detail}</p>` : "";
|
|
29981
30042
|
const rows = origin.topDomains.map((d) => `
|
|
29982
30043
|
<tr>
|
|
29983
30044
|
<td>${escapeHtml2(d.domain)}</td>
|
|
29984
30045
|
<td class="numeric">${d.count}</td>
|
|
29985
|
-
<td>${d.isCompetitor ?
|
|
30046
|
+
<td>${d.isCompetitor ? `<span class="badge tone-negative">${copy.trackedCompetitorTag}</span>` : `<span class="badge tone-neutral">${copy.externalTag}</span>`}</td>
|
|
29986
30047
|
</tr>`).join("");
|
|
29987
|
-
const
|
|
29988
|
-
|
|
29989
|
-
|
|
30048
|
+
const [domainHeader, citationsHeader, tagHeader] = copy.topSourceHeaders;
|
|
30049
|
+
const table = origin.topDomains.length > 0 ? `<div class="chart-card"><h3>${copy.topSourcesHeading}</h3>
|
|
30050
|
+
<div class="table-scroll"><table class="report-table">
|
|
30051
|
+
<thead><tr><th>${domainHeader}</th><th class="numeric">${citationsHeader}</th><th>${tagHeader}</th></tr></thead>
|
|
29990
30052
|
<tbody>${rows}</tbody>
|
|
29991
|
-
</table>
|
|
30053
|
+
</table></div>
|
|
29992
30054
|
</div>` : "";
|
|
29993
30055
|
return section(
|
|
29994
30056
|
{
|
|
29995
|
-
id: "ai-source-origin",
|
|
29996
|
-
eyebrow:
|
|
29997
|
-
title:
|
|
29998
|
-
intro:
|
|
30057
|
+
id: ReportSectionIds["ai-source-origin"],
|
|
30058
|
+
eyebrow: copy.eyebrow,
|
|
30059
|
+
title: copy.title,
|
|
30060
|
+
intro: copy.intro
|
|
29999
30061
|
},
|
|
30000
30062
|
`${headlineFragment}${table}${renderCategoryBars(origin.categories)}`
|
|
30001
30063
|
);
|
|
@@ -30022,7 +30084,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
30022
30084
|
}).join("");
|
|
30023
30085
|
return `<div class="chart-card">
|
|
30024
30086
|
<h3>${escapeHtml2(title)}</h3>
|
|
30025
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml2(title)}
|
|
30087
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${escapeHtml2(reportLineChartLabel(title))}">
|
|
30026
30088
|
<line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
|
|
30027
30089
|
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max2)}</text>
|
|
30028
30090
|
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
|
|
@@ -30034,10 +30096,11 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
30034
30096
|
}
|
|
30035
30097
|
function renderGsc(report) {
|
|
30036
30098
|
const gsc = report.gsc;
|
|
30099
|
+
const copy = REPORT_SECTION_COPY.gsc;
|
|
30037
30100
|
if (!gsc) {
|
|
30038
30101
|
return section(
|
|
30039
|
-
{ id:
|
|
30040
|
-
renderEmpty(
|
|
30102
|
+
{ id: ReportSectionIds.gsc, eyebrow: copy.eyebrow, title: copy.title },
|
|
30103
|
+
renderEmpty(copy.empty)
|
|
30041
30104
|
);
|
|
30042
30105
|
}
|
|
30043
30106
|
const rows = gsc.topQueries.map((q) => `
|
|
@@ -30050,48 +30113,48 @@ function renderGsc(report) {
|
|
|
30050
30113
|
<td><span class="badge tone-neutral">${escapeHtml2(q.category)}</span></td>
|
|
30051
30114
|
</tr>`).join("");
|
|
30052
30115
|
const categoryBars = renderShareBars(
|
|
30053
|
-
|
|
30116
|
+
copy.intentHeading,
|
|
30054
30117
|
gsc.categoryBreakdown.map((c, index2) => ({
|
|
30055
30118
|
label: c.category,
|
|
30056
30119
|
count: c.clicks,
|
|
30057
30120
|
sharePct: c.sharePct,
|
|
30058
30121
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30059
30122
|
})),
|
|
30060
|
-
|
|
30123
|
+
copy.intentCountLabel
|
|
30061
30124
|
);
|
|
30062
30125
|
const trendChart = renderLineChart(
|
|
30063
30126
|
gsc.trend.map((t) => ({ x: t.date, y: t.clicks, label: t.date.slice(5) })),
|
|
30064
30127
|
COLORS.accent,
|
|
30065
|
-
|
|
30128
|
+
copy.trendTitle
|
|
30066
30129
|
);
|
|
30067
30130
|
const crossoverBlocks = [];
|
|
30068
30131
|
if (gsc.trackedButNoGsc.length > 0) {
|
|
30069
|
-
crossoverBlocks.push(`<div class="chart-card"><h3
|
|
30070
|
-
<p class="section-intro"
|
|
30132
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>${copy.untrackedDemand.heading}</h3>
|
|
30133
|
+
<p class="section-intro">${copy.untrackedDemand.subtitle}</p>
|
|
30071
30134
|
${renderProofChips(gsc.trackedButNoGsc, 6)}
|
|
30072
30135
|
</div>`);
|
|
30073
30136
|
}
|
|
30074
30137
|
if (gsc.gscButNotTracked.length > 0) {
|
|
30075
|
-
crossoverBlocks.push(`<div class="chart-card"><h3
|
|
30076
|
-
<p class="section-intro"
|
|
30138
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>${copy.suggestedQueries.heading}</h3>
|
|
30139
|
+
<p class="section-intro">${copy.suggestedQueries.subtitle}</p>
|
|
30077
30140
|
${renderProofChips(gsc.gscButNotTracked, 6)}
|
|
30078
30141
|
</div>`);
|
|
30079
30142
|
}
|
|
30080
|
-
const
|
|
30143
|
+
const [queryHeader, clicksHeader, impressionsHeader, ctrHeader, positionHeader, categoryHeader] = copy.topQueryHeaders;
|
|
30081
30144
|
return section(
|
|
30082
|
-
{ id:
|
|
30145
|
+
{ id: ReportSectionIds.gsc, eyebrow: copy.eyebrow, title: copy.title, intro: reportGscIntro(report) },
|
|
30083
30146
|
`<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"
|
|
30147
|
+
<div class="metric"><div class="label">${copy.tiles.clicks}</div><div class="value">${formatNumber(gsc.totalClicks)}</div></div>
|
|
30148
|
+
<div class="metric"><div class="label">${copy.tiles.impressions}</div><div class="value">${formatNumber(gsc.totalImpressions)}</div></div>
|
|
30149
|
+
<div class="metric"><div class="label">${copy.tiles.ctr}</div><div class="value">${formatRatio(gsc.ctr)}</div></div>
|
|
30150
|
+
<div class="metric"><div class="label">${copy.tiles.position}</div><div class="value">${gsc.avgPosition.toFixed(1)}</div></div>
|
|
30088
30151
|
</div>
|
|
30089
30152
|
${trendChart}
|
|
30090
|
-
<div class="chart-card"><h3
|
|
30091
|
-
<table class="report-table">
|
|
30092
|
-
<thead><tr><th
|
|
30153
|
+
<div class="chart-card"><h3>${copy.topQueriesHeading}</h3>
|
|
30154
|
+
<div class="table-scroll"><table class="report-table">
|
|
30155
|
+
<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
30156
|
<tbody>${rows}</tbody>
|
|
30094
|
-
</table>
|
|
30157
|
+
</table></div>
|
|
30095
30158
|
</div>
|
|
30096
30159
|
${categoryBars}
|
|
30097
30160
|
${crossoverBlocks.join("\n")}`
|
|
@@ -30099,10 +30162,11 @@ function renderGsc(report) {
|
|
|
30099
30162
|
}
|
|
30100
30163
|
function renderGa(report) {
|
|
30101
30164
|
const ga = report.ga;
|
|
30165
|
+
const copy = REPORT_SECTION_COPY.ga;
|
|
30102
30166
|
if (!ga) {
|
|
30103
30167
|
return section(
|
|
30104
|
-
{ id:
|
|
30105
|
-
renderEmpty(
|
|
30168
|
+
{ id: ReportSectionIds.ga, eyebrow: copy.eyebrow, title: copy.title },
|
|
30169
|
+
renderEmpty(copy.empty)
|
|
30106
30170
|
);
|
|
30107
30171
|
}
|
|
30108
30172
|
const pageRows = ga.topLandingPages.map((p) => `
|
|
@@ -30112,48 +30176,50 @@ function renderGa(report) {
|
|
|
30112
30176
|
<td class="numeric">${formatNumber(p.organicSessions)}</td>
|
|
30113
30177
|
</tr>`).join("");
|
|
30114
30178
|
const channelBars = renderShareBars(
|
|
30115
|
-
|
|
30179
|
+
copy.channelsHeading,
|
|
30116
30180
|
ga.channelBreakdown.map((c, index2) => ({
|
|
30117
30181
|
label: c.channel,
|
|
30118
30182
|
count: c.sessions,
|
|
30119
30183
|
sharePct: c.sharePct,
|
|
30120
30184
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30121
30185
|
})),
|
|
30122
|
-
|
|
30186
|
+
copy.channelsCountLabel
|
|
30123
30187
|
);
|
|
30188
|
+
const [pageHeader, sessionsHeader, organicHeader] = copy.topPageHeaders;
|
|
30124
30189
|
return section(
|
|
30125
|
-
{ id:
|
|
30190
|
+
{ id: ReportSectionIds.ga, eyebrow: copy.eyebrow, title: copy.title, intro: reportGaIntro(ga) },
|
|
30126
30191
|
`<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"
|
|
30192
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(ga.totalSessions)}</div></div>
|
|
30193
|
+
<div class="metric"><div class="label">${copy.tiles.users}</div><div class="value">${formatNumber(ga.totalUsers)}</div></div>
|
|
30194
|
+
<div class="metric"><div class="label">${copy.tiles.organicSessions}</div><div class="value">${formatNumber(ga.totalOrganicSessions)}</div></div>
|
|
30130
30195
|
</div>
|
|
30131
|
-
<div class="chart-card"><h3
|
|
30132
|
-
<table class="report-table">
|
|
30133
|
-
<thead><tr><th
|
|
30196
|
+
<div class="chart-card"><h3>${copy.topPagesHeading}</h3>
|
|
30197
|
+
<div class="table-scroll"><table class="report-table">
|
|
30198
|
+
<thead><tr><th>${pageHeader}</th><th class="numeric">${sessionsHeader}</th><th class="numeric">${organicHeader}</th></tr></thead>
|
|
30134
30199
|
<tbody>${pageRows}</tbody>
|
|
30135
|
-
</table>
|
|
30200
|
+
</table></div>
|
|
30136
30201
|
</div>
|
|
30137
30202
|
${channelBars}`
|
|
30138
30203
|
);
|
|
30139
30204
|
}
|
|
30140
30205
|
function renderSocial(report) {
|
|
30141
30206
|
const social = report.socialReferrals;
|
|
30207
|
+
const copy = REPORT_SECTION_COPY["social-referrals"];
|
|
30142
30208
|
if (!social) {
|
|
30143
30209
|
return section(
|
|
30144
|
-
{ id: "social-referrals", eyebrow:
|
|
30145
|
-
renderEmpty(
|
|
30210
|
+
{ id: ReportSectionIds["social-referrals"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30211
|
+
renderEmpty(copy.empty)
|
|
30146
30212
|
);
|
|
30147
30213
|
}
|
|
30148
30214
|
const channelBars = renderShareBars(
|
|
30149
|
-
|
|
30215
|
+
copy.channelsHeading,
|
|
30150
30216
|
social.channels.map((c, index2) => ({
|
|
30151
30217
|
label: c.channelGroup,
|
|
30152
30218
|
count: c.sessions,
|
|
30153
30219
|
sharePct: c.sharePct,
|
|
30154
30220
|
color: COLORS.series[index2 % COLORS.series.length]
|
|
30155
30221
|
})),
|
|
30156
|
-
|
|
30222
|
+
copy.channelsCountLabel
|
|
30157
30223
|
);
|
|
30158
30224
|
const campaignRows = social.topCampaigns.map((c) => `
|
|
30159
30225
|
<tr>
|
|
@@ -30161,39 +30227,41 @@ function renderSocial(report) {
|
|
|
30161
30227
|
<td>${escapeHtml2(c.medium)}</td>
|
|
30162
30228
|
<td class="numeric">${formatNumber(c.sessions)}</td>
|
|
30163
30229
|
</tr>`).join("");
|
|
30230
|
+
const [sourceHeader, mediumHeader, sessionsHeader] = copy.campaignHeaders;
|
|
30164
30231
|
return section(
|
|
30165
|
-
{ id: "social-referrals", eyebrow:
|
|
30232
|
+
{ id: ReportSectionIds["social-referrals"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30166
30233
|
`<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"
|
|
30234
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(social.totalSessions)}</div></div>
|
|
30235
|
+
<div class="metric"><div class="label">${copy.tiles.organic}</div><div class="value">${formatNumber(social.organicSessions)}</div></div>
|
|
30236
|
+
<div class="metric"><div class="label">${copy.tiles.paid}</div><div class="value">${formatNumber(social.paidSessions)}</div></div>
|
|
30170
30237
|
</div>
|
|
30171
30238
|
${channelBars}
|
|
30172
|
-
<div class="chart-card"><h3
|
|
30173
|
-
<table class="report-table">
|
|
30174
|
-
<thead><tr><th
|
|
30239
|
+
<div class="chart-card"><h3>${copy.campaignsHeading}</h3>
|
|
30240
|
+
<div class="table-scroll"><table class="report-table">
|
|
30241
|
+
<thead><tr><th>${sourceHeader}</th><th>${mediumHeader}</th><th class="numeric">${sessionsHeader}</th></tr></thead>
|
|
30175
30242
|
<tbody>${campaignRows}</tbody>
|
|
30176
|
-
</table>
|
|
30243
|
+
</table></div>
|
|
30177
30244
|
</div>`
|
|
30178
30245
|
);
|
|
30179
30246
|
}
|
|
30180
30247
|
function renderAiReferrals(report) {
|
|
30181
30248
|
const ai = report.aiReferrals;
|
|
30249
|
+
const copy = REPORT_SECTION_COPY["ai-referrals"];
|
|
30182
30250
|
if (!ai) {
|
|
30183
30251
|
return section(
|
|
30184
|
-
{ id: "ai-referrals", eyebrow:
|
|
30185
|
-
renderEmpty(
|
|
30252
|
+
{ id: ReportSectionIds["ai-referrals"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30253
|
+
renderEmpty(copy.empty)
|
|
30186
30254
|
);
|
|
30187
30255
|
}
|
|
30188
30256
|
const sourceBars = renderShareBars(
|
|
30189
|
-
|
|
30257
|
+
copy.sourcesHeading,
|
|
30190
30258
|
ai.bySource.map((s, index2) => ({
|
|
30191
30259
|
label: s.source,
|
|
30192
30260
|
count: s.sessions,
|
|
30193
30261
|
sharePct: s.sharePct,
|
|
30194
30262
|
color: COLORS.series[(index2 + 2) % COLORS.series.length]
|
|
30195
30263
|
})),
|
|
30196
|
-
|
|
30264
|
+
copy.sourcesCountLabel
|
|
30197
30265
|
);
|
|
30198
30266
|
const pageRows = ai.topLandingPages.map((p) => `
|
|
30199
30267
|
<tr>
|
|
@@ -30203,58 +30271,50 @@ function renderAiReferrals(report) {
|
|
|
30203
30271
|
const trendChart = renderLineChart(
|
|
30204
30272
|
ai.trend.map((t) => ({ x: t.date, y: t.sessions, label: t.date.slice(5) })),
|
|
30205
30273
|
COLORS.series[2],
|
|
30206
|
-
|
|
30274
|
+
copy.trendTitle
|
|
30207
30275
|
);
|
|
30276
|
+
const [pageHeader, sessionsHeader] = copy.topPageHeaders;
|
|
30208
30277
|
return section(
|
|
30209
|
-
{ id: "ai-referrals", eyebrow:
|
|
30278
|
+
{ id: ReportSectionIds["ai-referrals"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30210
30279
|
`<div class="metric-grid">
|
|
30211
|
-
<div class="metric"><div class="label"
|
|
30280
|
+
<div class="metric"><div class="label">${copy.tiles.sessions}</div><div class="value">${formatNumber(ai.totalSessions)}</div></div>
|
|
30212
30281
|
</div>
|
|
30213
30282
|
${trendChart}
|
|
30214
30283
|
${sourceBars}
|
|
30215
|
-
<div class="chart-card"><h3
|
|
30216
|
-
<table class="report-table">
|
|
30217
|
-
<thead><tr><th
|
|
30284
|
+
<div class="chart-card"><h3>${copy.topPagesHeading}</h3>
|
|
30285
|
+
<div class="table-scroll"><table class="report-table">
|
|
30286
|
+
<thead><tr><th>${pageHeader}</th><th class="numeric">${sessionsHeader}</th></tr></thead>
|
|
30218
30287
|
<tbody>${pageRows}</tbody>
|
|
30219
|
-
</table>
|
|
30288
|
+
</table></div>
|
|
30220
30289
|
</div>`
|
|
30221
30290
|
);
|
|
30222
30291
|
}
|
|
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
30292
|
function renderServerActivity(report, audience) {
|
|
30233
30293
|
const sa = report.serverActivity;
|
|
30234
30294
|
const isClient = audience === "client";
|
|
30235
30295
|
const windowDays2 = report.meta.periodDays;
|
|
30236
|
-
const
|
|
30237
|
-
const priorWindowLabel =
|
|
30296
|
+
const copy = REPORT_SECTION_COPY["server-activity"];
|
|
30297
|
+
const priorWindowLabel = reportPriorWindowLabel(windowDays2);
|
|
30238
30298
|
if (!sa) {
|
|
30239
30299
|
if (isClient) return "";
|
|
30240
30300
|
return section(
|
|
30241
|
-
|
|
30242
|
-
renderEmpty(
|
|
30301
|
+
reportServerActivityHeading("agency", false, windowDays2),
|
|
30302
|
+
renderEmpty(copy.agency.emptyNotConnected)
|
|
30243
30303
|
);
|
|
30244
30304
|
}
|
|
30245
30305
|
if (!sa.hasData) {
|
|
30246
30306
|
return section(
|
|
30247
|
-
|
|
30248
|
-
renderEmpty(isClient ?
|
|
30307
|
+
reportServerActivityHeading(audience, false, windowDays2),
|
|
30308
|
+
renderEmpty(isClient ? copy.client.empty : copy.agency.empty)
|
|
30249
30309
|
);
|
|
30250
30310
|
}
|
|
30251
30311
|
const formatDelta = (d, suffix) => {
|
|
30252
|
-
const
|
|
30253
|
-
if (!
|
|
30254
|
-
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml2(
|
|
30312
|
+
const copy2 = formatDeltaCopy(d, suffix, priorWindowLabel);
|
|
30313
|
+
if (!copy2) return "";
|
|
30314
|
+
return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml2(copy2)}</span>`;
|
|
30255
30315
|
};
|
|
30256
|
-
const referralRedirectNote =
|
|
30257
|
-
const referralSubtitle = [formatDelta(sa.referralArrivals,
|
|
30316
|
+
const referralRedirectNote = reportReferralRedirectNote(sa.referralRedirects);
|
|
30317
|
+
const referralSubtitle = [formatDelta(sa.referralArrivals, copy.countNouns.sessions), escapeHtml2(sa.referralArrivalsClassSummary), referralRedirectNote].filter(Boolean).join(" \xB7 ");
|
|
30258
30318
|
if (isClient) {
|
|
30259
30319
|
const crawlerRequests = {
|
|
30260
30320
|
current: sa.verifiedCrawlerHits.current + sa.unverifiedCrawlerHits.current,
|
|
@@ -30264,11 +30324,11 @@ function renderServerActivity(report, audience) {
|
|
|
30264
30324
|
sa.verifiedCrawlerHits.prior + sa.unverifiedCrawlerHits.prior
|
|
30265
30325
|
)
|
|
30266
30326
|
};
|
|
30267
|
-
const crawlerTrustSummary =
|
|
30268
|
-
const crawlerDelta = formatDelta(crawlerRequests,
|
|
30327
|
+
const crawlerTrustSummary = reportCrawlerTrustSummary(sa.verifiedCrawlerHits.current, sa.unverifiedCrawlerHits.current);
|
|
30328
|
+
const crawlerDelta = formatDelta(crawlerRequests, copy.countNouns.requests);
|
|
30269
30329
|
const crawlerSubtitle = crawlerDelta ? `${escapeHtml2(crawlerTrustSummary)} \xB7 ${crawlerDelta}` : escapeHtml2(crawlerTrustSummary);
|
|
30270
|
-
const userFetchDelta = formatDelta(sa.aiUserFetchHits,
|
|
30271
|
-
const userFetchSubtitle = userFetchDelta || escapeHtml2(
|
|
30330
|
+
const userFetchDelta = formatDelta(sa.aiUserFetchHits, copy.countNouns.requests);
|
|
30331
|
+
const userFetchSubtitle = userFetchDelta || escapeHtml2(copy.client.userFetchFallback);
|
|
30272
30332
|
const clientOperators = sa.byOperator.filter((o) => o.verifiedHits > 0 || o.unverifiedHits > 0 || o.userFetchHits > 0 || o.referralArrivals > 0).slice(0, 5);
|
|
30273
30333
|
const clientOperatorRows = clientOperators.map((o) => `
|
|
30274
30334
|
<tr>
|
|
@@ -30277,36 +30337,37 @@ function renderServerActivity(report, audience) {
|
|
|
30277
30337
|
<td class="numeric">${formatNumber(o.userFetchHits)}</td>
|
|
30278
30338
|
<td class="numeric">${formatNumber(o.referralArrivals)}</td>
|
|
30279
30339
|
</tr>`).join("");
|
|
30340
|
+
const [toolHeader, botRequestsHeader, userFetchesHeader2, referralSessionsHeader2] = reportServerActivityClientOperatorHeaders(windowDays2);
|
|
30280
30341
|
return section(
|
|
30281
|
-
|
|
30342
|
+
reportServerActivityHeading("client", true, windowDays2),
|
|
30282
30343
|
`<div class="metric-grid">
|
|
30283
30344
|
<div class="metric">
|
|
30284
|
-
<div class="label"
|
|
30345
|
+
<div class="label">${copy.client.tiles.botRequests}</div>
|
|
30285
30346
|
<div class="value">${formatNumber(crawlerRequests.current)}</div>
|
|
30286
30347
|
<div class="subtitle">${crawlerSubtitle}</div>
|
|
30287
30348
|
</div>
|
|
30288
30349
|
<div class="metric">
|
|
30289
|
-
<div class="label"
|
|
30350
|
+
<div class="label">${copy.client.tiles.userFetches}</div>
|
|
30290
30351
|
<div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
|
|
30291
30352
|
<div class="subtitle">${userFetchSubtitle}</div>
|
|
30292
30353
|
</div>
|
|
30293
30354
|
<div class="metric">
|
|
30294
|
-
<div class="label"
|
|
30355
|
+
<div class="label">${copy.client.tiles.referralSessions}</div>
|
|
30295
30356
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
30296
30357
|
<div class="subtitle">${referralSubtitle}</div>
|
|
30297
30358
|
</div>
|
|
30298
30359
|
</div>
|
|
30299
|
-
${clientOperatorRows ? `<div class="chart-card"><h3
|
|
30300
|
-
<table class="report-table">
|
|
30301
|
-
<thead><tr><th
|
|
30360
|
+
${clientOperatorRows ? `<div class="chart-card"><h3>${copy.client.operatorsHeading}</h3>
|
|
30361
|
+
<div class="table-scroll"><table class="report-table">
|
|
30362
|
+
<thead><tr><th>${toolHeader}</th><th class="numeric">${botRequestsHeader}</th><th class="numeric">${userFetchesHeader2}</th><th class="numeric">${referralSessionsHeader2}</th></tr></thead>
|
|
30302
30363
|
<tbody>${clientOperatorRows}</tbody>
|
|
30303
|
-
</table>
|
|
30304
|
-
<p class="meta"
|
|
30364
|
+
</table></div>
|
|
30365
|
+
<p class="meta">${copy.client.operatorsFootnote}</p>
|
|
30305
30366
|
</div>` : ""}`
|
|
30306
30367
|
);
|
|
30307
30368
|
}
|
|
30308
30369
|
const operatorRows = sa.byOperator.map((o) => {
|
|
30309
|
-
const deltaText = o.deltaPct
|
|
30370
|
+
const deltaText = reportServerActivityOperatorDelta(o.deltaPct);
|
|
30310
30371
|
const toneClass = o.deltaPct === null ? "" : `tone-${deltaTone(o.deltaPct)}`;
|
|
30311
30372
|
return `
|
|
30312
30373
|
<tr>
|
|
@@ -30321,7 +30382,7 @@ function renderServerActivity(report, audience) {
|
|
|
30321
30382
|
const pathRows = sa.topCrawledPaths.map((p) => `
|
|
30322
30383
|
<tr>
|
|
30323
30384
|
<td class="page-cell">${formatLandingPageHtml(p.path)}</td>
|
|
30324
|
-
<td class="numeric">${formatNumber(
|
|
30385
|
+
<td class="numeric">${formatNumber(reportServerActivityPathHits(p))}</td>
|
|
30325
30386
|
<td class="numeric meta">${formatNumber(p.verifiedHits)}</td>
|
|
30326
30387
|
<td class="numeric">${p.distinctOperators}</td>
|
|
30327
30388
|
</tr>`).join("");
|
|
@@ -30340,75 +30401,82 @@ function renderServerActivity(report, audience) {
|
|
|
30340
30401
|
const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
|
|
30341
30402
|
sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
|
|
30342
30403
|
COLORS.series[1],
|
|
30343
|
-
|
|
30404
|
+
reportServerActivityTrendTitle(windowDays2)
|
|
30344
30405
|
) : "";
|
|
30406
|
+
const agency = copy.agency;
|
|
30407
|
+
const tiles = reportServerActivityAgencyTiles(windowDays2);
|
|
30408
|
+
const [operatorHeader, verifiedHeader, unverifiedHeader, userFetchesHeader, referralSessionsHeader, deltaHeader] = reportServerActivityAgencyOperatorHeaders(windowDays2);
|
|
30409
|
+
const [crawledPathHeader, hitsHeader, verifiedHitsHeader, operatorCountHeader] = agency.crawledPathHeaders;
|
|
30410
|
+
const [productHeader, productSessionsHeader, landingPathCountHeader] = agency.referralProductHeaders;
|
|
30411
|
+
const [landingPathHeader, landingSessionsHeader, productCountHeader] = agency.referralLandingHeaders;
|
|
30345
30412
|
return section(
|
|
30346
|
-
|
|
30413
|
+
reportServerActivityHeading("agency", true, windowDays2),
|
|
30347
30414
|
`<div class="metric-grid">
|
|
30348
30415
|
<div class="metric">
|
|
30349
|
-
<div class="label"
|
|
30416
|
+
<div class="label">${tiles.verified}</div>
|
|
30350
30417
|
<div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
|
|
30351
|
-
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits,
|
|
30418
|
+
<div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, copy.countNouns.hits)}</div>
|
|
30352
30419
|
</div>
|
|
30353
30420
|
<div class="metric">
|
|
30354
|
-
<div class="label"
|
|
30421
|
+
<div class="label">${tiles.unverified}</div>
|
|
30355
30422
|
<div class="value">${formatNumber(sa.unverifiedCrawlerHits.current)}</div>
|
|
30356
|
-
<div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits,
|
|
30423
|
+
<div class="subtitle">${formatDelta(sa.unverifiedCrawlerHits, copy.countNouns.hits)}</div>
|
|
30357
30424
|
</div>
|
|
30358
30425
|
<div class="metric">
|
|
30359
|
-
<div class="label"
|
|
30426
|
+
<div class="label">${tiles.userFetches}</div>
|
|
30360
30427
|
<div class="value">${formatNumber(sa.aiUserFetchHits.current)}</div>
|
|
30361
|
-
<div class="subtitle">${formatDelta(sa.aiUserFetchHits,
|
|
30428
|
+
<div class="subtitle">${formatDelta(sa.aiUserFetchHits, copy.countNouns.hits)}</div>
|
|
30362
30429
|
</div>
|
|
30363
30430
|
<div class="metric">
|
|
30364
|
-
<div class="label"
|
|
30431
|
+
<div class="label">${tiles.referralSessions}</div>
|
|
30365
30432
|
<div class="value">${formatNumber(sa.referralArrivals.current)}</div>
|
|
30366
30433
|
<div class="subtitle">${referralSubtitle}</div>
|
|
30367
30434
|
</div>
|
|
30368
30435
|
</div>
|
|
30369
30436
|
${trendChart}
|
|
30370
|
-
${operatorRows ? `<div class="chart-card"><h3
|
|
30371
|
-
<p class="meta"
|
|
30372
|
-
<table class="report-table">
|
|
30373
|
-
<thead><tr><th
|
|
30437
|
+
${operatorRows ? `<div class="chart-card"><h3>${agency.operatorsHeading}</h3>
|
|
30438
|
+
<p class="meta">${agency.operatorsNote}</p>
|
|
30439
|
+
<div class="table-scroll"><table class="report-table">
|
|
30440
|
+
<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
30441
|
<tbody>${operatorRows}</tbody>
|
|
30375
|
-
</table>
|
|
30442
|
+
</table></div>
|
|
30376
30443
|
</div>` : ""}
|
|
30377
|
-
${pathRows ? `<div class="chart-card"><h3
|
|
30378
|
-
<p class="meta"
|
|
30379
|
-
<table class="report-table">
|
|
30380
|
-
<thead><tr><th
|
|
30444
|
+
${pathRows ? `<div class="chart-card"><h3>${agency.crawledPathsHeading}</h3>
|
|
30445
|
+
<p class="meta">${reportServerActivityCrawledPathsNote(windowDays2)}</p>
|
|
30446
|
+
<div class="table-scroll"><table class="report-table">
|
|
30447
|
+
<thead><tr><th>${crawledPathHeader}</th><th class="numeric">${hitsHeader}</th><th class="numeric">${verifiedHitsHeader}</th><th class="numeric">${operatorCountHeader}</th></tr></thead>
|
|
30381
30448
|
<tbody>${pathRows}</tbody>
|
|
30382
|
-
</table>
|
|
30449
|
+
</table></div>
|
|
30383
30450
|
</div>` : ""}
|
|
30384
|
-
${referralProductRows ? `<div class="chart-card"><h3
|
|
30385
|
-
<p class="meta"
|
|
30386
|
-
<table class="report-table">
|
|
30387
|
-
<thead><tr><th
|
|
30451
|
+
${referralProductRows ? `<div class="chart-card"><h3>${agency.referralProductsHeading}</h3>
|
|
30452
|
+
<p class="meta">${agency.referralProductsNote}</p>
|
|
30453
|
+
<div class="table-scroll"><table class="report-table">
|
|
30454
|
+
<thead><tr><th>${productHeader}</th><th class="numeric">${productSessionsHeader}</th><th class="numeric">${landingPathCountHeader}</th></tr></thead>
|
|
30388
30455
|
<tbody>${referralProductRows}</tbody>
|
|
30389
|
-
</table>
|
|
30456
|
+
</table></div>
|
|
30390
30457
|
</div>` : ""}
|
|
30391
|
-
${referralLandingRows ? `<div class="chart-card"><h3
|
|
30392
|
-
<table class="report-table">
|
|
30393
|
-
<thead><tr><th
|
|
30458
|
+
${referralLandingRows ? `<div class="chart-card"><h3>${agency.referralLandingHeading}</h3>
|
|
30459
|
+
<div class="table-scroll"><table class="report-table">
|
|
30460
|
+
<thead><tr><th>${landingPathHeader}</th><th class="numeric">${landingSessionsHeader}</th><th class="numeric">${productCountHeader}</th></tr></thead>
|
|
30394
30461
|
<tbody>${referralLandingRows}</tbody>
|
|
30395
|
-
</table>
|
|
30462
|
+
</table></div>
|
|
30396
30463
|
</div>` : ""}`
|
|
30397
30464
|
);
|
|
30398
30465
|
}
|
|
30399
30466
|
function renderIndexingHealth(report) {
|
|
30400
30467
|
const ih = report.indexingHealth;
|
|
30468
|
+
const copy = REPORT_SECTION_COPY["indexing-health"];
|
|
30401
30469
|
if (!ih) {
|
|
30402
30470
|
return section(
|
|
30403
|
-
{ id: "indexing-health", eyebrow:
|
|
30404
|
-
renderEmpty(
|
|
30471
|
+
{ id: ReportSectionIds["indexing-health"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30472
|
+
renderEmpty(copy.empty)
|
|
30405
30473
|
);
|
|
30406
30474
|
}
|
|
30407
30475
|
const segments = [
|
|
30408
|
-
{ label:
|
|
30409
|
-
{ label:
|
|
30410
|
-
{ label:
|
|
30411
|
-
{ label:
|
|
30476
|
+
{ label: copy.segments.indexed, count: ih.indexed, color: COLORS.positive },
|
|
30477
|
+
{ label: copy.segments.notIndexed, count: ih.notIndexed, color: COLORS.caution },
|
|
30478
|
+
{ label: copy.segments.deindexed, count: ih.deindexed, color: COLORS.negative },
|
|
30479
|
+
{ label: copy.segments.unknown, count: ih.unknown, color: COLORS.neutral }
|
|
30412
30480
|
].filter((s) => s.count > 0);
|
|
30413
30481
|
const total = segments.reduce((s, x) => s + x.count, 0) || 1;
|
|
30414
30482
|
const width = 600;
|
|
@@ -30420,166 +30488,171 @@ function renderIndexingHealth(report) {
|
|
|
30420
30488
|
acc += w;
|
|
30421
30489
|
return `<rect x="${x}" y="0" width="${w}" height="${height}" fill="${s.color}" />`;
|
|
30422
30490
|
}).join("");
|
|
30423
|
-
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml2(s.label
|
|
30491
|
+
const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml2(reportIndexingLegendLabel(s.label, s.count))}</span>`).join("");
|
|
30424
30492
|
return section(
|
|
30425
|
-
{ id: "indexing-health", eyebrow:
|
|
30493
|
+
{ id: ReportSectionIds["indexing-health"], eyebrow: copy.eyebrow, title: copy.title, intro: reportIndexingIntro(ih.provider) },
|
|
30426
30494
|
`<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"
|
|
30495
|
+
<div class="metric"><div class="label">${copy.tiles.indexed}</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
|
|
30496
|
+
<div class="metric"><div class="label">${copy.tiles.total}</div><div class="value">${formatNumber(ih.total)}</div></div>
|
|
30497
|
+
<div class="metric"><div class="label">${copy.tiles.share}</div><div class="value">${ih.indexedPct}%</div></div>
|
|
30430
30498
|
</div>
|
|
30431
30499
|
<div class="chart-card">
|
|
30432
|
-
<h3
|
|
30433
|
-
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="
|
|
30500
|
+
<h3>${copy.coverageHeading}</h3>
|
|
30501
|
+
<svg viewBox="0 0 ${width} ${height}" width="100%" preserveAspectRatio="xMinYMin meet" role="img" aria-label="${copy.coverageLabel}">${bars}</svg>
|
|
30434
30502
|
<div class="legend">${legend}</div>
|
|
30435
30503
|
</div>`
|
|
30436
30504
|
);
|
|
30437
30505
|
}
|
|
30438
30506
|
function renderCitationsTrend(report) {
|
|
30439
30507
|
const trend = report.citationsTrend;
|
|
30508
|
+
const copy = REPORT_SECTION_COPY["citations-trend"];
|
|
30440
30509
|
if (trend.length === 0) {
|
|
30441
30510
|
return section(
|
|
30442
|
-
{ id: "citations-trend", eyebrow:
|
|
30443
|
-
renderEmpty(
|
|
30511
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30512
|
+
renderEmpty(copy.empty)
|
|
30444
30513
|
);
|
|
30445
30514
|
}
|
|
30446
30515
|
if (isTrendBaseline(trend)) {
|
|
30447
30516
|
return section(
|
|
30448
|
-
{ id: "citations-trend", eyebrow:
|
|
30449
|
-
renderEmpty(
|
|
30517
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title },
|
|
30518
|
+
renderEmpty(reportCitationsTrendBaseline(trend.length))
|
|
30450
30519
|
);
|
|
30451
30520
|
}
|
|
30452
30521
|
const chart = renderLineChart(
|
|
30453
30522
|
trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
|
|
30454
30523
|
COLORS.positive,
|
|
30455
|
-
|
|
30524
|
+
copy.chartTitle,
|
|
30456
30525
|
220
|
|
30457
30526
|
);
|
|
30458
30527
|
const rows = trend.map((t) => `
|
|
30459
30528
|
<tr>
|
|
30460
30529
|
<td>${formatDate(t.date)}</td>
|
|
30461
30530
|
<td class="numeric">${t.citationRate}% <span class="cell-pending">(${t.citedQueryCount}/${t.totalQueryCount})</span></td>
|
|
30462
|
-
<td>${t.providerRates
|
|
30531
|
+
<td>${escapeHtml2(reportTrendProviderRates(t.providerRates))}</td>
|
|
30463
30532
|
</tr>`).join("");
|
|
30533
|
+
const [checkHeader, citedQueriesHeader, engineRatesHeader] = copy.breakdownHeaders;
|
|
30464
30534
|
return section(
|
|
30465
|
-
{ id: "citations-trend", eyebrow:
|
|
30535
|
+
{ id: ReportSectionIds["citations-trend"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30466
30536
|
`${chart}
|
|
30467
|
-
<div class="chart-card"><h3
|
|
30468
|
-
<table class="report-table">
|
|
30469
|
-
<thead><tr><th
|
|
30537
|
+
<div class="chart-card"><h3>${copy.breakdownHeading}</h3>
|
|
30538
|
+
<div class="table-scroll"><table class="report-table">
|
|
30539
|
+
<thead><tr><th>${checkHeader}</th><th class="numeric">${citedQueriesHeader}</th><th>${engineRatesHeader}</th></tr></thead>
|
|
30470
30540
|
<tbody>${rows}</tbody>
|
|
30471
|
-
</table>
|
|
30541
|
+
</table></div>
|
|
30472
30542
|
</div>`
|
|
30473
30543
|
);
|
|
30474
30544
|
}
|
|
30475
30545
|
function renderInsights(report) {
|
|
30476
30546
|
const list = report.insights;
|
|
30547
|
+
const copy = REPORT_SECTION_COPY.insights;
|
|
30477
30548
|
if (list.length === 0) {
|
|
30478
30549
|
return section(
|
|
30479
|
-
{ id:
|
|
30480
|
-
renderEmpty(
|
|
30550
|
+
{ id: ReportSectionIds.insights, eyebrow: copy.eyebrow, title: copy.title },
|
|
30551
|
+
renderEmpty(copy.empty)
|
|
30481
30552
|
);
|
|
30482
30553
|
}
|
|
30483
30554
|
const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
|
|
30484
30555
|
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"
|
|
30556
|
+
const tone = reportInsightTone(i);
|
|
30557
|
+
const countChip = count4 > 1 ? ` <span class="badge tone-neutral">${reportInstanceCountLabel(count4)}</span>` : "";
|
|
30487
30558
|
return `<tr>
|
|
30488
30559
|
<td class="col-severity"><span class="badge tone-${tone}">${escapeHtml2(reportSeverityLabel(i.severity))}</span></td>
|
|
30489
30560
|
<td class="col-title">${escapeHtml2(i.title)}${countChip}</td>
|
|
30490
30561
|
<td class="col-query">${escapeHtml2(i.query)}</td>
|
|
30491
30562
|
<td class="col-provider">${escapeHtml2(i.provider)}</td>
|
|
30492
|
-
<td class="col-recommendation">${i.recommendation ? escapeHtml2(i.recommendation) :
|
|
30563
|
+
<td class="col-recommendation">${i.recommendation ? escapeHtml2(i.recommendation) : `<span class="cell-pending">${copy.noRecommendation}</span>`}</td>
|
|
30493
30564
|
</tr>`;
|
|
30494
30565
|
}).join("");
|
|
30566
|
+
const [severityHeader, titleHeader, queryHeader, providerHeader, recommendationHeader] = copy.headers;
|
|
30495
30567
|
return section(
|
|
30496
|
-
{ id:
|
|
30497
|
-
`<table class="report-table insights-table">
|
|
30568
|
+
{ id: ReportSectionIds.insights, eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30569
|
+
`<div class="table-scroll"><table class="report-table insights-table">
|
|
30498
30570
|
<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"
|
|
30571
|
+
<th class="col-severity">${severityHeader}</th>
|
|
30572
|
+
<th class="col-title">${titleHeader}</th>
|
|
30573
|
+
<th class="col-query">${queryHeader}</th>
|
|
30574
|
+
<th class="col-provider">${providerHeader}</th>
|
|
30575
|
+
<th class="col-recommendation">${recommendationHeader}</th>
|
|
30504
30576
|
</tr></thead>
|
|
30505
30577
|
<tbody>${rows}</tbody>
|
|
30506
|
-
</table>`
|
|
30578
|
+
</table></div>`
|
|
30507
30579
|
);
|
|
30508
30580
|
}
|
|
30509
30581
|
function renderOpportunities(report) {
|
|
30510
30582
|
const opps = dedupeReportOpportunities(report);
|
|
30511
30583
|
if (opps.length === 0) return "";
|
|
30512
30584
|
const canonical = report.meta.project.canonicalDomain;
|
|
30585
|
+
const copy = REPORT_SECTION_COPY["content-opportunities"];
|
|
30513
30586
|
const highlights = `<div class="opportunity-grid">
|
|
30514
30587
|
${opps.slice(0, 3).map((o) => `<article class="opportunity-card">
|
|
30515
|
-
<div class="opportunity-score" title="
|
|
30588
|
+
<div class="opportunity-score" title="${copy.scoreCardTooltip}">${Math.round(o.score)}<span class="opportunity-score-suffix">${copy.scoreSuffix}</span></div>
|
|
30516
30589
|
<h3>${escapeHtml2(o.query)}</h3>
|
|
30517
|
-
<p>${escapeHtml2(
|
|
30590
|
+
<p>${escapeHtml2(reportOpportunityActionLine(o))}</p>
|
|
30518
30591
|
${renderProofChips(o.drivers, 2)}
|
|
30519
30592
|
</article>`).join("")}
|
|
30520
30593
|
</div>`;
|
|
30521
30594
|
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>` :
|
|
30595
|
+
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>`;
|
|
30596
|
+
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>`;
|
|
30597
|
+
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
30598
|
const surfaceTone = o.winnabilityClass === "ceded" ? "tone-caution" : "tone-neutral";
|
|
30526
30599
|
return `<tr>
|
|
30527
30600
|
<td>${escapeHtml2(o.query)}</td>
|
|
30528
30601
|
<td><span class="badge tone-neutral">${escapeHtml2(contentActionLabel(o.action))}</span></td>
|
|
30529
30602
|
<td><span class="badge ${surfaceTone}">${escapeHtml2(winnabilityClassLabel(o.winnabilityClass))}</span></td>
|
|
30530
|
-
<td class="numeric" title="
|
|
30603
|
+
<td class="numeric" title="${copy.scoreHeaderTooltip}">${Math.round(o.score)}</td>
|
|
30531
30604
|
<td>${drivers}</td>
|
|
30532
30605
|
<td>${ourPage}</td>
|
|
30533
30606
|
<td>${winning}</td>
|
|
30534
30607
|
<td><span class="badge tone-neutral">${escapeHtml2(actionConfidenceLabel(o.actionConfidence))}</span></td>
|
|
30535
30608
|
</tr>`;
|
|
30536
30609
|
}).join("");
|
|
30610
|
+
const [queryHeader, actionHeader, winnabilityHeader, scoreHeader, whyHeader, ourPageHeader, winningHeader, confidenceHeader] = copy.headers;
|
|
30537
30611
|
return section(
|
|
30538
30612
|
{
|
|
30539
|
-
id: "content-opportunities",
|
|
30540
|
-
eyebrow:
|
|
30541
|
-
title:
|
|
30542
|
-
intro:
|
|
30613
|
+
id: ReportSectionIds["content-opportunities"],
|
|
30614
|
+
eyebrow: copy.eyebrow,
|
|
30615
|
+
title: copy.title,
|
|
30616
|
+
intro: copy.intro
|
|
30543
30617
|
},
|
|
30544
|
-
`${highlights}<table class="report-table">
|
|
30545
|
-
<thead><tr><th
|
|
30618
|
+
`${highlights}<div class="table-scroll"><table class="report-table">
|
|
30619
|
+
<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
30620
|
<tbody>${rows}</tbody>
|
|
30547
|
-
</table>`
|
|
30621
|
+
</table></div>`
|
|
30548
30622
|
);
|
|
30549
30623
|
}
|
|
30550
30624
|
function renderContentGaps(report) {
|
|
30551
30625
|
const gaps = report.contentGaps;
|
|
30552
30626
|
if (gaps.length === 0) return "";
|
|
30627
|
+
const copy = REPORT_SECTION_COPY["content-gaps"];
|
|
30553
30628
|
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
30629
|
return `<tr>
|
|
30557
30630
|
<td>${escapeHtml2(g.query)}</td>
|
|
30558
30631
|
<td class="numeric">${g.competitorCount}</td>
|
|
30559
|
-
<td>${
|
|
30560
|
-
<td class="numeric">${
|
|
30632
|
+
<td>${escapeHtml2(reportCompactList(g.competitorDomains, 5))}</td>
|
|
30633
|
+
<td class="numeric">${reportMissRateLabel(g.missRate)}</td>
|
|
30561
30634
|
</tr>`;
|
|
30562
30635
|
}).join("");
|
|
30636
|
+
const [queryHeader, competitorsHeader, domainsHeader, missRateHeader] = copy.headers;
|
|
30563
30637
|
return section(
|
|
30564
30638
|
{
|
|
30565
|
-
id: "content-gaps",
|
|
30566
|
-
eyebrow:
|
|
30567
|
-
title:
|
|
30568
|
-
intro:
|
|
30639
|
+
id: ReportSectionIds["content-gaps"],
|
|
30640
|
+
eyebrow: copy.eyebrow,
|
|
30641
|
+
title: copy.title,
|
|
30642
|
+
intro: copy.intro
|
|
30569
30643
|
},
|
|
30570
|
-
`<table class="report-table">
|
|
30571
|
-
<thead><tr><th
|
|
30644
|
+
`<div class="table-scroll"><table class="report-table">
|
|
30645
|
+
<thead><tr><th>${queryHeader}</th><th class="numeric">${competitorsHeader}</th><th>${domainsHeader}</th><th class="numeric">${missRateHeader}</th></tr></thead>
|
|
30572
30646
|
<tbody>${rows}</tbody>
|
|
30573
|
-
</table>`
|
|
30647
|
+
</table></div>`
|
|
30574
30648
|
);
|
|
30575
30649
|
}
|
|
30576
30650
|
function renderRecommendedNextSteps(report) {
|
|
30577
30651
|
const steps = report.recommendedNextSteps;
|
|
30652
|
+
const copy = REPORT_SECTION_COPY["recommended-next-steps"];
|
|
30653
|
+
const heading = { id: ReportSectionIds["recommended-next-steps"], eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro };
|
|
30578
30654
|
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
|
-
);
|
|
30655
|
+
return section(heading, renderEmpty(copy.empty));
|
|
30583
30656
|
}
|
|
30584
30657
|
const items = steps.map((s) => `
|
|
30585
30658
|
<div class="step">
|
|
@@ -30587,17 +30660,12 @@ function renderRecommendedNextSteps(report) {
|
|
|
30587
30660
|
<span class="title">${escapeHtml2(s.title)}</span>
|
|
30588
30661
|
<span class="rationale">${escapeHtml2(s.rationale)}</span>
|
|
30589
30662
|
</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;
|
|
30663
|
+
return section(heading, `<div class="steps">${items}</div>`);
|
|
30597
30664
|
}
|
|
30598
30665
|
function renderActionCards(actions, audience) {
|
|
30599
30666
|
const isClient = audience === "client";
|
|
30600
|
-
|
|
30667
|
+
const copy = isClient ? REPORT_SECTION_COPY["client-action-plan"] : REPORT_SECTION_COPY["agency-action-plan"];
|
|
30668
|
+
if (actions.length === 0) return renderEmpty(copy.empty);
|
|
30601
30669
|
return `<div class="action-card-grid">
|
|
30602
30670
|
${actions.map((action, idx) => {
|
|
30603
30671
|
const tone = reportActionTone(action);
|
|
@@ -30605,17 +30673,17 @@ function renderActionCards(actions, audience) {
|
|
|
30605
30673
|
const evidence = action.evidence.length > 0 ? `<ul>${action.evidence.map((item2) => `<li>${escapeHtml2(item2)}</li>`).join("")}</ul>` : "";
|
|
30606
30674
|
const proof = renderProofChips(action.evidence.length > 0 ? action.evidence : action.why, 3);
|
|
30607
30675
|
const details = why || evidence ? `<details class="action-details">
|
|
30608
|
-
<summary>${
|
|
30609
|
-
${why ? `<div><strong>${
|
|
30610
|
-
${evidence ? `<div><strong>${
|
|
30676
|
+
<summary>${copy.detailsSummary}</summary>
|
|
30677
|
+
${why ? `<div><strong>${copy.whyLabel}</strong>${why}</div>` : ""}
|
|
30678
|
+
${evidence ? `<div><strong>${copy.evidenceLabel}</strong>${evidence}</div>` : ""}
|
|
30611
30679
|
</details>` : "";
|
|
30612
|
-
const horizonLabel =
|
|
30613
|
-
const confidenceLabel =
|
|
30680
|
+
const horizonLabel = reportActionHorizonBadge(audience, action.horizon);
|
|
30681
|
+
const confidenceLabel = reportActionConfidenceBadge(audience, action.confidence);
|
|
30614
30682
|
const categoryBadge = isClient ? "" : `<span class="badge tone-neutral">${escapeHtml2(reportActionCategoryLabel(action.category))}</span>`;
|
|
30615
|
-
const successLabel =
|
|
30683
|
+
const successLabel = copy.successLabel;
|
|
30616
30684
|
return `<article class="action-card">
|
|
30617
30685
|
<div class="action-head">
|
|
30618
|
-
<div class="action-rank" title="${
|
|
30686
|
+
<div class="action-rank" title="${copy.rankTitle}">${idx + 1}</div>
|
|
30619
30687
|
<div>
|
|
30620
30688
|
<div class="action-meta">
|
|
30621
30689
|
<span class="badge tone-${tone}">${escapeHtml2(horizonLabel)}</span>
|
|
@@ -30634,71 +30702,68 @@ function renderActionCards(actions, audience) {
|
|
|
30634
30702
|
</div>`;
|
|
30635
30703
|
}
|
|
30636
30704
|
function renderAudienceActionPlan(report, audience) {
|
|
30637
|
-
const
|
|
30638
|
-
const
|
|
30705
|
+
const id = audience === "client" ? ReportSectionIds["client-action-plan"] : ReportSectionIds["agency-action-plan"];
|
|
30706
|
+
const copy = REPORT_SECTION_COPY[id];
|
|
30639
30707
|
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)
|
|
30708
|
+
{ id, eyebrow: copy.eyebrow, title: copy.title, intro: copy.intro },
|
|
30709
|
+
renderActionCards(reportAudienceActions(report, audience), audience)
|
|
30647
30710
|
);
|
|
30648
30711
|
}
|
|
30649
30712
|
function renderClientSummary(report) {
|
|
30650
30713
|
const s = report.executiveSummary;
|
|
30651
30714
|
const sc = report.citationScorecard;
|
|
30715
|
+
const copy = REPORT_SECTION_COPY["client-summary"];
|
|
30652
30716
|
const totalQ = s.totalQueryCount ?? 0;
|
|
30653
30717
|
const heroNumber = totalQ > 0 ? `${s.mentionRate}%` : "\u2014";
|
|
30654
|
-
const heroSentence =
|
|
30655
|
-
const trend =
|
|
30718
|
+
const heroSentence = reportClientHeroSentence(totalQ, s.mentionedQueryCount);
|
|
30719
|
+
const trend = reportClientTrendCopy(report.whatsChanged.mentionRate);
|
|
30656
30720
|
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
|
|
30657
30721
|
const hero = `<div class="client-hero">
|
|
30658
|
-
<div class="client-hero-eyebrow"
|
|
30722
|
+
<div class="client-hero-eyebrow">${copy.heroEyebrow}</div>
|
|
30659
30723
|
<div class="client-hero-number">${heroNumber}</div>
|
|
30660
30724
|
<p class="client-hero-sentence">${escapeHtml2(heroSentence)}</p>
|
|
30661
30725
|
${heroTrend}
|
|
30662
30726
|
</div>`;
|
|
30663
|
-
const providerSubtitle = sc.providers
|
|
30727
|
+
const providerSubtitle = reportClientProvidersSubtitle(sc.providers, s.queryCount);
|
|
30664
30728
|
const tiles = `<div class="client-metric-grid">
|
|
30665
30729
|
<div class="client-metric-tile">
|
|
30666
|
-
<div class="label"
|
|
30730
|
+
<div class="label">${copy.tiles.mentioned}</div>
|
|
30667
30731
|
<div class="value">${s.mentionRate}%</div>
|
|
30668
|
-
<div class="subtitle">${
|
|
30732
|
+
<div class="subtitle">${reportClientMentionedSubtitle(s.mentionedQueryCount, totalQ)}</div>
|
|
30669
30733
|
</div>
|
|
30670
30734
|
<div class="client-metric-tile">
|
|
30671
|
-
<div class="label"
|
|
30735
|
+
<div class="label">${copy.tiles.cited}</div>
|
|
30672
30736
|
<div class="value">${s.citationRate}%</div>
|
|
30673
|
-
<div class="subtitle">${
|
|
30737
|
+
<div class="subtitle">${reportClientCitedSubtitle(s.citedQueryCount, totalQ)}</div>
|
|
30674
30738
|
</div>
|
|
30675
30739
|
<div class="client-metric-tile">
|
|
30676
|
-
<div class="label"
|
|
30740
|
+
<div class="label">${copy.tiles.providers}</div>
|
|
30677
30741
|
<div class="value">${formatNumber(s.providerCount)}</div>
|
|
30678
30742
|
<div class="subtitle">${escapeHtml2(providerSubtitle)}</div>
|
|
30679
30743
|
</div>
|
|
30680
30744
|
</div>`;
|
|
30745
|
+
const { lead, mention, link, closing } = copy.explainer;
|
|
30681
30746
|
const explainer = `<div class="client-explainer">
|
|
30682
|
-
<strong
|
|
30683
|
-
|
|
30684
|
-
|
|
30685
|
-
|
|
30747
|
+
<strong>${lead}</strong>
|
|
30748
|
+
${mention.article} <span class="term">${mention.term}</span> ${mention.definition}
|
|
30749
|
+
${link.article} <span class="term">${link.term}</span> ${link.definition}
|
|
30750
|
+
${closing}
|
|
30686
30751
|
</div>`;
|
|
30687
30752
|
const questions = sc.queries.length > 0 ? `<div class="client-card">
|
|
30688
|
-
<h3
|
|
30689
|
-
<p class="card-subtitle"
|
|
30753
|
+
<h3>${copy.queriesHeading}</h3>
|
|
30754
|
+
<p class="card-subtitle">${reportClientQueriesSubtitle(sc.queries.length)}</p>
|
|
30690
30755
|
<ol class="client-questions-list">
|
|
30691
30756
|
${sc.queries.map((q, i) => `<li><span class="qnum">${String(i + 1).padStart(2, "0")}</span><span>"${escapeHtml2(q)}"</span></li>`).join("")}
|
|
30692
30757
|
</ol>
|
|
30693
30758
|
</div>` : "";
|
|
30694
30759
|
const providerBars = sc.providerRates.length > 0 ? `<div class="client-card">
|
|
30695
|
-
<h3
|
|
30696
|
-
<p class="card-subtitle"
|
|
30760
|
+
<h3>${copy.providerBarsHeading}</h3>
|
|
30761
|
+
<p class="card-subtitle">${copy.providerBarsSubtitle}</p>
|
|
30697
30762
|
<div class="client-bar-list">
|
|
30698
30763
|
${sc.providerRates.map((r) => {
|
|
30699
30764
|
const pct = Math.max(r.mentionRate, 1.5);
|
|
30700
30765
|
return `<div class="client-bar-row">
|
|
30701
|
-
<span class="bar-label">${escapeHtml2(
|
|
30766
|
+
<span class="bar-label">${escapeHtml2(reportProviderDisplayName(r.provider))}</span>
|
|
30702
30767
|
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
30703
30768
|
<span class="bar-value">${r.mentionRate}% <span class="bar-value-sub">(${r.mentionedCount}/${r.totalCount})</span></span>
|
|
30704
30769
|
</div>`;
|
|
@@ -30715,34 +30780,35 @@ function renderClientEvidenceSummary(report) {
|
|
|
30715
30780
|
const opportunities = dedupeReportOpportunities(report).slice(0, 5);
|
|
30716
30781
|
const aiMax = ai.length > 0 ? Math.max(...ai.map((d) => d.count)) : 0;
|
|
30717
30782
|
const gscMax = gsc ? Math.max(...gsc.topQueries.slice(0, 5).map((q) => q.impressions), 1) : 0;
|
|
30783
|
+
const copy = REPORT_SECTION_COPY["client-evidence-summary"];
|
|
30718
30784
|
const cards = [];
|
|
30719
30785
|
if (ai.length > 0) {
|
|
30720
30786
|
cards.push(`<div class="client-card">
|
|
30721
|
-
<h3
|
|
30722
|
-
<p class="card-subtitle"
|
|
30787
|
+
<h3>${copy.sources.heading}</h3>
|
|
30788
|
+
<p class="card-subtitle">${copy.sources.subtitle}</p>
|
|
30723
30789
|
<div class="client-bar-list">
|
|
30724
30790
|
${ai.map((d) => {
|
|
30725
30791
|
const pct = aiMax > 0 ? Math.max(d.count / aiMax * 100, 1.5) : 0;
|
|
30726
|
-
const label = escapeHtml2(d.domain) + (d.isCompetitor ?
|
|
30792
|
+
const label = escapeHtml2(d.domain) + (d.isCompetitor ? ` <span style="color:${COLORS.textFaint};font-size:11px;">${copy.sources.competitorTag}</span>` : "");
|
|
30727
30793
|
return `<div class="client-bar-row">
|
|
30728
30794
|
<span class="bar-label">${label}</span>
|
|
30729
30795
|
<div class="bar-track"><div class="bar-fill bar-fill-neutral" style="width:${pct}%"></div></div>
|
|
30730
|
-
<span class="bar-value">${
|
|
30796
|
+
<span class="bar-value">${reportClientSourceCount(d.count)}</span>
|
|
30731
30797
|
</div>`;
|
|
30732
30798
|
}).join("")}
|
|
30733
30799
|
</div>
|
|
30734
30800
|
</div>`);
|
|
30735
30801
|
}
|
|
30736
30802
|
if (indexing) {
|
|
30737
|
-
const tone = indexing.indexedPct
|
|
30803
|
+
const tone = reportClientIndexingTone(indexing.indexedPct);
|
|
30738
30804
|
const fillPct = Math.max(indexing.indexedPct, 1.5);
|
|
30739
30805
|
cards.push(`<div class="client-card">
|
|
30740
|
-
<h3
|
|
30741
|
-
<p class="card-subtitle"
|
|
30806
|
+
<h3>${copy.indexing.heading}</h3>
|
|
30807
|
+
<p class="card-subtitle">${copy.indexing.subtitle}</p>
|
|
30742
30808
|
<div class="client-progress-number tone-${tone}">${indexing.indexedPct}%</div>
|
|
30743
|
-
<div style="font-size:12px;color:${COLORS.textMuted};">${
|
|
30809
|
+
<div style="font-size:12px;color:${COLORS.textMuted};">${reportClientIndexedPages(indexing.indexed, indexing.total)}</div>
|
|
30744
30810
|
<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> ${
|
|
30811
|
+
<p style="margin:0;font-size:12px;color:${COLORS.textMuted};"><strong style="color:${COLORS.text};">${formatNumber(indexing.notIndexed)}</strong> ${reportClientNotIndexedTail(indexing.notIndexed)}</p>
|
|
30746
30812
|
</div>`);
|
|
30747
30813
|
}
|
|
30748
30814
|
if (gsc) {
|
|
@@ -30753,53 +30819,54 @@ function renderClientEvidenceSummary(report) {
|
|
|
30753
30819
|
return `<div class="client-bar-row">
|
|
30754
30820
|
<span class="bar-label">${escapeHtml2(q.query)}</span>
|
|
30755
30821
|
<div class="bar-track"><div class="bar-fill bar-fill-sky" style="width:${pct}%"></div></div>
|
|
30756
|
-
<span class="bar-value">${
|
|
30822
|
+
<span class="bar-value">${reportClientSearchCount(q.impressions)}</span>
|
|
30757
30823
|
</div>`;
|
|
30758
30824
|
}).join("")}
|
|
30759
30825
|
</div>` : "";
|
|
30760
30826
|
cards.push(`<div class="client-card">
|
|
30761
|
-
<h3
|
|
30762
|
-
<p class="card-subtitle"
|
|
30827
|
+
<h3>${copy.search.heading}</h3>
|
|
30828
|
+
<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
30829
|
${queryRows2}
|
|
30764
30830
|
</div>`);
|
|
30765
30831
|
}
|
|
30766
30832
|
if (opportunities.length > 0) {
|
|
30767
30833
|
cards.push(`<div class="client-card">
|
|
30768
|
-
<h3
|
|
30769
|
-
<p class="card-subtitle"
|
|
30834
|
+
<h3>${copy.opportunities.heading}</h3>
|
|
30835
|
+
<p class="card-subtitle">${copy.opportunities.subtitle}</p>
|
|
30770
30836
|
<ul class="client-opportunity-list">
|
|
30771
30837
|
${opportunities.map((o) => `<li>
|
|
30772
30838
|
<div class="op-query">${escapeHtml2(o.query)}</div>
|
|
30773
|
-
<div class="op-action">${escapeHtml2(contentActionLabel(o.action))}${o.winnabilityClass === "ceded" ?
|
|
30839
|
+
<div class="op-action">${escapeHtml2(contentActionLabel(o.action))}${o.winnabilityClass === "ceded" ? ` <span class="badge tone-caution">${copy.opportunities.cededTag}</span>` : ""}</div>
|
|
30774
30840
|
</li>`).join("")}
|
|
30775
30841
|
</ul>
|
|
30776
30842
|
</div>`);
|
|
30777
30843
|
}
|
|
30778
30844
|
return section(
|
|
30779
30845
|
{
|
|
30780
|
-
id: "client-evidence-summary",
|
|
30781
|
-
eyebrow:
|
|
30782
|
-
title:
|
|
30783
|
-
intro:
|
|
30846
|
+
id: ReportSectionIds["client-evidence-summary"],
|
|
30847
|
+
eyebrow: copy.eyebrow,
|
|
30848
|
+
title: copy.title,
|
|
30849
|
+
intro: copy.intro
|
|
30784
30850
|
},
|
|
30785
|
-
cards.length > 0 ? `<div class="client-evidence-grid">${cards.join("")}</div>` : renderEmpty(
|
|
30851
|
+
cards.length > 0 ? `<div class="client-evidence-grid">${cards.join("")}</div>` : renderEmpty(copy.empty)
|
|
30786
30852
|
);
|
|
30787
30853
|
}
|
|
30788
30854
|
function renderAgencyDiagnostics(report) {
|
|
30789
|
-
const
|
|
30855
|
+
const copy = REPORT_SECTION_COPY["agency-diagnostics"];
|
|
30856
|
+
const diagnostics = report.agencyDiagnostics.diagnostics.filter((d) => d.title !== copy.hiddenTitle);
|
|
30790
30857
|
const body = diagnostics.length > 0 ? `<div class="diagnostics-grid">
|
|
30791
30858
|
${diagnostics.map((d) => `<div class="diagnostic-card tone-${d.severity}">
|
|
30792
30859
|
<h3>${escapeHtml2(d.title)}</h3>
|
|
30793
30860
|
<p>${escapeHtml2(d.detail)}</p>
|
|
30794
30861
|
${renderProofChips(d.evidence, 3)}
|
|
30795
30862
|
</div>`).join("")}
|
|
30796
|
-
</div>` : renderEmpty(
|
|
30863
|
+
</div>` : renderEmpty(copy.empty);
|
|
30797
30864
|
return section(
|
|
30798
30865
|
{
|
|
30799
|
-
id: "agency-diagnostics",
|
|
30800
|
-
eyebrow:
|
|
30801
|
-
title:
|
|
30802
|
-
intro:
|
|
30866
|
+
id: ReportSectionIds["agency-diagnostics"],
|
|
30867
|
+
eyebrow: copy.eyebrow,
|
|
30868
|
+
title: copy.title,
|
|
30869
|
+
intro: copy.intro
|
|
30803
30870
|
},
|
|
30804
30871
|
body
|
|
30805
30872
|
);
|
|
@@ -30855,9 +30922,9 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30855
30922
|
<body>
|
|
30856
30923
|
<div class="container">
|
|
30857
30924
|
<header class="header">
|
|
30858
|
-
<div class="eyebrow"
|
|
30925
|
+
<div class="eyebrow">${REPORT_HEADER_COPY.eyebrow}</div>
|
|
30859
30926
|
<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
|
|
30927
|
+
<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
30928
|
</header>
|
|
30862
30929
|
${sections}
|
|
30863
30930
|
<footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml2(formatIsoDate(report.meta.generatedAt))}</footer>
|
|
@@ -30871,8 +30938,8 @@ function renderReportVisibility(visibility) {
|
|
|
30871
30938
|
const historyPopulations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.trend.some((point) => point.answerCount > 0));
|
|
30872
30939
|
const rateCell = (rate2) => `<td><strong>${escapeHtml2(reportVisibilityRate(rate2))}</strong><p class="muted">${escapeHtml2(reportVisibilityEvidence(rate2))}</p></td>`;
|
|
30873
30940
|
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>`;
|
|
30941
|
+
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>`;
|
|
30942
|
+
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
30943
|
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
30944
|
}
|
|
30878
30945
|
|
|
@@ -31402,7 +31469,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31402
31469
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
31403
31470
|
and29(
|
|
31404
31471
|
eq33(trafficSources.projectId, projectId),
|
|
31405
|
-
|
|
31472
|
+
ne5(trafficSources.status, TrafficSourceStatuses.archived)
|
|
31406
31473
|
)
|
|
31407
31474
|
).all();
|
|
31408
31475
|
if (sourceRows.length === 0) return null;
|
|
@@ -31420,7 +31487,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31420
31487
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31421
31488
|
eq33(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
31422
31489
|
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31423
|
-
exclusiveEnd ?
|
|
31490
|
+
exclusiveEnd ? lt6(crawlerEventsHourly.tsHour, windowEndIso) : lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31424
31491
|
)
|
|
31425
31492
|
).get()?.total ?? 0
|
|
31426
31493
|
);
|
|
@@ -31428,9 +31495,9 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31428
31495
|
db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31429
31496
|
and29(
|
|
31430
31497
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31431
|
-
|
|
31498
|
+
ne5(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
31432
31499
|
gte8(crawlerEventsHourly.tsHour, windowStartIso),
|
|
31433
|
-
exclusiveEnd ?
|
|
31500
|
+
exclusiveEnd ? lt6(crawlerEventsHourly.tsHour, windowEndIso) : lte6(crawlerEventsHourly.tsHour, windowEndIso)
|
|
31434
31501
|
)
|
|
31435
31502
|
).get()?.total ?? 0
|
|
31436
31503
|
);
|
|
@@ -31444,7 +31511,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31444
31511
|
eq33(aiReferralEventsHourly.projectId, projectId),
|
|
31445
31512
|
countableReferralCondition(),
|
|
31446
31513
|
gte8(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
31447
|
-
exclusiveEnd ?
|
|
31514
|
+
exclusiveEnd ? lt6(aiReferralEventsHourly.tsHour, windowEndIso) : lte6(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
31448
31515
|
)
|
|
31449
31516
|
).get();
|
|
31450
31517
|
return aiReferralClassCounts(Number(row?.total ?? 0), Number(row?.paid ?? 0), Number(row?.organic ?? 0));
|
|
@@ -31454,7 +31521,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31454
31521
|
and29(
|
|
31455
31522
|
eq33(aiUserFetchEventsHourly.projectId, projectId),
|
|
31456
31523
|
gte8(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
31457
|
-
exclusiveEnd ?
|
|
31524
|
+
exclusiveEnd ? lt6(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte6(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
31458
31525
|
)
|
|
31459
31526
|
).get()?.total ?? 0
|
|
31460
31527
|
);
|
|
@@ -31495,7 +31562,7 @@ function buildServerActivity(db, projectId, windowDays2) {
|
|
|
31495
31562
|
and29(
|
|
31496
31563
|
eq33(crawlerEventsHourly.projectId, projectId),
|
|
31497
31564
|
gte8(crawlerEventsHourly.tsHour, priorStart),
|
|
31498
|
-
|
|
31565
|
+
lt6(crawlerEventsHourly.tsHour, headlineStart)
|
|
31499
31566
|
)
|
|
31500
31567
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
31501
31568
|
const referralByOperatorRows = db.select({
|
|
@@ -31786,7 +31853,7 @@ function buildInsightList(db, projectId, locationFilter) {
|
|
|
31786
31853
|
and29(
|
|
31787
31854
|
eq33(runs.projectId, projectId),
|
|
31788
31855
|
eq33(runs.kind, RunKinds["answer-visibility"]),
|
|
31789
|
-
|
|
31856
|
+
or6(eq33(runs.status, RunStatuses.completed), eq33(runs.status, RunStatuses.partial)),
|
|
31790
31857
|
notProbeRun()
|
|
31791
31858
|
)
|
|
31792
31859
|
).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 +31999,6 @@ function buildProviderLocationHandling(providersInRun) {
|
|
|
31932
31999
|
};
|
|
31933
32000
|
});
|
|
31934
32001
|
}
|
|
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
32002
|
function contentActionVerb(action) {
|
|
31941
32003
|
switch (action) {
|
|
31942
32004
|
case "create":
|
|
@@ -31954,7 +32016,7 @@ function confidenceFromEvidence(count4) {
|
|
|
31954
32016
|
if (count4 >= 1) return "medium";
|
|
31955
32017
|
return "low";
|
|
31956
32018
|
}
|
|
31957
|
-
function
|
|
32019
|
+
function actionAudienceMatches(action, audience) {
|
|
31958
32020
|
return action.audience === "both" || action.audience === audience;
|
|
31959
32021
|
}
|
|
31960
32022
|
function configuredCompetitorDomainsForActionPlan(db, projectId, projectCompetitorDomains) {
|
|
@@ -32065,10 +32127,10 @@ function buildReportActionPlan(input) {
|
|
|
32065
32127
|
if (input.gsc && (input.gsc.trackedButNoGsc.length > 0 || input.gsc.gscButNotTracked.length > 0)) {
|
|
32066
32128
|
const evidence = [];
|
|
32067
32129
|
if (input.gsc.trackedButNoGsc.length > 0) {
|
|
32068
|
-
evidence.push(`Tracked with no GSC demand: ${
|
|
32130
|
+
evidence.push(`Tracked with no GSC demand: ${reportCompactList(input.gsc.trackedButNoGsc)}`);
|
|
32069
32131
|
}
|
|
32070
32132
|
if (input.gsc.gscButNotTracked.length > 0) {
|
|
32071
|
-
evidence.push(`Search demand not tracked in AEO: ${
|
|
32133
|
+
evidence.push(`Search demand not tracked in AEO: ${reportCompactList(input.gsc.gscButNotTracked)}`);
|
|
32072
32134
|
}
|
|
32073
32135
|
actions.push({
|
|
32074
32136
|
audience: "agency",
|
|
@@ -32100,8 +32162,8 @@ function buildReportActionPlan(input) {
|
|
|
32100
32162
|
"They are stronger evidence than a generic topic list because the model is already retrieving competing content."
|
|
32101
32163
|
],
|
|
32102
32164
|
evidence: [
|
|
32103
|
-
`"${topGap.query}" missed at ${
|
|
32104
|
-
`Cited competitors: ${
|
|
32165
|
+
`"${topGap.query}" missed at ${formatWholePercent(topGap.missRate)} with ${topGap.competitorCount} competitor${topGap.competitorCount === 1 ? "" : "s"} cited`,
|
|
32166
|
+
`Cited competitors: ${reportCompactList(topGap.competitorDomains)}`
|
|
32105
32167
|
],
|
|
32106
32168
|
successMetric: "The top content-gap query moves from missed to cited or mentioned after the recommended content work ships.",
|
|
32107
32169
|
confidence: topGap.competitorCount >= 2 ? "high" : "medium"
|
|
@@ -32111,10 +32173,10 @@ function buildReportActionPlan(input) {
|
|
|
32111
32173
|
const ignoredProviders = input.providerLocationHandling.filter((p) => p.treatment === "ignored" || p.treatment === "browser-geo").map((p) => p.provider);
|
|
32112
32174
|
const evidence = [
|
|
32113
32175
|
`Current report location: ${input.reportLocation.label}`,
|
|
32114
|
-
`Other configured locations: ${
|
|
32176
|
+
`Other configured locations: ${reportCompactList(input.reportLocation.otherConfiguredLabels)}`
|
|
32115
32177
|
];
|
|
32116
32178
|
if (ignoredProviders.length > 0) {
|
|
32117
|
-
evidence.push(`Providers with weak/indirect location handling: ${
|
|
32179
|
+
evidence.push(`Providers with weak/indirect location handling: ${reportCompactList(ignoredProviders)}`);
|
|
32118
32180
|
}
|
|
32119
32181
|
actions.push({
|
|
32120
32182
|
audience: "agency",
|
|
@@ -32186,7 +32248,7 @@ function buildClientSummary(reportLike) {
|
|
|
32186
32248
|
return {
|
|
32187
32249
|
headline,
|
|
32188
32250
|
overview,
|
|
32189
|
-
actionItems: reportLike.actionPlan.filter((a) =>
|
|
32251
|
+
actionItems: reportLike.actionPlan.filter((a) => actionAudienceMatches(a, "client")).slice(0, 3),
|
|
32190
32252
|
confidenceNotes
|
|
32191
32253
|
};
|
|
32192
32254
|
}
|
|
@@ -32211,8 +32273,8 @@ function buildAgencyDiagnostics(input) {
|
|
|
32211
32273
|
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
32274
|
severity: input.gsc.trackedButNoGsc.length > 0 || input.gsc.gscButNotTracked.length > 0 ? "caution" : "positive",
|
|
32213
32275
|
evidence: [
|
|
32214
|
-
...input.gsc.trackedButNoGsc.length > 0 ? [`Tracked with no GSC demand: ${
|
|
32215
|
-
...input.gsc.gscButNotTracked.length > 0 ? [`GSC queries not tracked in AEO: ${
|
|
32276
|
+
...input.gsc.trackedButNoGsc.length > 0 ? [`Tracked with no GSC demand: ${reportCompactList(input.gsc.trackedButNoGsc)}`] : [],
|
|
32277
|
+
...input.gsc.gscButNotTracked.length > 0 ? [`GSC queries not tracked in AEO: ${reportCompactList(input.gsc.gscButNotTracked)}`] : []
|
|
32216
32278
|
]
|
|
32217
32279
|
});
|
|
32218
32280
|
}
|
|
@@ -32237,7 +32299,7 @@ function buildAgencyDiagnostics(input) {
|
|
|
32237
32299
|
evidence: input.contentOpportunities.slice(0, 3).map((o) => `${o.query}: ${o.action} (${Math.round(o.score)})`)
|
|
32238
32300
|
});
|
|
32239
32301
|
return {
|
|
32240
|
-
priorities: input.actionPlan.filter((a) =>
|
|
32302
|
+
priorities: input.actionPlan.filter((a) => actionAudienceMatches(a, "agency")).slice(0, 6),
|
|
32241
32303
|
diagnostics
|
|
32242
32304
|
};
|
|
32243
32305
|
}
|
|
@@ -32357,7 +32419,7 @@ function buildWhatsChanged(input) {
|
|
|
32357
32419
|
function buildProjectReport(db, projectName, periodDays) {
|
|
32358
32420
|
const project = resolveProject(db, projectName);
|
|
32359
32421
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
32360
|
-
const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" });
|
|
32422
|
+
const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" }, { includeComparison: false });
|
|
32361
32423
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
32362
32424
|
const historyWindow = { from: new Date(Date.parse(generatedAt) - periodDays * 864e5).toISOString(), to: generatedAt };
|
|
32363
32425
|
const visibility = {
|
|
@@ -32668,10 +32730,10 @@ async function reportRoutes(app) {
|
|
|
32668
32730
|
}
|
|
32669
32731
|
|
|
32670
32732
|
// ../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
|
|
32733
|
+
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
32734
|
|
|
32673
32735
|
// ../api-routes/src/ga-measurement-analysis.ts
|
|
32674
|
-
import { and as and30, eq as eq34, gte as gte9, lte as lte7, or as
|
|
32736
|
+
import { and as and30, eq as eq34, gte as gte9, lte as lte7, or as or7, sql as sql12 } from "drizzle-orm";
|
|
32675
32737
|
var UNKNOWN_LANDING_PAGE = "(not set)";
|
|
32676
32738
|
function addDays(date, offset) {
|
|
32677
32739
|
const value = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
@@ -32845,7 +32907,7 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32845
32907
|
if (parsedHostScope.data === "marketing") {
|
|
32846
32908
|
const lowerHost = sql12`lower(${hostColumn})`;
|
|
32847
32909
|
const normalizedHost2 = sql12`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
|
|
32848
|
-
conditions.push(
|
|
32910
|
+
conditions.push(or7(...marketingHosts.flatMap((host) => [
|
|
32849
32911
|
sql12`${normalizedHost2} = ${host}`,
|
|
32850
32912
|
sql12`${normalizedHost2} like ${`%.${host}`}`
|
|
32851
32913
|
])));
|
|
@@ -32853,14 +32915,14 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32853
32915
|
if (pathPrefix && pathPrefix !== "/") {
|
|
32854
32916
|
const rawPath = sql12`case when instr(${rawPathColumn}, '?') = 0 then ${rawPathColumn} else substr(${rawPathColumn}, 1, instr(${rawPathColumn}, '?') - 1) end`;
|
|
32855
32917
|
const landingPath = sql12`case when trim(coalesce(${normalizedPathColumn}, '')) = '' then ${rawPath} else ${normalizedPathColumn} end`;
|
|
32856
|
-
conditions.push(
|
|
32918
|
+
conditions.push(or7(sql12`${landingPath} = ${pathPrefix}`, sql12`${landingPath} like ${`${pathPrefix}/%`}`));
|
|
32857
32919
|
}
|
|
32858
32920
|
return conditions;
|
|
32859
32921
|
};
|
|
32860
32922
|
const acquisitionScope = scopedConditions(gaAcquisitionDaily.hostName, gaAcquisitionDaily.landingPageNormalized, gaAcquisitionDaily.landingPage);
|
|
32861
32923
|
const leadLandingScope = scopedConditions(gaLeadEventsDaily.hostName, gaLeadEventsDaily.landingPageNormalized, gaLeadEventsDaily.landingPage);
|
|
32862
32924
|
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),
|
|
32925
|
+
const leadAnchor = db.select({ date: sql12`max(${gaLeadEventsDaily.date})` }).from(gaLeadEventsDaily).where(and30(eq34(gaLeadEventsDaily.projectId, project.id), or7(
|
|
32864
32926
|
eq34(gaLeadEventsDaily.attributionScope, "channel"),
|
|
32865
32927
|
and30(eq34(gaLeadEventsDaily.attributionScope, "landing-page"), ...leadLandingScope)
|
|
32866
32928
|
))).get()?.date ?? null;
|
|
@@ -32881,7 +32943,7 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
|
|
|
32881
32943
|
date: sql12`min(${gaDailyTotals.date})`
|
|
32882
32944
|
}).from(gaDailyTotals).where(and30(
|
|
32883
32945
|
eq34(gaDailyTotals.projectId, project.id),
|
|
32884
|
-
|
|
32946
|
+
or7(
|
|
32885
32947
|
sql12`${gaDailyTotals.engagementRate} is not null`,
|
|
32886
32948
|
sql12`${gaDailyTotals.newUsers} is not null`
|
|
32887
32949
|
)
|
|
@@ -33228,7 +33290,7 @@ function buildOrganicEvidence(db, projectName, periodDays) {
|
|
|
33228
33290
|
observedDays: sql13`count(distinct ${gaAcquisitionDaily.date})`
|
|
33229
33291
|
}).from(gaAcquisitionDaily).where(and31(
|
|
33230
33292
|
eq35(gaAcquisitionDaily.projectId, project.id),
|
|
33231
|
-
|
|
33293
|
+
or8(...normalizedMarketingHosts.map((candidate) => sql13`lower(trim(${gaAcquisitionDaily.hostName})) = ${candidate} or lower(trim(${gaAcquisitionDaily.hostName})) like ${`%.${candidate}`}`))
|
|
33232
33294
|
)).get()) : null;
|
|
33233
33295
|
const gaCoverage = nativeAcquisitionActive ? nativeCoverage : legacyGaCoverage;
|
|
33234
33296
|
const asOfDate = gscCoverage?.endDate ?? gaCoverage?.endDate ?? null;
|
|
@@ -34570,7 +34632,7 @@ async function resultsExportRoutes(app) {
|
|
|
34570
34632
|
}
|
|
34571
34633
|
|
|
34572
34634
|
// ../api-routes/src/composites.ts
|
|
34573
|
-
import { eq as eq39, and as and35, desc as desc17, sql as sql14, like as like3, or as
|
|
34635
|
+
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
34636
|
var TOP_INSIGHT_LIMIT = 5;
|
|
34575
34637
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
34576
34638
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -34767,7 +34829,7 @@ async function compositeRoutes(app) {
|
|
|
34767
34829
|
}).from(querySnapshots).innerJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(
|
|
34768
34830
|
and35(
|
|
34769
34831
|
eq39(queries.projectId, project.id),
|
|
34770
|
-
|
|
34832
|
+
or9(
|
|
34771
34833
|
sql14`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
34772
34834
|
sql14`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
34773
34835
|
sql14`${querySnapshots.citedUrls} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -34779,7 +34841,7 @@ async function compositeRoutes(app) {
|
|
|
34779
34841
|
const insightMatches = app.db.select().from(insights).where(
|
|
34780
34842
|
and35(
|
|
34781
34843
|
eq39(insights.projectId, project.id),
|
|
34782
|
-
|
|
34844
|
+
or9(
|
|
34783
34845
|
like3(insights.title, pattern),
|
|
34784
34846
|
like3(insights.query, pattern),
|
|
34785
34847
|
sql14`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -42611,7 +42673,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42611
42673
|
method: "get",
|
|
42612
42674
|
path: "/api/v1/projects/{name}/agent/transcript",
|
|
42613
42675
|
summary: "Get the rolling Aero transcript for this project",
|
|
42614
|
-
description: "Returns the full message history of the project-scoped Aero session plus the persisted model provider/id and last-updated timestamp. Empty messages array when the project has no session yet.",
|
|
42676
|
+
description: "Returns the full message history of the project-scoped Aero session plus the persisted model provider/id and last-updated timestamp. Empty messages array when the project has no session yet. Administrator-only: a signed-in viewer, and any API key narrower than the install (read-only, or scoped to a single project), are refused with 403. There is one Aero session per project, so this is the operator conversation rather than metadata about it.",
|
|
42615
42677
|
tags: ["agent"],
|
|
42616
42678
|
parameters: [nameParameter],
|
|
42617
42679
|
responses: {
|
|
@@ -42624,7 +42686,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42624
42686
|
method: "delete",
|
|
42625
42687
|
path: "/api/v1/projects/{name}/agent/transcript",
|
|
42626
42688
|
summary: "Reset the Aero transcript + queued follow-ups",
|
|
42627
|
-
description: "Evicts any live Agent instance, clears the persisted messages and follow_up_queue. A subsequent prompt starts a fresh session.",
|
|
42689
|
+
description: "Evicts any live Agent instance, clears the persisted messages and follow_up_queue. A subsequent prompt starts a fresh session. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403.",
|
|
42628
42690
|
tags: ["agent"],
|
|
42629
42691
|
parameters: [nameParameter],
|
|
42630
42692
|
responses: {
|
|
@@ -42637,7 +42699,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42637
42699
|
method: "get",
|
|
42638
42700
|
path: "/api/v1/projects/{name}/agent/memory",
|
|
42639
42701
|
summary: "List durable Aero memory entries for a project",
|
|
42640
|
-
description: "Returns the project-scoped agent_memory rows newest-first. Includes both operator-authored notes (source `user`/`aero`) and LLM-authored compaction summaries (source `compaction`, key prefix `compaction:`). The N most-recent rows are also injected into the system prompt at every new session start.",
|
|
42702
|
+
description: "Returns the project-scoped agent_memory rows newest-first. Includes both operator-authored notes (source `user`/`aero`) and LLM-authored compaction summaries (source `compaction`, key prefix `compaction:`). The N most-recent rows are also injected into the system prompt at every new session start. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403.",
|
|
42641
42703
|
tags: ["agent"],
|
|
42642
42704
|
parameters: [nameParameter],
|
|
42643
42705
|
responses: {
|
|
@@ -42650,7 +42712,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42650
42712
|
method: "put",
|
|
42651
42713
|
path: "/api/v1/projects/{name}/agent/memory",
|
|
42652
42714
|
summary: "Upsert a durable Aero memory entry",
|
|
42653
|
-
description: "Creates or replaces a project-scoped note (max 2 KB, max 128-char key). Same key replaces the prior value. Keys with the reserved `compaction:` prefix are rejected \u2014 that namespace is owned by transcript compaction.",
|
|
42715
|
+
description: "Creates or replaces a project-scoped note (max 2 KB, max 128-char key). Same key replaces the prior value. Keys with the reserved `compaction:` prefix are rejected \u2014 that namespace is owned by transcript compaction. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403.",
|
|
42654
42716
|
tags: ["agent"],
|
|
42655
42717
|
parameters: [nameParameter],
|
|
42656
42718
|
requestBody: {
|
|
@@ -42679,7 +42741,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42679
42741
|
method: "delete",
|
|
42680
42742
|
path: "/api/v1/projects/{name}/agent/memory",
|
|
42681
42743
|
summary: "Delete a durable Aero memory entry",
|
|
42682
|
-
description: "Removes a single project-scoped note by key. Returns `status: missing` (non-error) when the key never existed. Keys with the reserved `compaction:` prefix are rejected \u2014 those notes are pruned automatically.",
|
|
42744
|
+
description: "Removes a single project-scoped note by key. Returns `status: missing` (non-error) when the key never existed. Keys with the reserved `compaction:` prefix are rejected \u2014 those notes are pruned automatically. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403.",
|
|
42683
42745
|
tags: ["agent"],
|
|
42684
42746
|
parameters: [nameParameter],
|
|
42685
42747
|
requestBody: {
|
|
@@ -42707,7 +42769,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42707
42769
|
method: "get",
|
|
42708
42770
|
path: "/api/v1/projects/{name}/agent/providers",
|
|
42709
42771
|
summary: "List the LLM providers Aero can route to",
|
|
42710
|
-
description: "Returns every provider Aero knows about with its default model, whether a usable API key is configured, and where the key resolved from (`config` | `env`). `defaultProvider` is the one Aero auto-picks when a caller omits `provider` on the prompt endpoint. Path is project-scoped for auth symmetry; the response does not vary per project today.",
|
|
42772
|
+
description: "Returns every provider Aero knows about with its default model, whether a usable API key is configured, and where the key resolved from (`config` | `env`). `defaultProvider` is the one Aero auto-picks when a caller omits `provider` on the prompt endpoint. Path is project-scoped for auth symmetry; the response does not vary per project today. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403, because each entry names a default model and which providers are configured is itself administrator knowledge.",
|
|
42711
42773
|
tags: ["agent"],
|
|
42712
42774
|
parameters: [nameParameter],
|
|
42713
42775
|
responses: {
|
|
@@ -42719,7 +42781,7 @@ var canonryLocalRouteCatalog = [
|
|
|
42719
42781
|
method: "post",
|
|
42720
42782
|
path: "/api/v1/projects/{name}/agent/prompt",
|
|
42721
42783
|
summary: "Send a prompt to Aero and stream events back as SSE",
|
|
42722
|
-
description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). Body field `profile` accepts "default" | "ads-operator"; omitted keeps the default full Canonry operator surface. The CLI passes "all" to keep write tools available.',
|
|
42784
|
+
description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). Body field `profile` accepts "default" | "ads-operator"; omitted keeps the default full Canonry operator surface. The CLI passes "all" to keep write tools available. Administrator-only: a signed-in viewer, and any API key narrower than the install, are refused with 403. Tools execute with the install root key, so this route is an operator surface regardless of the scope requested.',
|
|
42723
42785
|
tags: ["agent"],
|
|
42724
42786
|
parameters: [nameParameter],
|
|
42725
42787
|
requestBody: {
|
|
@@ -47297,7 +47359,7 @@ async function googleRoutes(app, opts) {
|
|
|
47297
47359
|
|
|
47298
47360
|
// ../api-routes/src/google-marketing.ts
|
|
47299
47361
|
import crypto33 from "crypto";
|
|
47300
|
-
import { and as and38, desc as desc20, eq as eq45, isNull as isNull6, lt as
|
|
47362
|
+
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
47363
|
import { z as z4 } from "zod";
|
|
47302
47364
|
var snapshotPageQuerySchema = z4.object({
|
|
47303
47365
|
limit: z4.coerce.number().int().min(1).max(GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX).optional(),
|
|
@@ -48512,7 +48574,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48512
48574
|
const existing = tx.select().from(runs).where(and38(
|
|
48513
48575
|
eq45(runs.projectId, project.id),
|
|
48514
48576
|
eq45(runs.kind, RunKinds["google-ads-sync"]),
|
|
48515
|
-
|
|
48577
|
+
or10(eq45(runs.status, RunStatuses.queued), eq45(runs.status, RunStatuses.running))
|
|
48516
48578
|
)).orderBy(desc20(runs.createdAt)).get();
|
|
48517
48579
|
if (existing) {
|
|
48518
48580
|
run = existing;
|
|
@@ -48548,7 +48610,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48548
48610
|
const existing = tx.select().from(runs).where(and38(
|
|
48549
48611
|
eq45(runs.projectId, project.id),
|
|
48550
48612
|
eq45(runs.kind, RunKinds["gtm-sync"]),
|
|
48551
|
-
|
|
48613
|
+
or10(eq45(runs.status, RunStatuses.queued), eq45(runs.status, RunStatuses.running))
|
|
48552
48614
|
)).orderBy(desc20(runs.createdAt)).get();
|
|
48553
48615
|
if (existing) {
|
|
48554
48616
|
run = existing;
|
|
@@ -48578,9 +48640,9 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48578
48640
|
const cursor = decodeSnapshotCursor(query.cursor);
|
|
48579
48641
|
const conditions = [eq45(googleAdsRawSnapshots.projectId, project.id)];
|
|
48580
48642
|
if (cursor) {
|
|
48581
|
-
conditions.push(
|
|
48582
|
-
|
|
48583
|
-
and38(eq45(googleAdsRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48643
|
+
conditions.push(or10(
|
|
48644
|
+
lt7(googleAdsRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48645
|
+
and38(eq45(googleAdsRawSnapshots.capturedAt, cursor.capturedAt), lt7(googleAdsRawSnapshots.id, cursor.id))
|
|
48584
48646
|
));
|
|
48585
48647
|
}
|
|
48586
48648
|
const limit = query.limit ?? 50;
|
|
@@ -48610,9 +48672,9 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48610
48672
|
const cursor = decodeSnapshotCursor(query.cursor);
|
|
48611
48673
|
const conditions = [eq45(gtmRawSnapshots.projectId, project.id)];
|
|
48612
48674
|
if (cursor) {
|
|
48613
|
-
conditions.push(
|
|
48614
|
-
|
|
48615
|
-
and38(eq45(gtmRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48675
|
+
conditions.push(or10(
|
|
48676
|
+
lt7(gtmRawSnapshots.capturedAt, cursor.capturedAt),
|
|
48677
|
+
and38(eq45(gtmRawSnapshots.capturedAt, cursor.capturedAt), lt7(gtmRawSnapshots.id, cursor.id))
|
|
48616
48678
|
));
|
|
48617
48679
|
}
|
|
48618
48680
|
const limit = query.limit ?? 50;
|
|
@@ -48798,7 +48860,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48798
48860
|
const duplicate = app.db.select({ id: conversionTrackingContracts.id }).from(conversionTrackingContracts).where(and38(
|
|
48799
48861
|
eq45(conversionTrackingContracts.projectId, project.id),
|
|
48800
48862
|
eq45(conversionTrackingContracts.name, body.name),
|
|
48801
|
-
|
|
48863
|
+
ne6(conversionTrackingContracts.id, existing.id)
|
|
48802
48864
|
)).get();
|
|
48803
48865
|
if (duplicate) throw alreadyExists("Conversion tracking contract", body.name);
|
|
48804
48866
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -48887,7 +48949,7 @@ async function googleMarketingRoutes(app, opts) {
|
|
|
48887
48949
|
|
|
48888
48950
|
// ../api-routes/src/ads.ts
|
|
48889
48951
|
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
|
|
48952
|
+
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
48953
|
|
|
48892
48954
|
// ../api-routes/src/ads-activation.ts
|
|
48893
48955
|
import crypto34 from "crypto";
|
|
@@ -50273,7 +50335,7 @@ async function executeApprovedAdsActivation(dependencies, request) {
|
|
|
50273
50335
|
|
|
50274
50336
|
// ../api-routes/src/ads-activation-routes.ts
|
|
50275
50337
|
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
|
|
50338
|
+
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
50339
|
var MAX_ACTIVATION_GRANT_LIFETIME_MS = 24 * 60 * 60 * 1e3;
|
|
50278
50340
|
var ACTIVATION_LEASE_MS = 5 * 6e4;
|
|
50279
50341
|
var ACTIVATION_REVOCATION_SETTLEMENT_MS = ACTIVATION_LEASE_MS;
|
|
@@ -50399,7 +50461,7 @@ function createActivationStore(app, request) {
|
|
|
50399
50461
|
}).where(and39(
|
|
50400
50462
|
eq46(adsOperations.id, existing.id),
|
|
50401
50463
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
50402
|
-
|
|
50464
|
+
or11(
|
|
50403
50465
|
isNull7(adsOperations.leaseOwner),
|
|
50404
50466
|
eq46(adsOperations.leaseOwner, input.leaseOwner),
|
|
50405
50467
|
isNull7(adsOperations.leaseExpiresAt),
|
|
@@ -50817,29 +50879,29 @@ async function executeActivationGrant(app, opts, request, project, executorApiKe
|
|
|
50817
50879
|
return activationResponse(opts, result.grant, result.operation);
|
|
50818
50880
|
}
|
|
50819
50881
|
function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLease) {
|
|
50820
|
-
const leaseAvailable =
|
|
50882
|
+
const leaseAvailable = or11(
|
|
50821
50883
|
isNull7(adsOperations.leaseOwner),
|
|
50822
50884
|
isNull7(adsOperations.leaseExpiresAt),
|
|
50823
50885
|
lte10(adsOperations.leaseExpiresAt, nowIso)
|
|
50824
50886
|
);
|
|
50825
|
-
const campaignHasMutationEvidence =
|
|
50887
|
+
const campaignHasMutationEvidence = or11(
|
|
50826
50888
|
eq46(adsOperationSteps.state, AdsOperationStepStates.executing),
|
|
50827
50889
|
eq46(adsOperationSteps.state, AdsOperationStepStates.active),
|
|
50828
50890
|
eq46(adsOperationSteps.state, AdsOperationStepStates.rollback_executing),
|
|
50829
50891
|
eq46(adsOperationSteps.state, AdsOperationStepStates.rollback_failed),
|
|
50830
50892
|
eq46(adsOperationSteps.state, AdsOperationStepStates.unknown)
|
|
50831
50893
|
);
|
|
50832
|
-
const eligibleState = allowLiveRevocationLease ?
|
|
50894
|
+
const eligibleState = allowLiveRevocationLease ? or11(
|
|
50833
50895
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50834
50896
|
and39(
|
|
50835
50897
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
50836
|
-
|
|
50898
|
+
or11(
|
|
50837
50899
|
isNotNull3(adsActivationGrants.revocationRequestedAt),
|
|
50838
50900
|
and39(leaseAvailable, campaignHasMutationEvidence)
|
|
50839
50901
|
)
|
|
50840
50902
|
)
|
|
50841
50903
|
) : and39(
|
|
50842
|
-
|
|
50904
|
+
or11(
|
|
50843
50905
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50844
50906
|
eq46(adsOperations.state, AdsOperationStates.pending)
|
|
50845
50907
|
),
|
|
@@ -50983,7 +51045,7 @@ async function containUrgentAdsActivationCampaignsOnce(app, opts) {
|
|
|
50983
51045
|
lastReconciledAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
50984
51046
|
}).where(and39(
|
|
50985
51047
|
eq46(adsOperations.id, operation.id),
|
|
50986
|
-
|
|
51048
|
+
or11(
|
|
50987
51049
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
50988
51050
|
eq46(adsOperations.state, AdsOperationStates.pending)
|
|
50989
51051
|
)
|
|
@@ -51009,11 +51071,11 @@ async function sweepStaleAdsActivationsOnce(app, opts) {
|
|
|
51009
51071
|
updatedAt: nowIso
|
|
51010
51072
|
}).where(and39(
|
|
51011
51073
|
eq46(adsOperations.id, operation.id),
|
|
51012
|
-
|
|
51074
|
+
or11(
|
|
51013
51075
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
51014
51076
|
and39(
|
|
51015
51077
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
51016
|
-
|
|
51078
|
+
or11(
|
|
51017
51079
|
isNull7(adsOperations.leaseOwner),
|
|
51018
51080
|
isNull7(adsOperations.leaseExpiresAt),
|
|
51019
51081
|
lte10(adsOperations.leaseExpiresAt, nowIso)
|
|
@@ -51046,11 +51108,11 @@ async function sweepStaleAdsActivationsOnce(app, opts) {
|
|
|
51046
51108
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
51047
51109
|
app.db.update(adsOperations).set({ updatedAt: failedAt }).where(and39(
|
|
51048
51110
|
eq46(adsOperations.id, operation.id),
|
|
51049
|
-
|
|
51111
|
+
or11(
|
|
51050
51112
|
eq46(adsOperations.state, AdsOperationStates.unknown),
|
|
51051
51113
|
and39(
|
|
51052
51114
|
eq46(adsOperations.state, AdsOperationStates.pending),
|
|
51053
|
-
|
|
51115
|
+
or11(
|
|
51054
51116
|
isNull7(adsOperations.leaseOwner),
|
|
51055
51117
|
isNull7(adsOperations.leaseExpiresAt),
|
|
51056
51118
|
lte10(adsOperations.leaseExpiresAt, failedAt)
|
|
@@ -52236,7 +52298,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
|
|
|
52236
52298
|
enforceBackoff ? lte11(adsOperations.updatedAt, unknownCutoff) : void 0
|
|
52237
52299
|
) : and40(
|
|
52238
52300
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52239
|
-
|
|
52301
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52240
52302
|
);
|
|
52241
52303
|
const claimed = app.db.update(adsOperations).set({
|
|
52242
52304
|
state: AdsOperationStates.reconciling,
|
|
@@ -52248,7 +52310,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
|
|
|
52248
52310
|
eq47(adsOperations.id, row.id),
|
|
52249
52311
|
eq47(adsOperations.projectId, row.projectId),
|
|
52250
52312
|
eq47(adsOperations.reconcileAttempts, row.reconcileAttempts),
|
|
52251
|
-
|
|
52313
|
+
lt8(adsOperations.reconcileAttempts, policy.maxAttempts),
|
|
52252
52314
|
claimableState
|
|
52253
52315
|
)).returning({ id: adsOperations.id }).all();
|
|
52254
52316
|
if (claimed.length === 0) return void 0;
|
|
@@ -52558,28 +52620,28 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52558
52620
|
);
|
|
52559
52621
|
}
|
|
52560
52622
|
);
|
|
52561
|
-
const retryableStateWhere =
|
|
52623
|
+
const retryableStateWhere = or12(
|
|
52562
52624
|
and40(
|
|
52563
52625
|
eq47(adsOperations.state, AdsOperationStates.pending),
|
|
52564
52626
|
lte11(adsOperations.updatedAt, pendingCutoff)
|
|
52565
52627
|
),
|
|
52566
52628
|
and40(
|
|
52567
52629
|
eq47(adsOperations.state, AdsOperationStates.unknown),
|
|
52568
|
-
|
|
52630
|
+
or12(...unknownRetryWindows)
|
|
52569
52631
|
),
|
|
52570
52632
|
and40(
|
|
52571
52633
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52572
|
-
|
|
52634
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52573
52635
|
)
|
|
52574
52636
|
);
|
|
52575
52637
|
const staleReconcileWhere = and40(
|
|
52576
|
-
|
|
52577
|
-
|
|
52578
|
-
|
|
52638
|
+
lt8(adsOperations.reconcileAttempts, config.policy.maxAttempts),
|
|
52639
|
+
ne7(adsOperations.kind, AdsOperationKinds.campaign_tree_activate),
|
|
52640
|
+
or12(
|
|
52579
52641
|
eq47(adsOperations.reconcileStrategy, AdsReconcileStrategies.known_entity),
|
|
52580
52642
|
and40(
|
|
52581
52643
|
eq47(adsOperations.reconcileStrategy, AdsReconcileStrategies.create_fingerprint),
|
|
52582
|
-
|
|
52644
|
+
or12(
|
|
52583
52645
|
isNotNull4(adsOperations.entityId),
|
|
52584
52646
|
inArray20(adsOperations.state, [
|
|
52585
52647
|
AdsOperationStates.pending,
|
|
@@ -52591,9 +52653,9 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52591
52653
|
retryableStateWhere
|
|
52592
52654
|
);
|
|
52593
52655
|
const exhaustedRows = app.db.select().from(adsOperations).where(and40(
|
|
52594
|
-
|
|
52656
|
+
ne7(adsOperations.kind, AdsOperationKinds.campaign_tree_activate),
|
|
52595
52657
|
gte12(adsOperations.reconcileAttempts, config.policy.maxAttempts),
|
|
52596
|
-
|
|
52658
|
+
or12(
|
|
52597
52659
|
and40(
|
|
52598
52660
|
eq47(adsOperations.state, AdsOperationStates.pending),
|
|
52599
52661
|
lte11(adsOperations.updatedAt, pendingCutoff)
|
|
@@ -52601,12 +52663,12 @@ async function sweepAdsOperationsOnce(app, opts, config) {
|
|
|
52601
52663
|
eq47(adsOperations.state, AdsOperationStates.unknown),
|
|
52602
52664
|
and40(
|
|
52603
52665
|
eq47(adsOperations.state, AdsOperationStates.reconciling),
|
|
52604
|
-
|
|
52666
|
+
or12(isNull8(adsOperations.leaseExpiresAt), lte11(adsOperations.leaseExpiresAt, nowIso))
|
|
52605
52667
|
)
|
|
52606
52668
|
),
|
|
52607
|
-
|
|
52669
|
+
or12(
|
|
52608
52670
|
isNull8(adsOperations.errorCode),
|
|
52609
|
-
|
|
52671
|
+
ne7(adsOperations.errorCode, ADS_RECONCILIATION_QUARANTINED)
|
|
52610
52672
|
)
|
|
52611
52673
|
)).orderBy(asc9(adsOperations.updatedAt)).limit(config.batchSize).all();
|
|
52612
52674
|
for (const row of exhaustedRows) {
|
|
@@ -52952,7 +53014,7 @@ async function adsRoutes(app, opts) {
|
|
|
52952
53014
|
const rows = app.db.select().from(adsOperations).where(and40(
|
|
52953
53015
|
eq47(adsOperations.projectId, project.id),
|
|
52954
53016
|
inArray20(adsOperations.state, parsed.data.state),
|
|
52955
|
-
cursor ?
|
|
53017
|
+
cursor ? or12(
|
|
52956
53018
|
gt3(adsOperations.createdAt, cursor.createdAt),
|
|
52957
53019
|
and40(
|
|
52958
53020
|
eq47(adsOperations.createdAt, cursor.createdAt),
|
|
@@ -58084,7 +58146,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
58084
58146
|
}
|
|
58085
58147
|
|
|
58086
58148
|
// ../api-routes/src/backlinks-filter.ts
|
|
58087
|
-
import { and as and44, ne as
|
|
58149
|
+
import { and as and44, ne as ne8, notLike } from "drizzle-orm";
|
|
58088
58150
|
var BACKLINK_FILTER_PATTERNS = [
|
|
58089
58151
|
"*.google.com",
|
|
58090
58152
|
"*.googleusercontent.com",
|
|
@@ -58101,10 +58163,10 @@ function backlinkCrawlerExclusionClause() {
|
|
|
58101
58163
|
for (const pattern of BACKLINK_FILTER_PATTERNS) {
|
|
58102
58164
|
if (pattern.startsWith("*.")) {
|
|
58103
58165
|
const suffix = pattern.slice(2);
|
|
58104
|
-
conditions.push(
|
|
58166
|
+
conditions.push(ne8(backlinkDomains.linkingDomain, suffix));
|
|
58105
58167
|
conditions.push(notLike(backlinkDomains.linkingDomain, `%.${suffix}`));
|
|
58106
58168
|
} else {
|
|
58107
|
-
conditions.push(
|
|
58169
|
+
conditions.push(ne8(backlinkDomains.linkingDomain, pattern));
|
|
58108
58170
|
}
|
|
58109
58171
|
}
|
|
58110
58172
|
const combined = and44(...conditions);
|
|
@@ -75138,10 +75200,10 @@ async function ackCloudflareQueueMessages(client, options) {
|
|
|
75138
75200
|
|
|
75139
75201
|
// ../api-routes/src/traffic-event-ingest.ts
|
|
75140
75202
|
import crypto43 from "crypto";
|
|
75141
|
-
import { and as and47, eq as eq53, gte as gte13, lt as
|
|
75203
|
+
import { and as and47, eq as eq53, gte as gte13, lt as lt10, lte as lte12, sql as sql22 } from "drizzle-orm";
|
|
75142
75204
|
|
|
75143
75205
|
// ../api-routes/src/raw-event-sample-retention.ts
|
|
75144
|
-
import { and as and46, eq as eq52, lt as
|
|
75206
|
+
import { and as and46, eq as eq52, lt as lt9, sql as sql21 } from "drizzle-orm";
|
|
75145
75207
|
var RAW_EVENT_SAMPLE_RETENTION_MS = 30 * 24 * 60 * 6e4;
|
|
75146
75208
|
var RAW_EVENT_SAMPLE_RETENTION_SWEEP_INTERVAL_MS = 24 * 60 * 6e4;
|
|
75147
75209
|
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 +75218,7 @@ function deleteExpiredRawEventSamples(db, cutoff, sourceId) {
|
|
|
75156
75218
|
db.delete(rawEventSamples).where(and46(
|
|
75157
75219
|
sourceScope,
|
|
75158
75220
|
sql21`${rawEventSamples.ts} GLOB ${CANONICAL_UTC_TIMESTAMP_GLOB}`,
|
|
75159
|
-
|
|
75221
|
+
lt9(rawEventSamples.ts, cutoff)
|
|
75160
75222
|
)).run();
|
|
75161
75223
|
db.delete(rawEventSamples).where(and46(
|
|
75162
75224
|
sourceScope,
|
|
@@ -75418,7 +75480,7 @@ function writeTrafficEventBatch(opts) {
|
|
|
75418
75480
|
const existing = tx.select({ total: sql22`COUNT(*)` }).from(rawEventSamples).where(and47(
|
|
75419
75481
|
eq53(rawEventSamples.sourceId, opts.sourceId),
|
|
75420
75482
|
gte13(rawEventSamples.ts, hour.start),
|
|
75421
|
-
|
|
75483
|
+
lt10(rawEventSamples.ts, hour.end)
|
|
75422
75484
|
)).get();
|
|
75423
75485
|
remaining = Math.max(0, hourlySampleLimit - Number(existing?.total ?? 0));
|
|
75424
75486
|
}
|
|
@@ -75475,7 +75537,7 @@ function writeTrafficEventBatch(opts) {
|
|
|
75475
75537
|
}
|
|
75476
75538
|
|
|
75477
75539
|
// ../api-routes/src/traffic-sync-lease.ts
|
|
75478
|
-
import { and as and48, eq as eq54, isNull as isNull9, lte as lte13, or as
|
|
75540
|
+
import { and as and48, eq as eq54, isNull as isNull9, lte as lte13, or as or13 } from "drizzle-orm";
|
|
75479
75541
|
function leaseExpiresAt(now, ttlMs) {
|
|
75480
75542
|
const nowMs = Date.parse(now);
|
|
75481
75543
|
if (!Number.isFinite(nowMs)) throw new RangeError("Traffic sync lease now must be an ISO instant");
|
|
@@ -75490,7 +75552,7 @@ function tryClaimTrafficSyncLease(input) {
|
|
|
75490
75552
|
syncLeaseExpiresAt: expiresAt
|
|
75491
75553
|
}).where(and48(
|
|
75492
75554
|
eq54(trafficSources.id, input.sourceId),
|
|
75493
|
-
|
|
75555
|
+
or13(
|
|
75494
75556
|
isNull9(trafficSources.syncLeaseOwner),
|
|
75495
75557
|
lte13(trafficSources.syncLeaseExpiresAt, input.now),
|
|
75496
75558
|
eq54(trafficSources.syncLeaseOwner, input.owner)
|
|
@@ -80678,6 +80740,14 @@ var agentProvidersConfiguredCheck = {
|
|
|
80678
80740
|
scope: CheckScopes.global,
|
|
80679
80741
|
title: "Agent provider keys",
|
|
80680
80742
|
run: (ctx) => {
|
|
80743
|
+
if (ctx.callerIsInstanceAdministrator === false) {
|
|
80744
|
+
return {
|
|
80745
|
+
status: CheckStatuses.skipped,
|
|
80746
|
+
code: "agent-providers.restricted",
|
|
80747
|
+
summary: "Agent provider configuration is visible to administrators only.",
|
|
80748
|
+
remediation: null
|
|
80749
|
+
};
|
|
80750
|
+
}
|
|
80681
80751
|
const summary2 = ctx.getAgentProviderSummary?.();
|
|
80682
80752
|
if (!summary2) {
|
|
80683
80753
|
return {
|
|
@@ -80794,7 +80864,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
80794
80864
|
];
|
|
80795
80865
|
|
|
80796
80866
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
80797
|
-
import { and as and53, eq as eq62, gte as gte15, inArray as inArray23, ne as
|
|
80867
|
+
import { and as and53, eq as eq62, gte as gte15, inArray as inArray23, ne as ne9, sql as sql24 } from "drizzle-orm";
|
|
80798
80868
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
80799
80869
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
80800
80870
|
function isCloudflareDirectPush(source) {
|
|
@@ -80835,7 +80905,7 @@ function loadProbes(ctx) {
|
|
|
80835
80905
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
80836
80906
|
and53(
|
|
80837
80907
|
eq62(trafficSources.projectId, ctx.project.id),
|
|
80838
|
-
|
|
80908
|
+
ne9(trafficSources.status, TrafficSourceStatuses.archived)
|
|
80839
80909
|
)
|
|
80840
80910
|
).all();
|
|
80841
80911
|
return rows.map((r) => ({
|
|
@@ -81944,6 +82014,7 @@ async function doctorRoutes(app, opts) {
|
|
|
81944
82014
|
redirectUri,
|
|
81945
82015
|
providerSummary: opts.providerSummary,
|
|
81946
82016
|
getAgentProviderSummary: opts.getAgentProviderSummary,
|
|
82017
|
+
callerIsInstanceAdministrator: isInstanceAdministrator(request),
|
|
81947
82018
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
81948
82019
|
runtimeStatePaths: opts.runtimeStatePaths,
|
|
81949
82020
|
bundledSkills: opts.bundledSkills,
|
|
@@ -81974,6 +82045,7 @@ async function doctorRoutes(app, opts) {
|
|
|
81974
82045
|
redirectUri,
|
|
81975
82046
|
providerSummary: opts.providerSummary,
|
|
81976
82047
|
getAgentProviderSummary: opts.getAgentProviderSummary,
|
|
82048
|
+
callerIsInstanceAdministrator: isInstanceAdministrator(request),
|
|
81977
82049
|
trafficSourceValidators: opts.trafficSourceValidators,
|
|
81978
82050
|
runtimeStatePaths: opts.runtimeStatePaths,
|
|
81979
82051
|
bundledSkills: opts.bundledSkills,
|
|
@@ -81987,7 +82059,7 @@ async function doctorRoutes(app, opts) {
|
|
|
81987
82059
|
|
|
81988
82060
|
// ../api-routes/src/discovery/routes.ts
|
|
81989
82061
|
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
|
|
82062
|
+
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
82063
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
81992
82064
|
async function discoveryRoutes(app, opts) {
|
|
81993
82065
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -82037,7 +82109,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
82037
82109
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
82038
82110
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
82039
82111
|
// upgrade, never a wrong reuse.
|
|
82040
|
-
locations.length === 0 ?
|
|
82112
|
+
locations.length === 0 ? or14(isNull10(discoverySessions.locations), eq63(discoverySessions.locations, locations)) : eq63(discoverySessions.locations, locations),
|
|
82041
82113
|
// Seed provider set is identity: a different phrasing distribution
|
|
82042
82114
|
// must never reuse another set's session. Null (= Gemini-only
|
|
82043
82115
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
@@ -82667,7 +82739,7 @@ function dedupeStrings(input) {
|
|
|
82667
82739
|
|
|
82668
82740
|
// ../api-routes/src/technical-aeo.ts
|
|
82669
82741
|
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
|
|
82742
|
+
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
82743
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
82672
82744
|
var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
|
|
82673
82745
|
...SiteCrawlFetchedStates,
|
|
@@ -83063,7 +83135,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83063
83135
|
return emptyScore(project.name);
|
|
83064
83136
|
}
|
|
83065
83137
|
const snap = latest.snap;
|
|
83066
|
-
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq65(siteAuditSnapshots.runId, runs.id)).where(and55(...baseFilters,
|
|
83138
|
+
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
83139
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
83068
83140
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
83069
83141
|
return {
|
|
@@ -83309,7 +83381,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83309
83381
|
eq65(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
|
|
83310
83382
|
eq65(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
|
|
83311
83383
|
eq65(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
|
|
83312
|
-
)).where(and55(...graphScope,
|
|
83384
|
+
)).where(and55(...graphScope, lt11(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc11(siteCrawlGraphNodes.sampleRank)).all();
|
|
83313
83385
|
const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
|
|
83314
83386
|
...row,
|
|
83315
83387
|
healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey })
|
|
@@ -83327,18 +83399,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83327
83399
|
eq65(graphSourceNode.runId, siteCrawlGraphEdges.runId),
|
|
83328
83400
|
eq65(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
83329
83401
|
eq65(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
|
|
83330
|
-
|
|
83402
|
+
lt11(graphSourceNode.sampleRank, maxNodes)
|
|
83331
83403
|
)).innerJoin(graphTargetNode, and55(
|
|
83332
83404
|
eq65(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
|
|
83333
83405
|
eq65(graphTargetNode.runId, siteCrawlGraphEdges.runId),
|
|
83334
83406
|
eq65(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
83335
83407
|
eq65(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
|
|
83336
|
-
|
|
83408
|
+
lt11(graphTargetNode.sampleRank, maxNodes)
|
|
83337
83409
|
)).where(and55(
|
|
83338
83410
|
eq65(siteCrawlGraphEdges.projectId, project.id),
|
|
83339
83411
|
eq65(siteCrawlGraphEdges.runId, snapshot.runId),
|
|
83340
83412
|
eq65(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
|
|
83341
|
-
|
|
83413
|
+
lt11(siteCrawlGraphEdges.sampleRank, maxEdges),
|
|
83342
83414
|
graphLinkKindFilter
|
|
83343
83415
|
)).orderBy(asc11(siteCrawlGraphEdges.sampleRank)).all();
|
|
83344
83416
|
const totalNodes = persistedLayout.totalNodes;
|
|
@@ -83437,7 +83509,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83437
83509
|
eq65(siteCrawlEdges.attemptId, scope.attemptId),
|
|
83438
83510
|
eq65(siteCrawlEdges.internal, true),
|
|
83439
83511
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
83440
|
-
|
|
83512
|
+
or15(
|
|
83441
83513
|
inArray25(siteCrawlEdges.sourceNodeKey, frontier),
|
|
83442
83514
|
inArray25(siteCrawlEdges.targetNodeKey, frontier)
|
|
83443
83515
|
)
|
|
@@ -83684,11 +83756,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83684
83756
|
if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
|
|
83685
83757
|
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
83758
|
...completeSnapshotFilters,
|
|
83687
|
-
|
|
83688
|
-
|
|
83759
|
+
or15(
|
|
83760
|
+
lt11(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
83689
83761
|
and55(
|
|
83690
83762
|
eq65(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
83691
|
-
|
|
83763
|
+
lt11(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
|
|
83692
83764
|
)
|
|
83693
83765
|
)
|
|
83694
83766
|
)).orderBy(desc28(siteCrawlSnapshots.createdAt), desc28(siteCrawlSnapshots.runId)).limit(1).get() : void 0;
|
|
@@ -84281,8 +84353,8 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84281
84353
|
eq65(siteCrawlEdges.internal, true),
|
|
84282
84354
|
linkKindFilter(linkKind)
|
|
84283
84355
|
];
|
|
84284
|
-
const inboundMatch = request.query.nodeKey && request.query.url ?
|
|
84285
|
-
const outboundMatch = request.query.nodeKey && request.query.url ?
|
|
84356
|
+
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);
|
|
84357
|
+
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
84358
|
const limit = parseBoundedLimit(request.query.limit, 50, 100);
|
|
84287
84359
|
const inboundRows = app.db.select().from(siteCrawlEdges).where(and55(...scope, inboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
84288
84360
|
const outboundRows = app.db.select().from(siteCrawlEdges).where(and55(...scope, outboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
@@ -84484,7 +84556,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84484
84556
|
}).from(siteCrawlPages).where(and55(
|
|
84485
84557
|
auditedWhere,
|
|
84486
84558
|
isNotNull5(siteCrawlPages.auditScore),
|
|
84487
|
-
|
|
84559
|
+
lt11(siteCrawlPages.auditScore, 70)
|
|
84488
84560
|
)).orderBy(asc11(siteCrawlPages.auditScore), asc11(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
|
|
84489
84561
|
const examples = candidates.flatMap((candidate) => {
|
|
84490
84562
|
if (candidate.auditScore == null) return [];
|
|
@@ -84577,7 +84649,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84577
84649
|
// ../api-routes/src/research.ts
|
|
84578
84650
|
import crypto49 from "crypto";
|
|
84579
84651
|
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
|
|
84652
|
+
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
84653
|
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
84654
|
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
84583
84655
|
async function researchRoutes(app, opts) {
|
|
@@ -84638,7 +84710,7 @@ async function researchRoutes(app, opts) {
|
|
|
84638
84710
|
const used = tx.select({ value: count3() }).from(researchRuns).where(and56(
|
|
84639
84711
|
eq66(researchRuns.projectId, project.id),
|
|
84640
84712
|
gte17(researchRuns.createdAt, start),
|
|
84641
|
-
|
|
84713
|
+
lt12(researchRuns.createdAt, end),
|
|
84642
84714
|
sql26`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
84643
84715
|
)).get()?.value ?? 0;
|
|
84644
84716
|
if (used >= viewerDailyRunLimit) {
|
|
@@ -84693,7 +84765,7 @@ async function researchRoutes(app, opts) {
|
|
|
84693
84765
|
const used = tx.select({ value: count3() }).from(researchRuns).where(and56(
|
|
84694
84766
|
eq66(researchRuns.projectId, project.id),
|
|
84695
84767
|
gte17(researchRuns.createdAt, start),
|
|
84696
|
-
|
|
84768
|
+
lt12(researchRuns.createdAt, end),
|
|
84697
84769
|
sql26`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
84698
84770
|
)).get()?.value ?? 0;
|
|
84699
84771
|
if (used + prepared.length > viewerDailyRunLimit) throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
@@ -84747,7 +84819,7 @@ async function researchRoutes(app, opts) {
|
|
|
84747
84819
|
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
84748
84820
|
const rows = app.db.select().from(researchRuns).where(and56(
|
|
84749
84821
|
eq66(researchRuns.projectId, project.id),
|
|
84750
|
-
cursor ?
|
|
84822
|
+
cursor ? or16(lt12(researchRuns.createdAt, cursor.createdAt), and56(eq66(researchRuns.createdAt, cursor.createdAt), lt12(researchRuns.id, cursor.id))) : void 0
|
|
84751
84823
|
)).orderBy(desc29(researchRuns.createdAt), desc29(researchRuns.id)).limit(limit + 1).all();
|
|
84752
84824
|
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
84753
84825
|
const last = runs2.at(-1);
|
|
@@ -85924,11 +85996,11 @@ function analyzableRunPredicates(projectId) {
|
|
|
85924
85996
|
eq68(runs.projectId, projectId),
|
|
85925
85997
|
// Only this kind writes query_snapshots.
|
|
85926
85998
|
eq68(runs.kind, RunKinds["answer-visibility"]),
|
|
85927
|
-
|
|
85999
|
+
or17(eq68(runs.status, RunStatuses.completed), eq68(runs.status, RunStatuses.partial)),
|
|
85928
86000
|
// Defensive: RunCoordinator already skips probes before analyzeAndPersist
|
|
85929
86001
|
// is called, but a future call site invoking it directly for a probe must
|
|
85930
86002
|
// still not pollute the intelligence window.
|
|
85931
|
-
|
|
86003
|
+
ne10(runs.trigger, RunTriggers.probe)
|
|
85932
86004
|
];
|
|
85933
86005
|
}
|
|
85934
86006
|
function readStoredGroundingSources(rawResponse) {
|
|
@@ -86514,9 +86586,9 @@ var IntelligenceService = class {
|
|
|
86514
86586
|
and58(
|
|
86515
86587
|
eq68(runs.projectId, projectId),
|
|
86516
86588
|
eq68(runs.kind, RunKinds["answer-visibility"]),
|
|
86517
|
-
|
|
86589
|
+
or17(eq68(runs.status, "completed"), eq68(runs.status, "partial")),
|
|
86518
86590
|
// Defensive — see top of file.
|
|
86519
|
-
|
|
86591
|
+
ne10(runs.trigger, RunTriggers.probe)
|
|
86520
86592
|
)
|
|
86521
86593
|
).orderBy(desc30(runs.createdAt), desc30(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
86522
86594
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
@@ -86727,6 +86799,7 @@ export {
|
|
|
86727
86799
|
createUserSession,
|
|
86728
86800
|
resolveUserSession,
|
|
86729
86801
|
createCredentialChecker,
|
|
86802
|
+
requireInstanceAdministrator,
|
|
86730
86803
|
hashApiKey,
|
|
86731
86804
|
registerOAuthRoutes,
|
|
86732
86805
|
registerOAuthAdminRoutes,
|