@canonry/canonry 7.65.1 → 7.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/seo-analysis.md +15 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +2 -2
- package/assets/assets/{AgentSetupPage-BDLMjn1I.js → AgentSetupPage-ORsy3QEa.js} +3 -3
- package/assets/assets/{AuditHistoryPanel-qhIB8KRK.js → AuditHistoryPanel-vTHZDbfs.js} +1 -1
- package/assets/assets/{BacklinksPage-iWrZ0-Pp.js → BacklinksPage-FqADB5sO.js} +1 -1
- package/assets/assets/{HistoryPage-BP1_GGlT.js → HistoryPage-C4l5Toqa.js} +1 -1
- package/assets/assets/MeasurementPropertyPage-x8f7xanT.js +4 -0
- package/assets/assets/ProjectPage-CG9I2-B4.js +9 -0
- package/assets/assets/{RunsPage-Ddd2oE1Y.js → RunsPage-oyhhwAPq.js} +1 -1
- package/assets/assets/{SettingsPage-D_QdYWDO.js → SettingsPage-DJc4Di1M.js} +1 -1
- package/assets/assets/SiteHealthSection-BvqFg3rG.js +2 -0
- package/assets/assets/ToneBadge-CTWpeKu8.js +60 -0
- package/assets/assets/{TrafficPage-VnBUE1S6.js → TrafficPage-DAD0FFkL.js} +2 -2
- package/assets/assets/TrafficSourceDetailPage-BBEzbawh.js +1 -0
- package/assets/assets/{account-context-CWRd3KeS.js → account-context-DMHOefRt.js} +1 -1
- package/assets/assets/{card-Cb_fM36G.js → card-Brp6fFNG.js} +1 -1
- package/assets/assets/{extract-error-message-DSIAoHTR.js → extract-error-message-5QpQCsGb.js} +1 -1
- package/assets/assets/{format-helpers-CxNDa1oa.js → format-helpers-BFM0FRjT.js} +1 -1
- package/assets/assets/index-BBNs-RhI.js +60 -0
- package/assets/assets/{query-client-DLdAMlsX.js → query-client-BMTW-xss.js} +1 -1
- package/assets/assets/react-query-DGmY6hJa.js +1 -0
- package/assets/assets/{react-sigma_core.esm.min-DEG8vFYD.js → react-sigma_core.esm.min-C0VjhgDo.js} +1 -1
- package/assets/assets/service-DW9u_0aZ.js +1 -0
- package/assets/index.html +7 -7
- package/dist/{ads-D4WQOT5K.js → ads-BXGEL4WI.js} +3 -3
- package/dist/{agent-E3IYC7OJ.js → agent-LFX7GHWD.js} +7 -7
- package/dist/{agent-ask-ZSKRPB2Z.js → agent-ask-QJV225AT.js} +3 -3
- package/dist/{agent-conversations-YHPYPQE2.js → agent-conversations-2TZDIGA6.js} +3 -3
- package/dist/{agent-memory-6OWOOD5O.js → agent-memory-XCY3ZG5C.js} +3 -3
- package/dist/{agent-providers-7KZK37UO.js → agent-providers-VS6PH55Y.js} +3 -3
- package/dist/{agent-transcript-3O7GPML6.js → agent-transcript-BUUHOTZM.js} +3 -3
- package/dist/{analytics-P2LLIUYK.js → analytics-YWJWPUNF.js} +3 -3
- package/dist/{apply-VBNNJOCT.js → apply-BMCYFBEJ.js} +3 -3
- package/dist/{backfill-F5SSURCH.js → backfill-YBOKT5LT.js} +9 -9
- package/dist/{backlinks-4RE3TNNA.js → backlinks-QNNABB4N.js} +3 -3
- package/dist/{bing-V6UN7D44.js → bing-KEMXBABP.js} +3 -3
- package/dist/{bootstrap-SHTSPAI5.js → bootstrap-X2AADK4M.js} +10 -10
- package/dist/{cdp-GDL2VFI3.js → cdp-WVYUEV4U.js} +7 -7
- package/dist/{chunk-3QKKHU7D.js → chunk-2X7DFLMD.js} +8 -8
- package/dist/{chunk-2AROR73F.js → chunk-32BTTG5N.js} +5 -5
- package/dist/{chunk-BV6NXSAH.js → chunk-37R6GTU7.js} +21 -3
- package/dist/{chunk-HPUZMBS6.js → chunk-5DMESZDF.js} +2 -2
- package/dist/{chunk-XOMUSIM7.js → chunk-6DJN7HCK.js} +83 -11
- package/dist/{chunk-SRTX6XYR.js → chunk-AFRCTLUL.js} +370 -337
- package/dist/{chunk-Z2EENWM3.js → chunk-AVISYPUU.js} +877 -478
- package/dist/{chunk-HI2HCYLB.js → chunk-CBYKQRDH.js} +2 -2
- package/dist/{chunk-S4VCXCRQ.js → chunk-EKSXR7RJ.js} +1 -1
- package/dist/{chunk-66DPEVLJ.js → chunk-G6PZEDTH.js} +3 -3
- package/dist/{chunk-GZPGDAAF.js → chunk-GTPGYFZ4.js} +2 -2
- package/dist/{chunk-Q2WZAD5W.js → chunk-HX3WLTZU.js} +1 -1
- package/dist/{chunk-JSAYC36T.js → chunk-IEBMODUI.js} +193 -47
- package/dist/{chunk-2UFA3DYM.js → chunk-KJLICF53.js} +4 -4
- package/dist/{chunk-ZPRJCSSR.js → chunk-LU3M54IY.js} +1 -1
- package/dist/{chunk-HO5W6QC7.js → chunk-MD5PHJBX.js} +2 -1
- package/dist/{chunk-SL3Y7IOX.js → chunk-MLOGEXIO.js} +1 -1
- package/dist/{chunk-LTX3TJ3E.js → chunk-MOJ2XZXK.js} +1 -1
- package/dist/{chunk-IYB3EGSD.js → chunk-NHHZQWMI.js} +1 -1
- package/dist/{chunk-H6HIJAIU.js → chunk-P74TPIWZ.js} +2 -2
- package/dist/{chunk-M3OZL6PQ.js → chunk-RRS7U6HQ.js} +4 -4
- package/dist/{chunk-X4AHYJCE.js → chunk-UHLWNU6I.js} +5 -5
- package/dist/{chunk-C2PYKUD3.js → chunk-UNCOK6DA.js} +1 -1
- package/dist/{chunk-IJKKK7F4.js → chunk-UXTNDZKM.js} +1 -1
- package/dist/{chunk-GM5ZOWBK.js → chunk-VR4MYSEH.js} +4 -4
- package/dist/{chunk-ZWZOQNLD.js → chunk-WSYLDPYF.js} +1 -1
- package/dist/{chunk-AE6HJZBU.js → chunk-WZ4LUM26.js} +3 -3
- package/dist/{chunk-DQVJB6CZ.js → chunk-XEGYSZEV.js} +99 -9
- package/dist/{chunk-F7NZEUXP.js → chunk-XEPEMON2.js} +14 -2
- package/dist/{chunk-CRKOGLKJ.js → chunk-XIA6NWIF.js} +4 -4
- package/dist/{chunk-737TOG2F.js → chunk-XMIKSCAO.js} +1 -1
- package/dist/{chunk-2Y7RN5HG.js → chunk-Y54PMWBH.js} +3 -3
- package/dist/{chunk-QNRMYDK6.js → chunk-YP743QX5.js} +1 -1
- package/dist/{chunk-XMKXXWMO.js → chunk-ZGUR4AZM.js} +2 -2
- package/dist/{citations-GO4SPZYS.js → citations-ZSI5BOGD.js} +3 -3
- package/dist/{cli-catalog-6XYQY3PI.js → cli-catalog-J2HUCGJL.js} +3 -3
- package/dist/{cli-commands-2TZKVGK6.js → cli-commands-XFMVFREG.js} +7 -7
- package/dist/cli.js +13 -13
- package/dist/{competitor-JBTKRJ6Y.js → competitor-WJDN3DLP.js} +3 -3
- package/dist/{content-LKXXQ7X4.js → content-K52YV4BK.js} +3 -3
- package/dist/{dashboard-LUA7UX6B.js → dashboard-L6RKTP35.js} +6 -6
- package/dist/{demo-I7JDZJUW.js → demo-Y7XRD4X4.js} +1 -1
- package/dist/{demo-server-GNGGWERV.js → demo-server-QNQX5CNP.js} +73 -11
- package/dist/{discover-TODAYYJ2.js → discover-UO5M7HQZ.js} +3 -3
- package/dist/{discover-eval-FWQESEYB.js → discover-eval-DN4PS6VN.js} +3 -3
- package/dist/{doctor-AXNJT2UC.js → doctor-7UYUXG7J.js} +3 -3
- package/dist/{evidence-2XGEY4ZC.js → evidence-DGKN5SGQ.js} +3 -3
- package/dist/{export-cmd-7RMYBIBZ.js → export-cmd-QAQWWIRL.js} +3 -3
- package/dist/{ga-HZDIZGCO.js → ga-4LGTEA6J.js} +3 -3
- package/dist/{gbp-SMRBWJEL.js → gbp-XOLYZ4XC.js} +3 -3
- package/dist/{google-2CN345XS.js → google-CAQDCFA6.js} +4 -4
- package/dist/{google-marketing-W7T2VTVW.js → google-marketing-HU5I3XPP.js} +1 -1
- package/dist/{health-cmd-W7OWZ4ZI.js → health-cmd-3PEB3M45.js} +3 -3
- package/dist/{history-UVNCBHTE.js → history-UGUWYPOY.js} +3 -3
- package/dist/index.d.ts +342 -161
- package/dist/index.js +24 -24
- package/dist/{init-2IWS2R7G.js → init-DEW65BLN.js} +11 -11
- package/dist/{insights-PQ52G5LU.js → insights-ZC3R4ZYG.js} +3 -3
- package/dist/intelligence-service-XRJVXZYI.js +15 -0
- package/dist/{journeys-3PP5GYVJ.js → journeys-W6F4BCXF.js} +3 -3
- package/dist/{keys-CWZUX7IQ.js → keys-ME7VRF73.js} +3 -3
- package/dist/{keyword-5OMMULLY.js → keyword-R3PLGSO5.js} +3 -3
- package/dist/{logs-3FTE42E3.js → logs-UM7CWHYB.js} +3 -3
- package/dist/{mcp-FK5DTI7I.js → mcp-BUPP5HC6.js} +2 -2
- package/dist/mcp.js +9 -9
- package/dist/{measurement-plan-ICO2BQNT.js → measurement-plan-R4KZHJII.js} +5 -5
- package/dist/{notify-ML4PYD3L.js → notify-4C26D2MY.js} +3 -3
- package/dist/{organic-evidence-4U4H3DCR.js → organic-evidence-SLIISYHW.js} +3 -3
- package/dist/{overview-NF427VFO.js → overview-JJZZOYQU.js} +4 -4
- package/dist/{project-AAO65OF7.js → project-XJH2OECP.js} +3 -3
- package/dist/{query-7ZOUQNF5.js → query-ZWAIWIYB.js} +3 -3
- package/dist/{research-UQ7AF5N3.js → research-GTVWVZBI.js} +3 -3
- package/dist/{results-export-CZCLC4ZH.js → results-export-CPAWO5AV.js} +3 -3
- package/dist/{run-4XJZW52R.js → run-RI2BMD7U.js} +4 -4
- package/dist/{schedule-D3E5FDBE.js → schedule-HDDWULBX.js} +3 -3
- package/dist/{search-DYET2IL4.js → search-TT3Q2BBP.js} +3 -3
- package/dist/{serve-QWMTR3SN.js → serve-6JYPUU24.js} +25 -25
- package/dist/{session-7CW2V5EH.js → session-DDESFV3N.js} +12 -12
- package/dist/{settings-ZISHACFX.js → settings-4AYXYJVA.js} +3 -3
- package/dist/{skills-WU6IN3NL.js → skills-UFMLMMY5.js} +2 -2
- package/dist/{snapshot-KCKQDJ2W.js → snapshot-KLWQGZB6.js} +3 -3
- package/dist/{src-WIOU5G4V.js → src-6IHGEMT2.js} +6 -2
- package/dist/{status-CYZFVMYA.js → status-HRIUPGUM.js} +4 -4
- package/dist/{technical-aeo-P5CS22AR.js → technical-aeo-HKJQHMMS.js} +3 -3
- package/dist/{telemetry-LPK5F2WU.js → telemetry-IS6KZK32.js} +4 -4
- package/dist/{tools-E3B2GZ6Q.js → tools-ZNVQFEAX.js} +11 -11
- package/dist/{traffic-5KZYXXIQ.js → traffic-PFHFGN44.js} +5 -5
- package/dist/{users-2F346HIW.js → users-BSLUSU6W.js} +3 -3
- package/dist/{visibility-stats-ITA6DNF3.js → visibility-stats-MPRNHPZW.js} +3 -3
- package/dist/{wordpress-ZHCFT6V3.js → wordpress-U5XKHURL.js} +3 -3
- package/package.json +1 -1
- package/assets/assets/MeasurementPropertyPage-dxGJ1GF9.js +0 -4
- package/assets/assets/ProjectPage-W6oYA85b.js +0 -9
- package/assets/assets/SiteHealthSection-DT20rNw-.js +0 -2
- package/assets/assets/ToneBadge-CJ6ObMRe.js +0 -60
- package/assets/assets/TrafficSourceDetailPage-CdoL5Dt9.js +0 -1
- package/assets/assets/index-D6WlecSB.js +0 -60
- package/assets/assets/react-query-BktZZtSC.js +0 -1
- package/assets/assets/service-DKP-BIBy.js +0 -1
- package/dist/intelligence-service-IAUGKMDO.js +0 -15
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
normalizeCloudflareWorkerEvent,
|
|
10
10
|
sessionizeJourneyRequests,
|
|
11
11
|
verifyRequestSignature
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HX3WLTZU.js";
|
|
13
13
|
import {
|
|
14
14
|
currentOutcomeAttribution,
|
|
15
15
|
getRequestContext,
|
|
16
16
|
registerRequestContext,
|
|
17
17
|
requestAuditIdentity,
|
|
18
18
|
sanitizeRequestContext
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-UXTNDZKM.js";
|
|
20
20
|
import {
|
|
21
21
|
SentimentIdempotencyConflict,
|
|
22
22
|
SentimentRepository,
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
conversionTrackingContracts,
|
|
50
50
|
crawlerEventsHourly,
|
|
51
51
|
crawlerVerificationManifestsHourly,
|
|
52
|
+
currentSeoTaskCondition,
|
|
53
|
+
currentSeoTasks,
|
|
52
54
|
dashboardSessions,
|
|
53
55
|
discoveryProbes,
|
|
54
56
|
discoverySessions,
|
|
@@ -154,7 +156,7 @@ import {
|
|
|
154
156
|
userSessions,
|
|
155
157
|
users,
|
|
156
158
|
writeNegativeReviewMaxStars
|
|
157
|
-
} from "./chunk-
|
|
159
|
+
} from "./chunk-6DJN7HCK.js";
|
|
158
160
|
import {
|
|
159
161
|
ADS_ACTIVATE_SCOPE,
|
|
160
162
|
ADS_ACTIVATION_ABSOLUTE_MAX_ENTITIES,
|
|
@@ -338,6 +340,8 @@ import {
|
|
|
338
340
|
SENTIMENT_ATTEMPT_PAGE_DEFAULT,
|
|
339
341
|
SENTIMENT_INTERVAL_LIMITATION,
|
|
340
342
|
SENTIMENT_QUERY_PAGE_DEFAULT,
|
|
343
|
+
SEO_MARKET_RANKING_MAX_SEARCHES,
|
|
344
|
+
SEO_PROJECT_MARKET_KEY,
|
|
341
345
|
SEO_RUN_SCOPE,
|
|
342
346
|
SEO_WRITE_SCOPE,
|
|
343
347
|
SITE_AUDIT_ISSUES_DEFAULT_LIMIT,
|
|
@@ -364,6 +368,7 @@ import {
|
|
|
364
368
|
SeoAdapterOutcomes,
|
|
365
369
|
SeoAvailability,
|
|
366
370
|
SeoCapabilities,
|
|
371
|
+
SeoMarketRankingSearchStatuses,
|
|
367
372
|
SeoRunStatuses,
|
|
368
373
|
SeoScopeKinds,
|
|
369
374
|
SeoTargetKinds,
|
|
@@ -603,7 +608,6 @@ import {
|
|
|
603
608
|
effectiveDomains,
|
|
604
609
|
emptyCitationVisibility,
|
|
605
610
|
emptySentimentCounts,
|
|
606
|
-
emptySeoResults,
|
|
607
611
|
escapeLikePattern,
|
|
608
612
|
escapeRegExp,
|
|
609
613
|
evaluateModelPointerExposure,
|
|
@@ -949,6 +953,7 @@ import {
|
|
|
949
953
|
providersFailing,
|
|
950
954
|
qualifiedNameIncludesName,
|
|
951
955
|
queryClassFilterSchema,
|
|
956
|
+
queryClassSchema,
|
|
952
957
|
queryDtoSchema,
|
|
953
958
|
queryGenerateRequestSchema,
|
|
954
959
|
queryGenerateResponseSchema,
|
|
@@ -1017,6 +1022,7 @@ import {
|
|
|
1017
1022
|
runtimeStateMissing,
|
|
1018
1023
|
schedulableRunKindSchema,
|
|
1019
1024
|
scheduleDtoSchema,
|
|
1025
|
+
scheduleEnabledAt,
|
|
1020
1026
|
scheduleExpectedUpdatedAtSchema,
|
|
1021
1027
|
scheduleUpsertRequestSchema,
|
|
1022
1028
|
scheduleVersionConflict,
|
|
@@ -1042,6 +1048,8 @@ import {
|
|
|
1042
1048
|
seoDomainIntelligenceListSchema,
|
|
1043
1049
|
seoEvidenceQuerySchema,
|
|
1044
1050
|
seoKeywordMetricsListSchema,
|
|
1051
|
+
seoMarketRankingListSchema,
|
|
1052
|
+
seoMarketRankingQuerySchema,
|
|
1045
1053
|
seoOperatorTaskSchema,
|
|
1046
1054
|
seoOverviewSchema,
|
|
1047
1055
|
seoProfileInputSchema,
|
|
@@ -1055,6 +1063,7 @@ import {
|
|
|
1055
1063
|
seoRunDetailSchema,
|
|
1056
1064
|
seoRunListSchema,
|
|
1057
1065
|
seoRunSchema,
|
|
1066
|
+
seoSearchTextForQuery,
|
|
1058
1067
|
seoStatusSchema,
|
|
1059
1068
|
seoSyncInputSchema,
|
|
1060
1069
|
serializeRunError,
|
|
@@ -1167,7 +1176,7 @@ import {
|
|
|
1167
1176
|
wordpressSchemaDeployResultDtoSchema,
|
|
1168
1177
|
wordpressSchemaStatusResultDtoSchema,
|
|
1169
1178
|
wordpressStatusDtoSchema
|
|
1170
|
-
} from "./chunk-
|
|
1179
|
+
} from "./chunk-XEGYSZEV.js";
|
|
1171
1180
|
|
|
1172
1181
|
// ../api-routes/src/user-session.ts
|
|
1173
1182
|
import crypto2 from "crypto";
|
|
@@ -7256,9 +7265,9 @@ var SentimentService = class {
|
|
|
7256
7265
|
const estimatedInputTokens = this.options.estimate ? inputs.reduce((sum, input) => sum + this.options.estimate(input), 0) : 0;
|
|
7257
7266
|
const expiresAt = new Date(Date.parse(this.now()) + 15 * 6e4).toISOString();
|
|
7258
7267
|
const previewToken = definitionId && settings.enabled && settings.ready && source.assessments.length ? this.sign({ projectId, selection: frozenSelection, definitionId, fingerprint: sentimentHash(source.assessments), expiresAt }) : null;
|
|
7259
|
-
const
|
|
7268
|
+
const skipped6 = source.runIds.flatMap((runId) => Object.entries(source.skippedByRun?.[runId] ?? {}).map(([reason, count10]) => ({ runId, reason, count: count10 })));
|
|
7260
7269
|
const estimateMethod = this.options.estimate ? `Adapter estimate of the full request including all questions and evidence candidates; final billed usage is recorded per attempt.${definitionId ? "" : " Sentiment is not configured for this project, so the estimate uses the current evaluator definition."}` : "Token and cost estimates unavailable on this host.";
|
|
7261
|
-
return { previewToken, expiresAt: previewToken ? expiresAt : null, selection: frozenSelection, evaluationDefinitionId: definitionId, eligibleAssessments: source.assessments.length, alreadyClassified, skipped:
|
|
7270
|
+
return { previewToken, expiresAt: previewToken ? expiresAt : null, selection: frozenSelection, evaluationDefinitionId: definitionId, eligibleAssessments: source.assessments.length, alreadyClassified, skipped: skipped6, estimatedInputTokens, estimatedCostUsd: this.options.estimate ? estimatedInputTokens * 0.042 / 1e6 : null, estimateMethod };
|
|
7262
7271
|
}
|
|
7263
7272
|
/** The receipt carries the first page of attempts; a replay of a long-running job never returns all of them. */
|
|
7264
7273
|
submit(projectId, previewToken, idempotencyKey, actor2) {
|
|
@@ -8413,6 +8422,7 @@ var HEALTH_CHECK_LABELS = {
|
|
|
8413
8422
|
"ga.auth.connection": "GA4 sign-in",
|
|
8414
8423
|
"ga.data.recent-data": "GA4 data stopped",
|
|
8415
8424
|
"gsc.data.recent-data": "Search Console data stopped",
|
|
8425
|
+
"seo.data.recent-refresh": "SEO refresh stalled",
|
|
8416
8426
|
"report.sweeps": "Monthly report sweeps",
|
|
8417
8427
|
"report.models": "Monthly report models",
|
|
8418
8428
|
"report.daily-data": "Monthly report data gaps",
|
|
@@ -8465,7 +8475,11 @@ var HEALTH_FIXED_LINES = {
|
|
|
8465
8475
|
"traffic.source.all-errored-recently": "Traffic syncs are working again.",
|
|
8466
8476
|
"traffic.queue-backlog.remaining": "The traffic backlog is cleared.",
|
|
8467
8477
|
"traffic.worker-version.stale": "The Cloudflare worker is up to date.",
|
|
8468
|
-
"gbp.auth.no-connection": "Business Profile is connected."
|
|
8478
|
+
"gbp.auth.no-connection": "Business Profile is connected.",
|
|
8479
|
+
"seo.data.refresh-failed": "SEO refreshes complete again.",
|
|
8480
|
+
"seo.data.refresh-partial": "SEO refreshes complete again.",
|
|
8481
|
+
"seo.data.overdue": "SEO data is current again.",
|
|
8482
|
+
"seo.data.schedule-invalid": "The SEO refresh schedule can run again."
|
|
8469
8483
|
};
|
|
8470
8484
|
function firstSentence(text, max2 = 180) {
|
|
8471
8485
|
if (!text) return null;
|
|
@@ -15214,7 +15228,40 @@ function runDto(job, counts2) {
|
|
|
15214
15228
|
return { id: job.id, projectId: job.projectKey, profileId: job.profileId, profileRevision: job.profileRevision, locationCode: job.profile.locationCode, languageCode: job.profile.languageCode, cancelRequested: job.cancelRequested, status: job.status, capabilities: job.profile.capabilities, scope: job.profile.scope, taskCount: counts2.reduce((sum, group) => sum + group.count, 0), completedTasks: counts2.filter((group) => group.status === SeoRunStatuses.completed || group.status === SeoRunStatuses.partial).reduce((sum, group) => sum + group.count, 0), failedTasks: counts2.filter((group) => group.status === SeoRunStatuses.failed).reduce((sum, group) => sum + group.count, 0), createdAt: job.createdAt, startedAt: job.startedAt, finishedAt: job.finishedAt, error: job.error };
|
|
15215
15229
|
}
|
|
15216
15230
|
function resultRows(tasks) {
|
|
15217
|
-
|
|
15231
|
+
const kept = tasks.filter((task) => task.reconciliation?.outcome !== SeoAdapterOutcomes.failed);
|
|
15232
|
+
return {
|
|
15233
|
+
keywordMetrics: kept.flatMap((task) => task.results.keywordMetrics),
|
|
15234
|
+
domainIntelligence: kept.flatMap((task) => task.results.domainIntelligence),
|
|
15235
|
+
rankedKeywords: kept.flatMap((task) => task.results.rankedKeywords),
|
|
15236
|
+
marketRankings: tasks.flatMap(marketRankingRows)
|
|
15237
|
+
};
|
|
15238
|
+
}
|
|
15239
|
+
function marketRankingRows(task) {
|
|
15240
|
+
const stored = task.reconciliation?.outcome === SeoAdapterOutcomes.failed ? [] : task.results.marketRankings ?? [];
|
|
15241
|
+
if (stored.length > 0) return stored.map((row) => ({ ...row, searchStatus: row.searchStatus ?? SeoMarketRankingSearchStatuses.completed }));
|
|
15242
|
+
const { search, target } = task.request;
|
|
15243
|
+
if (task.request.capability !== SeoCapabilities["market-rankings"] || !search || !target) return [];
|
|
15244
|
+
const searchStatus = task.status === SeoRunStatuses.cancelled ? SeoMarketRankingSearchStatuses.cancelled : task.status === SeoRunStatuses.failed || task.status === SeoRunStatuses["reconciliation-required"] ? SeoMarketRankingSearchStatuses.failed : null;
|
|
15245
|
+
if (!searchStatus) return [];
|
|
15246
|
+
return search.markets.map((market) => ({
|
|
15247
|
+
...market.context,
|
|
15248
|
+
searchText: task.request.keywords[0] ?? "",
|
|
15249
|
+
locationLabel: search.locationLabel,
|
|
15250
|
+
target,
|
|
15251
|
+
searchStatus,
|
|
15252
|
+
rankOrganic: null,
|
|
15253
|
+
rankAbsolute: null,
|
|
15254
|
+
url: null,
|
|
15255
|
+
depth: 0,
|
|
15256
|
+
competitors: [],
|
|
15257
|
+
provenance: null
|
|
15258
|
+
}));
|
|
15259
|
+
}
|
|
15260
|
+
function taskSearch(task) {
|
|
15261
|
+
return task.request.capability === SeoCapabilities["market-rankings"] ? { searchText: task.request.keywords[0] ?? null, locationLabel: task.request.search?.locationLabel ?? null } : { searchText: null, locationLabel: null };
|
|
15262
|
+
}
|
|
15263
|
+
function compareMarketRankings(a, b) {
|
|
15264
|
+
return compareText(a.regionLabel ?? "", b.regionLabel ?? "") || compareText(a.marketLabel, b.marketLabel) || compareText(a.searchText, b.searchText) || compareText(a.queryText, b.queryText);
|
|
15218
15265
|
}
|
|
15219
15266
|
function scopedPlanTargets(plan, scope) {
|
|
15220
15267
|
const market = scope.kind === SeoScopeKinds.market ? plan.reportingScopes?.find((market2) => market2.stableKey === scope.key) : void 0;
|
|
@@ -15222,9 +15269,14 @@ function scopedPlanTargets(plan, scope) {
|
|
|
15222
15269
|
return { targets: plan.targets.filter((target) => targetKeys.includes(target.stableKey)), market, targetKeys };
|
|
15223
15270
|
}
|
|
15224
15271
|
function scopeCapabilities(kind, targets) {
|
|
15225
|
-
if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"]];
|
|
15272
|
+
if (kind === SeoScopeKinds.project) return [SeoCapabilities["keyword-metrics"], SeoCapabilities["domain-intelligence"], SeoCapabilities["ranked-keywords"], SeoCapabilities["market-rankings"]];
|
|
15226
15273
|
const exactHosts = targets.length > 0 && targets.every((target) => target.urlMatchers.length > 0 && target.urlMatchers.every((matcher) => matcher.kind === "host"));
|
|
15227
|
-
|
|
15274
|
+
const markets = kind === SeoScopeKinds.group || kind === SeoScopeKinds.market ? [SeoCapabilities["market-rankings"]] : [];
|
|
15275
|
+
return exactHosts ? [SeoCapabilities["keyword-metrics"], SeoCapabilities["ranked-keywords"], ...markets] : [SeoCapabilities["keyword-metrics"], ...markets];
|
|
15276
|
+
}
|
|
15277
|
+
function scopeReason(capabilities) {
|
|
15278
|
+
const markets = capabilities.includes(SeoCapabilities["market-rankings"]);
|
|
15279
|
+
return capabilities.includes(SeoCapabilities["ranked-keywords"]) ? `Supports explicit keyword metrics${markets ? ", market rankings" : ""} and exact-host ranked keywords. Whole-domain intelligence requires project scope.` : `Supports explicit keyword metrics${markets ? " and market rankings" : " only"}. Path and entity Properties cannot be replaced by whole-domain data.`;
|
|
15228
15280
|
}
|
|
15229
15281
|
function cancelSeoRefresh(db, projectId, id) {
|
|
15230
15282
|
db.transaction((tx) => {
|
|
@@ -15252,7 +15304,7 @@ function resolveScope2(db, projectId, input) {
|
|
|
15252
15304
|
if (supported.includes(SeoCapabilities["ranked-keywords"])) throw validationError("Domain intelligence requires project scope with a whole-domain Target. This Advanced scope supports exact-host ranked keywords and explicit keyword metrics.");
|
|
15253
15305
|
throw validationError("This Advanced scope contains path or entity Properties and supports explicit keyword metrics only. Select a host-only scope for ranked keywords, or project scope for whole-domain intelligence.");
|
|
15254
15306
|
}
|
|
15255
|
-
if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.
|
|
15307
|
+
if (input.scope.kind !== SeoScopeKinds.project && input.capabilities.includes(SeoCapabilities["ranked-keywords"])) {
|
|
15256
15308
|
const matchers = targets.flatMap((target) => target.urlMatchers).filter((matcher) => matcher.kind === "host");
|
|
15257
15309
|
if (input.targets.some((target) => target.kind !== SeoTargetKinds.host || target.includeSubdomains || !matchers.some((matcher) => matchesMeasurementTargetUrl(`https://${target.value}/`, matcher)))) throw validationError("Ranked keywords in this Advanced scope require an exact host Target belonging to a selected Property. Other hosts and broader domains are not supported.");
|
|
15258
15310
|
}
|
|
@@ -15271,12 +15323,128 @@ function normalizeProfile(input) {
|
|
|
15271
15323
|
const keywords = [...new Set(input.keywords)];
|
|
15272
15324
|
if (input.capabilities.includes(SeoCapabilities["keyword-metrics"]) && !keywords.length) throw validationError("Keyword metrics requires at least one explicit keyword.");
|
|
15273
15325
|
if (input.capabilities.some((capability) => capability !== SeoCapabilities["keyword-metrics"]) && !targets.length) throw validationError("Domain capabilities require at least one explicit Target.");
|
|
15326
|
+
if (input.capabilities.includes(SeoCapabilities["market-rankings"])) {
|
|
15327
|
+
if (input.capabilities.length !== 1) throw validationError("Keep market rankings in their own profile: they run one search for each tracked query and market.");
|
|
15328
|
+
if (!input.rankings) throw validationError("Market rankings need search settings: market locations, result depth, device and search text.");
|
|
15329
|
+
if (targets.length !== 1) throw validationError("Market rankings need exactly one Target: the site whose position they report.");
|
|
15330
|
+
if (keywords.length) throw validationError("Market rankings search the project's tracked queries. Leave keywords empty.");
|
|
15331
|
+
const keys = input.rankings.locations.map((location) => location.key);
|
|
15332
|
+
if (new Set(keys).size !== keys.length) throw validationError("Set each market location once.");
|
|
15333
|
+
} else if (input.rankings) {
|
|
15334
|
+
throw validationError("Search settings apply only to market rankings.");
|
|
15335
|
+
}
|
|
15274
15336
|
return { ...input, targets, keywords };
|
|
15275
15337
|
}
|
|
15276
|
-
|
|
15277
|
-
|
|
15338
|
+
var NO_MARKET_RANKINGS = { searches: [] };
|
|
15339
|
+
function advancedMarketQueries(plan, scope) {
|
|
15340
|
+
const groups = new Map(plan.groups.map((group) => [group.stableKey, group]));
|
|
15341
|
+
const lineage = (groupKey) => {
|
|
15342
|
+
const keys = [];
|
|
15343
|
+
for (let key = groupKey; key && !keys.includes(key); key = groups.get(key)?.parentGroupKey) keys.push(key);
|
|
15344
|
+
return keys;
|
|
15345
|
+
};
|
|
15346
|
+
const nonBrandEdge = new Set(plan.assignments.filter((assignment) => assignment.queryClass === queryClassSchema.enum["non-brand"]).map((assignment) => `${assignment.targetKey}\0${assignment.queryId}`));
|
|
15347
|
+
const queryText = new Map(plan.querySnapshots.map((snapshot) => [snapshot.queryId, snapshot.queryText]));
|
|
15348
|
+
const queriesFor = (edges) => {
|
|
15349
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
15350
|
+
for (const edge of edges) nodes.set(edge.queryId, (nodes.get(edge.queryId) ?? /* @__PURE__ */ new Set()).add(edge.executionNodeKey));
|
|
15351
|
+
return [...nodes].flatMap(([id, keys]) => {
|
|
15352
|
+
const text = queryText.get(id);
|
|
15353
|
+
return text ? [{ id, text, executionNodeKeys: [...keys] }] : [];
|
|
15354
|
+
});
|
|
15355
|
+
};
|
|
15356
|
+
const markets = plan.reportingScopes ?? [];
|
|
15357
|
+
if (!markets.length) {
|
|
15358
|
+
const { targetKeys } = scopedPlanTargets(plan, scope);
|
|
15359
|
+
return [{ context: { marketKey: SEO_PROJECT_MARKET_KEY, marketLabel: "Project", regionKey: null, regionLabel: null }, locationKeys: [SEO_PROJECT_MARKET_KEY], queries: queriesFor(plan.assignments.filter((assignment) => targetKeys.includes(assignment.targetKey) && nonBrandEdge.has(`${assignment.targetKey}\0${assignment.queryId}`))) }];
|
|
15360
|
+
}
|
|
15361
|
+
return markets.flatMap((market) => {
|
|
15362
|
+
const chain = lineage(market.groupKey);
|
|
15363
|
+
const inScope = scope.kind === SeoScopeKinds.project || (scope.kind === SeoScopeKinds.market ? market.stableKey === scope.key : scope.kind === SeoScopeKinds.group && chain.includes(scope.key));
|
|
15364
|
+
if (!inScope) return [];
|
|
15365
|
+
const region = chain.at(-1);
|
|
15366
|
+
return [{
|
|
15367
|
+
context: { marketKey: market.stableKey, marketLabel: market.label, regionKey: region ?? null, regionLabel: region ? groups.get(region)?.label ?? region : null },
|
|
15368
|
+
locationKeys: [market.stableKey, ...chain],
|
|
15369
|
+
queries: queriesFor(market.usageEdges.filter((edge) => nonBrandEdge.has(`${edge.targetKey}\0${edge.queryId}`)))
|
|
15370
|
+
}];
|
|
15371
|
+
});
|
|
15372
|
+
}
|
|
15373
|
+
function simpleMarketQueries(db, project) {
|
|
15374
|
+
const rows = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all();
|
|
15375
|
+
const classes = readQueryClasses(db, project, rows);
|
|
15376
|
+
if (rows.length > 0 && rows.every((row) => classes.get(row.id) == null)) {
|
|
15377
|
+
throw validationError("Canonry cannot tell this project's branded queries apart without a brand name. Add one before setting up market rankings.");
|
|
15378
|
+
}
|
|
15379
|
+
const nonBrand = rows.filter((row) => classes.get(row.id) === queryClassSchema.enum["non-brand"]).map((row) => ({ id: row.id, text: row.query, executionNodeKeys: [] }));
|
|
15380
|
+
const locations = project.locations.length > 0 ? project.locations.map((location) => ({ key: location.label, label: location.label })) : [{ key: SEO_PROJECT_MARKET_KEY, label: "Project" }];
|
|
15381
|
+
return locations.map((location) => ({ context: { marketKey: location.key, marketLabel: location.label, regionKey: null, regionLabel: null }, locationKeys: [location.key], queries: nonBrand }));
|
|
15382
|
+
}
|
|
15383
|
+
function marketRankingPlan(db, projectId, input) {
|
|
15384
|
+
if (!input.capabilities.includes(SeoCapabilities["market-rankings"]) || !input.rankings) return NO_MARKET_RANKINGS;
|
|
15385
|
+
const settings = input.rankings;
|
|
15386
|
+
const project = db.select().from(projects).where(eq32(projects.id, projectId)).get();
|
|
15387
|
+
if (!project) throw notFound("Project", projectId);
|
|
15388
|
+
const own = new Set(effectiveDomains({ canonicalDomain: project.canonicalDomain, ownedDomains: project.ownedDomains }).map((domain) => registrableDomain(domain)).filter(Boolean));
|
|
15389
|
+
const target = input.targets[0];
|
|
15390
|
+
if (!target || !own.has(registrableDomain(target.value))) throw validationError("The market-rankings Target must be one of this project's own domains.");
|
|
15391
|
+
const active = activeMeasurementPlan(db, projectId);
|
|
15392
|
+
const advanced = active?.plan.schemaVersion === 2 ? { versionId: active.version.id, plan: active.plan } : null;
|
|
15393
|
+
const markets = advanced ? advancedMarketQueries(advanced.plan, input.scope) : simpleMarketQueries(db, project);
|
|
15394
|
+
const rival = (domain) => {
|
|
15395
|
+
const registrable = registrableDomain(normalizeHostname(domain) ?? domain);
|
|
15396
|
+
return registrable && !own.has(registrable) ? [registrable] : [];
|
|
15397
|
+
};
|
|
15398
|
+
const projectCompetitors = db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, projectId)).all().flatMap((row) => rival(row.domain));
|
|
15399
|
+
const pinnedFor = createPlanPinLookup(db);
|
|
15400
|
+
const competitorsFor = (query) => [.../* @__PURE__ */ new Set([...projectCompetitors, ...query.executionNodeKeys.flatMap((key) => pinnedFor(advanced?.versionId, key)).flatMap(rival)])].sort(compareText);
|
|
15401
|
+
const locations = new Map(settings.locations.map((location) => [location.key, location]));
|
|
15402
|
+
const searches = /* @__PURE__ */ new Map();
|
|
15403
|
+
const unmapped = [];
|
|
15404
|
+
for (const market of markets) {
|
|
15405
|
+
const mapped = market.locationKeys.map((key) => locations.get(key)).find((location2) => location2 !== void 0);
|
|
15406
|
+
const location = mapped ?? (market.context.marketKey === SEO_PROJECT_MARKET_KEY ? { locationCode: input.locationCode, label: `Location ${input.locationCode}` } : void 0);
|
|
15407
|
+
if (!location) {
|
|
15408
|
+
unmapped.push(market.context.marketLabel);
|
|
15409
|
+
continue;
|
|
15410
|
+
}
|
|
15411
|
+
for (const query of market.queries) {
|
|
15412
|
+
const searchText = seoSearchTextForQuery(query.text, market.context.marketKey === SEO_PROJECT_MARKET_KEY ? null : market.context.marketLabel, settings.searchText);
|
|
15413
|
+
if (!searchText) continue;
|
|
15414
|
+
const answers = {
|
|
15415
|
+
context: { ...market.context, queryId: query.id, queryText: query.text, queryClass: queryClassSchema.enum["non-brand"] },
|
|
15416
|
+
competitors: competitorsFor(query)
|
|
15417
|
+
};
|
|
15418
|
+
const key = `${location.locationCode}\0${searchText.toLowerCase()}`;
|
|
15419
|
+
const existing = searches.get(key);
|
|
15420
|
+
if (existing) existing.markets.push(answers);
|
|
15421
|
+
else searches.set(key, { searchText, locationCode: location.locationCode, locationLabel: location.label, markets: [answers] });
|
|
15422
|
+
}
|
|
15423
|
+
}
|
|
15424
|
+
if (unmapped.length) throw validationError(`Set a search location for ${unmapped.length === 1 ? "1 market" : `${unmapped.length} markets`} or their groups: ${unmapped.slice(0, 5).join("; ")}${unmapped.length > 5 ? "; \u2026" : ""}.`);
|
|
15425
|
+
if (!searches.size) throw validationError("There are no non-brand tracked queries to search in this scope.");
|
|
15426
|
+
if (searches.size > SEO_MARKET_RANKING_MAX_SEARCHES) throw validationError(`One market-rankings refresh runs at most ${SEO_MARKET_RANKING_MAX_SEARCHES} searches; this scope needs ${searches.size}. Select a narrower scope.`);
|
|
15427
|
+
return { searches: [...searches.values()].sort((a, b) => compareText(a.locationLabel, b.locationLabel) || compareText(a.searchText, b.searchText)) };
|
|
15428
|
+
}
|
|
15429
|
+
function taskRequests(profile, rankings) {
|
|
15430
|
+
return profile.capabilities.flatMap((capability) => {
|
|
15431
|
+
if (capability === SeoCapabilities["market-rankings"]) {
|
|
15432
|
+
const settings = profile.rankings;
|
|
15433
|
+
return rankings.searches.map((search) => ({
|
|
15434
|
+
capability,
|
|
15435
|
+
target: profile.targets[0],
|
|
15436
|
+
keywords: [search.searchText],
|
|
15437
|
+
locationCode: search.locationCode,
|
|
15438
|
+
languageCode: profile.languageCode,
|
|
15439
|
+
limit: profile.limit,
|
|
15440
|
+
tag: "",
|
|
15441
|
+
search: { device: settings.device, depth: settings.depth, locationLabel: search.locationLabel, markets: search.markets }
|
|
15442
|
+
}));
|
|
15443
|
+
}
|
|
15444
|
+
return (capability === SeoCapabilities["keyword-metrics"] ? [null] : profile.targets).map((target) => ({ capability, target, keywords: capability === SeoCapabilities["keyword-metrics"] ? profile.keywords : [], locationCode: profile.locationCode, languageCode: profile.languageCode, limit: profile.limit, tag: "" }));
|
|
15445
|
+
});
|
|
15278
15446
|
}
|
|
15279
|
-
function planSeoTasks(opts, input) {
|
|
15447
|
+
function planSeoTasks(opts, input, rankings = NO_MARKET_RANKINGS) {
|
|
15280
15448
|
const runtime = opts.getSeoRuntime?.() ?? null;
|
|
15281
15449
|
const connection = runtime?.connections.find((connection2) => connection2.id === input.connectionId);
|
|
15282
15450
|
if (!runtime || !opts.onSeoRunRequested) throw missingDependency("SEO execution is not configured on this host.");
|
|
@@ -15284,7 +15452,7 @@ function planSeoTasks(opts, input) {
|
|
|
15284
15452
|
if (input.capabilities.some((capability) => !connection.adapter.capabilities.includes(capability))) throw validationError("The selected SEO connection does not support every requested capability. Select capabilities listed for this connection in SEO context.");
|
|
15285
15453
|
let planned;
|
|
15286
15454
|
try {
|
|
15287
|
-
planned = taskRequests(input).map((request) => {
|
|
15455
|
+
planned = taskRequests(input, rankings).map((request) => {
|
|
15288
15456
|
const estimate = connection.adapter.estimate(request);
|
|
15289
15457
|
return { request, reservedMicros: estimate.maxCostMicros, catalogRevision: estimate.catalogRevision };
|
|
15290
15458
|
});
|
|
@@ -15304,14 +15472,20 @@ function budgetBuckets(account, projectId, policy, now) {
|
|
|
15304
15472
|
{ scope: `project-month:${projectId}`, period: now.slice(0, 7), limit: policy.projectMonthlyLimitMicros }
|
|
15305
15473
|
];
|
|
15306
15474
|
}
|
|
15475
|
+
function skippedRefreshReason(profileId, reason) {
|
|
15476
|
+
return `Scheduled SEO refresh for ${profileId ? `profile "${profileId}"` : "this project"} did not start: ${reason}`;
|
|
15477
|
+
}
|
|
15478
|
+
var SKIPPED_REFRESH_PROFILE = /^Scheduled SEO refresh for profile "([^"]+)" did not start: /;
|
|
15479
|
+
function skippedRefreshProfile(error) {
|
|
15480
|
+
return error ? SKIPPED_REFRESH_PROFILE.exec(error)?.[1] ?? null : null;
|
|
15481
|
+
}
|
|
15307
15482
|
function startScheduledSeoRefreshes(db, opts, input) {
|
|
15308
15483
|
const repository = new SeoRepository(db);
|
|
15309
15484
|
const principalId = `schedule:${input.scheduleId}`;
|
|
15310
15485
|
const recordSkipped = (profileId, reason, detail) => {
|
|
15311
15486
|
const runId = crypto17.randomUUID();
|
|
15312
15487
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15313
|
-
|
|
15314
|
-
db.insert(runs).values({ id: runId, projectId: input.projectId, kind: RunKinds["seo-sync"], status: RunStatuses.failed, trigger: RunTriggers.scheduled, createdAt: now, finishedAt: now, error: `Scheduled SEO refresh for ${subject} did not start: ${reason}` }).run();
|
|
15488
|
+
db.insert(runs).values({ id: runId, projectId: input.projectId, kind: RunKinds["seo-sync"], status: RunStatuses.failed, trigger: RunTriggers.scheduled, createdAt: now, finishedAt: now, error: skippedRefreshReason(profileId, reason) }).run();
|
|
15315
15489
|
return { profileId, status: "skipped", runId, reason, ...detail ? { detail } : {} };
|
|
15316
15490
|
};
|
|
15317
15491
|
const saved = db.select().from(seoProfiles).where(eq32(seoProfiles.projectId, input.projectId)).orderBy(asc6(seoProfiles.id)).all().map(profileDto);
|
|
@@ -15322,7 +15496,7 @@ function startScheduledSeoRefreshes(db, opts, input) {
|
|
|
15322
15496
|
if (prior?.runId) return { profileId: profile.id, status: "replayed", runId: prior.runId };
|
|
15323
15497
|
try {
|
|
15324
15498
|
const scopeSnapshot = resolveScope2(db, input.projectId, profile);
|
|
15325
|
-
const runtime = planSeoTasks(opts, profile);
|
|
15499
|
+
const runtime = planSeoTasks(opts, profile, marketRankingPlan(db, input.projectId, profile));
|
|
15326
15500
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15327
15501
|
const account = runtime.connection.adapter.accountKey;
|
|
15328
15502
|
const requestedId = crypto17.randomUUID();
|
|
@@ -15363,8 +15537,8 @@ async function seoRoutes(app, opts) {
|
|
|
15363
15537
|
if (!row) throw notFound("SEO profile", id);
|
|
15364
15538
|
return profileDto(row);
|
|
15365
15539
|
};
|
|
15366
|
-
const tasks = (id) => app.db.select().from(seoTasks).where(eq32(seoTasks.jobId, id)).orderBy(asc6(seoTasks.id)).all();
|
|
15367
|
-
const taskCounts = (ids) => ids.length ? app.db.select({ jobId: seoTasks.jobId, status: seoTasks.status, count: sql8`count(*)` }).from(seoTasks).where(inArray16(seoTasks.jobId, ids)).groupBy(seoTasks.jobId, seoTasks.status).all() : [];
|
|
15540
|
+
const tasks = (id) => currentSeoTasks(app.db.select().from(seoTasks).where(eq32(seoTasks.jobId, id)).orderBy(asc6(seoTasks.id)).all());
|
|
15541
|
+
const taskCounts = (ids) => ids.length ? app.db.select({ jobId: seoTasks.jobId, status: seoTasks.status, count: sql8`count(*)` }).from(seoTasks).where(and24(inArray16(seoTasks.jobId, ids), currentSeoTaskCondition())).groupBy(seoTasks.jobId, seoTasks.status).all() : [];
|
|
15368
15542
|
const runSummary = (row) => runDto(row, taskCounts([row.id]));
|
|
15369
15543
|
const job = (projectId, id) => {
|
|
15370
15544
|
const row = app.db.select().from(seoJobs).where(and24(eq32(seoJobs.projectId, projectId), isNotNull3(seoJobs.runId), eq32(seoJobs.id, id))).get();
|
|
@@ -15394,7 +15568,7 @@ async function seoRoutes(app, opts) {
|
|
|
15394
15568
|
const available = !!runtime?.connections.length && !!opts.onSeoRunRequested;
|
|
15395
15569
|
return { available, reason: available ? null : "SEO execution is not configured on this host.", capabilities: [...new Set(runtime?.connections.flatMap((connection) => [...connection.adapter.capabilities]) ?? [])], profileCount: profiles(projectId).length, latestRunId: latest?.id ?? null, latestRunStatus: latest?.status ?? null, lastCompletedAt: last?.finishedAt ?? null };
|
|
15396
15570
|
};
|
|
15397
|
-
const planTasks = (input) => planSeoTasks(opts, input);
|
|
15571
|
+
const planTasks = (projectId, input) => planSeoTasks(opts, input, marketRankingPlan(app.db, projectId, input));
|
|
15398
15572
|
app.get("/seo/operator/tasks/:id", async (request) => {
|
|
15399
15573
|
requireOperator(request);
|
|
15400
15574
|
const task = app.db.select().from(seoTasks).where(eq32(seoTasks.id, request.params.id)).get();
|
|
@@ -15422,7 +15596,7 @@ async function seoRoutes(app, opts) {
|
|
|
15422
15596
|
const plan = active?.plan.schemaVersion === 2 ? active.plan : null;
|
|
15423
15597
|
const scopes = plan ? planScopeOptions(plan, { marketLinks: true }).map((option) => {
|
|
15424
15598
|
const capabilities = scopeCapabilities(option.kind, scopedPlanTargets(plan, { kind: option.kind, ...option.kind === SeoScopeKinds.project ? {} : { key: option.id } }).targets).filter((capability) => current.capabilities.includes(capability));
|
|
15425
|
-
return { kind: option.kind, key: option.kind === SeoScopeKinds.project ? null : option.id, label: option.label, planRevision: active.version.revision, capabilities, supported: capabilities.length > 0, reason: !capabilities.length ? current.reason ?? "No configured connection supports this scope." : option.kind === SeoScopeKinds.project ? null : capabilities
|
|
15599
|
+
return { kind: option.kind, key: option.kind === SeoScopeKinds.project ? null : option.id, label: option.label, planRevision: active.version.revision, capabilities, supported: capabilities.length > 0, reason: !capabilities.length ? current.reason ?? "No configured connection supports this scope." : option.kind === SeoScopeKinds.project ? null : scopeReason(capabilities) };
|
|
15426
15600
|
}) : [{ kind: SeoScopeKinds.project, key: null, label: "Project", capabilities: active ? [] : current.capabilities, supported: !active && current.capabilities.length > 0, reason: active ? "Publish a current Advanced Measurement plan first." : current.reason }];
|
|
15427
15601
|
return { available: current.available, reason: current.reason, connections: getRuntime()?.connections.map((connection) => ({ id: connection.id, label: connection.label, provider: connection.adapter.provider, capabilities: [...connection.adapter.capabilities] })) ?? [], scopes, access: { canRun: can(request, SEO_RUN_SCOPE), canWrite: can(request, SEO_WRITE_SCOPE) } };
|
|
15428
15602
|
});
|
|
@@ -15438,7 +15612,7 @@ async function seoRoutes(app, opts) {
|
|
|
15438
15612
|
if (!parsed.success) throw validationError("Invalid SEO profile.", { issues: parsed.error.issues });
|
|
15439
15613
|
const input = normalizeProfile(parsed.data);
|
|
15440
15614
|
resolveScope2(app.db, project.id, input);
|
|
15441
|
-
planTasks(input);
|
|
15615
|
+
planTasks(project.id, input);
|
|
15442
15616
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15443
15617
|
app.db.transaction((tx) => {
|
|
15444
15618
|
const current = tx.select().from(seoProfiles).where(and24(eq32(seoProfiles.projectId, project.id), eq32(seoProfiles.id, request.params.id))).get();
|
|
@@ -15454,7 +15628,7 @@ async function seoRoutes(app, opts) {
|
|
|
15454
15628
|
if (!parsed.success) throw validationError("Select an SEO profile.");
|
|
15455
15629
|
const saved = profile(project.id, parsed.data.profileId);
|
|
15456
15630
|
const scope = resolveScope2(app.db, project.id, saved);
|
|
15457
|
-
const planned = planTasks(saved);
|
|
15631
|
+
const planned = planTasks(project.id, saved);
|
|
15458
15632
|
const expiresAt = new Date(Date.now() + 5 * 6e4).toISOString();
|
|
15459
15633
|
const payload = Buffer.from(JSON.stringify({ projectId: project.id, actor: actor(request), profileId: saved.id, revision: saved.revision, scopeHash: hash2(JSON.stringify(scope)), intentHash: planned.intentHash, nonce: crypto17.randomUUID(), expiresAt })).toString("base64url");
|
|
15460
15634
|
const signature = crypto17.createHmac("sha256", intentSecret).update(payload).digest("base64url");
|
|
@@ -15491,7 +15665,7 @@ async function seoRoutes(app, opts) {
|
|
|
15491
15665
|
const saved = profile(project.id, claims.profileId);
|
|
15492
15666
|
const scopeSnapshot = resolveScope2(app.db, project.id, saved);
|
|
15493
15667
|
if (saved.revision !== claims.revision || hash2(JSON.stringify(scopeSnapshot)) !== claims.scopeHash) throw validationError("This SEO profile or measurement scope changed. Create a new refresh intent.");
|
|
15494
|
-
const runtime = planTasks(saved);
|
|
15668
|
+
const runtime = planTasks(project.id, saved);
|
|
15495
15669
|
if (runtime.intentHash !== claims.intentHash) throw validationError("The SEO connection or host policy changed. Create a new refresh intent.");
|
|
15496
15670
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15497
15671
|
const account = runtime.connection.adapter.accountKey;
|
|
@@ -15515,7 +15689,7 @@ async function seoRoutes(app, opts) {
|
|
|
15515
15689
|
const row = job(resolveProject(app.db, request.params.name).id, request.params.id);
|
|
15516
15690
|
const rows = tasks(row.id);
|
|
15517
15691
|
const results = resultRows(rows);
|
|
15518
|
-
return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
|
|
15692
|
+
return { ...runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))), tasks: rows.map((task) => ({ id: task.id, capability: task.request.capability, target: task.request.target, status: task.status, error: task.error, remoteTaskId: task.receipts[0]?.remoteTaskId ?? null, ...taskSearch(task), retried: task.retryOf !== null })), results: { keywordMetrics: results.keywordMetrics.slice(0, 20), domainIntelligence: results.domainIntelligence.slice(0, 20), rankedKeywords: results.rankedKeywords.slice(0, 20), marketRankings: [...results.marketRankings].sort(compareMarketRankings).slice(0, 20) }, resultCounts: { keywordMetrics: results.keywordMetrics.length, domainIntelligence: results.domainIntelligence.length, rankedKeywords: results.rankedKeywords.length, marketRankings: results.marketRankings.length }, resultsTruncated: Object.values(results).some((items) => items.length > 20) };
|
|
15519
15693
|
});
|
|
15520
15694
|
app.post("/projects/:name/seo/runs/:id/cancel", { config: { paidRead: true, writeScope: SEO_RUN_SCOPE } }, async (request) => {
|
|
15521
15695
|
requireScope(request, SEO_RUN_SCOPE);
|
|
@@ -15534,7 +15708,7 @@ async function seoRoutes(app, opts) {
|
|
|
15534
15708
|
const results = resultRows(rows);
|
|
15535
15709
|
const primary = row?.profile.targets[0];
|
|
15536
15710
|
const domain = primary ? results.domainIntelligence.find((result) => result.target.kind === primary.kind && result.target.value === primary.value && result.target.includeSubdomains === primary.includeSubdomains) : null;
|
|
15537
|
-
return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
|
|
15711
|
+
return { status: status(project.id), profiles: profiles(project.id), latestRun: latest ? runSummary(latest) : null, evidenceRunId: row?.id ?? null, evidenceRun: row ? runDto(row, rows.map((task) => ({ status: task.status, count: 1 }))) : null, keywordMetricsCount: results.keywordMetrics.length, domainIntelligenceCount: results.domainIntelligence.length, rankedKeywordCount: results.rankedKeywords.length, marketRankingCount: results.marketRankings.length, summary: domain ? { target: domain.target, organicKeywords: domain.organicKeywords, inTop10: domain.top1 !== null && domain.top2To3 !== null && domain.top4To10 !== null ? domain.top1 + domain.top2To3 + domain.top4To10 : null, estimatedOrganicTraffic: domain.estimatedOrganicTraffic, provenance: domain.provenance } : null };
|
|
15538
15712
|
});
|
|
15539
15713
|
for (const [path8, key] of [["keyword-metrics", "keywordMetrics"], ["domain-intelligence", "domainIntelligence"], ["ranked-keywords", "rankedKeywords"]]) app.get(`/projects/:name/seo/${path8}`, async (request) => {
|
|
15540
15714
|
const query = queryInput(request.query);
|
|
@@ -15544,6 +15718,17 @@ async function seoRoutes(app, opts) {
|
|
|
15544
15718
|
const offset = query.offset ?? 0;
|
|
15545
15719
|
return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
|
|
15546
15720
|
});
|
|
15721
|
+
app.get("/projects/:name/seo/market-rankings", async (request) => {
|
|
15722
|
+
const raw = request.query ?? {};
|
|
15723
|
+
const parsed = seoMarketRankingQuerySchema.safeParse({ ...raw, ...raw.limit === void 0 ? {} : { limit: Number(raw.limit) }, ...raw.offset === void 0 ? {} : { offset: Number(raw.offset) } });
|
|
15724
|
+
if (!parsed.success) throw validationError("Invalid SEO evidence selection.");
|
|
15725
|
+
const query = parsed.data;
|
|
15726
|
+
const row = selected(resolveProject(app.db, request.params.name).id, query, true);
|
|
15727
|
+
const items = (row ? resultRows(tasks(row.id)).marketRankings : []).filter((item2) => (query.marketKey === void 0 || item2.marketKey === query.marketKey) && (query.regionKey === void 0 || item2.regionKey === query.regionKey)).sort(compareMarketRankings);
|
|
15728
|
+
const limit = query.limit ?? 50;
|
|
15729
|
+
const offset = query.offset ?? 0;
|
|
15730
|
+
return { runId: row?.id ?? null, items: items.slice(offset, offset + limit), total: items.length, limit, offset };
|
|
15731
|
+
});
|
|
15547
15732
|
}
|
|
15548
15733
|
|
|
15549
15734
|
// ../api-routes/src/runs.ts
|
|
@@ -26989,7 +27174,7 @@ async function applyRoutes(app, opts) {
|
|
|
26989
27174
|
timezone: resolvedSchedule.timezone,
|
|
26990
27175
|
nextRunAt,
|
|
26991
27176
|
providers: config.spec.schedule?.providers ?? [],
|
|
26992
|
-
...specEnabled === void 0 ? {} : { enabled: specEnabled },
|
|
27177
|
+
...specEnabled === void 0 ? {} : { enabled: specEnabled, enabledAt: scheduleEnabledAt(existingSched, specEnabled, now) },
|
|
26993
27178
|
updatedAt: nextScheduleUpdatedAt(existingSched.updatedAt, Date.parse(now))
|
|
26994
27179
|
}).where(eq44(schedules.id, existingSched.id)).run();
|
|
26995
27180
|
} else {
|
|
@@ -27003,6 +27188,7 @@ async function applyRoutes(app, opts) {
|
|
|
27003
27188
|
timezone: resolvedSchedule.timezone,
|
|
27004
27189
|
nextRunAt,
|
|
27005
27190
|
enabled: specEnabled ?? true,
|
|
27191
|
+
enabledAt: scheduleEnabledAt(void 0, specEnabled ?? true, now),
|
|
27006
27192
|
providers: config.spec.schedule?.providers ?? [],
|
|
27007
27193
|
createdAt: now,
|
|
27008
27194
|
updatedAt: now
|
|
@@ -33750,6 +33936,7 @@ var SCHEMA_TABLE = {
|
|
|
33750
33936
|
SeoKeywordMetricsList: seoKeywordMetricsListSchema,
|
|
33751
33937
|
SeoDomainIntelligenceList: seoDomainIntelligenceListSchema,
|
|
33752
33938
|
SeoRankedKeywordList: seoRankedKeywordListSchema,
|
|
33939
|
+
SeoMarketRankingList: seoMarketRankingListSchema,
|
|
33753
33940
|
SentimentSummary: sentimentSummarySchema,
|
|
33754
33941
|
SentimentSettings: sentimentSettingsSchema,
|
|
33755
33942
|
SentimentSettingsUpdate: sentimentSettingsUpdateSchema,
|
|
@@ -34977,6 +35164,26 @@ var routeCatalog = [
|
|
|
34977
35164
|
409: errorResponse("Selection, intent or execution identity changed.")
|
|
34978
35165
|
}
|
|
34979
35166
|
},
|
|
35167
|
+
{
|
|
35168
|
+
method: "get",
|
|
35169
|
+
path: "/api/v1/projects/{name}/seo/market-rankings",
|
|
35170
|
+
summary: "Read stored market rankings",
|
|
35171
|
+
tags: ["seo"],
|
|
35172
|
+
description: "Stored evidence only; never calls a provider. The project's position for each tracked non-brand query, searched on Google from its market's location, ordered by region, market and search text. searchStatus says how each search ended: in a completed or partial row a null rank means the site was not in the checked results (depth, which is lower when only part of the results came back); a failed or cancelled search keeps its rows with no rank.",
|
|
35173
|
+
parameters: [
|
|
35174
|
+
nameParameter,
|
|
35175
|
+
...seoEvidenceParameters,
|
|
35176
|
+
{ name: "marketKey", in: "query", description: "Only rows for this market (an Advanced market key or a Simple location label).", schema: stringSchema },
|
|
35177
|
+
{ name: "regionKey", in: "query", description: "Only rows whose market sits under this top-level group.", schema: stringSchema }
|
|
35178
|
+
],
|
|
35179
|
+
responses: {
|
|
35180
|
+
200: jsonResponse("SEO response.", "SeoMarketRankingList"),
|
|
35181
|
+
400: errorResponse("Invalid scope or input."),
|
|
35182
|
+
403: errorResponse("Insufficient authority."),
|
|
35183
|
+
404: errorResponse("Project or selected evidence not found."),
|
|
35184
|
+
409: errorResponse("Selection, intent or execution identity changed.")
|
|
35185
|
+
}
|
|
35186
|
+
},
|
|
34980
35187
|
{
|
|
34981
35188
|
method: "put",
|
|
34982
35189
|
path: "/api/v1/projects/{name}/seo/profiles/{id}",
|
|
@@ -42929,6 +43136,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
42929
43136
|
providers: providers ?? [],
|
|
42930
43137
|
sourceId: sourceId ?? null,
|
|
42931
43138
|
enabled: enabledBool,
|
|
43139
|
+
enabledAt: scheduleEnabledAt(existing, enabledBool, now),
|
|
42932
43140
|
updatedAt: now
|
|
42933
43141
|
};
|
|
42934
43142
|
if (existing) {
|
|
@@ -59673,11 +59881,11 @@ async function wordpressRoutes(app, opts) {
|
|
|
59673
59881
|
reportMetaStep(metaWriteOutcome(result.results));
|
|
59674
59882
|
const applied = result.results.filter((r) => r.status === "applied").length;
|
|
59675
59883
|
const manual = result.results.filter((r) => r.status === "manual").length;
|
|
59676
|
-
const
|
|
59884
|
+
const skipped6 = result.results.filter((r) => r.status === "skipped").length;
|
|
59677
59885
|
steps.push({
|
|
59678
59886
|
name: "set-meta",
|
|
59679
59887
|
status: "completed",
|
|
59680
|
-
summary: `${applied} applied, ${manual} manual-assist, ${
|
|
59888
|
+
summary: `${applied} applied, ${manual} manual-assist, ${skipped6} skipped`
|
|
59681
59889
|
});
|
|
59682
59890
|
}
|
|
59683
59891
|
} catch (err) {
|
|
@@ -59707,11 +59915,11 @@ async function wordpressRoutes(app, opts) {
|
|
|
59707
59915
|
reportSchemaStep(schemaDeployOutcome(result.results));
|
|
59708
59916
|
const deployed = result.results.filter((r) => r.status === "deployed").length;
|
|
59709
59917
|
const stripped = result.results.filter((r) => r.status === "stripped").length;
|
|
59710
|
-
const
|
|
59918
|
+
const skipped6 = result.results.filter((r) => r.status === "skipped").length;
|
|
59711
59919
|
steps.push({
|
|
59712
59920
|
name: "schema-deploy",
|
|
59713
59921
|
status: "completed",
|
|
59714
|
-
summary: `${deployed} deployed, ${stripped} stripped (manual-assist), ${
|
|
59922
|
+
summary: `${deployed} deployed, ${stripped} stripped (manual-assist), ${skipped6} skipped`
|
|
59715
59923
|
});
|
|
59716
59924
|
}
|
|
59717
59925
|
} catch (err) {
|
|
@@ -60099,7 +60307,7 @@ async function queryBacklinks(opts) {
|
|
|
60099
60307
|
const reversed = opts.targets.map(reverseDomain);
|
|
60100
60308
|
const targetList = reversed.map(quote).join(", ");
|
|
60101
60309
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
60102
|
-
const
|
|
60310
|
+
const sql38 = `
|
|
60103
60311
|
WITH vertices AS (
|
|
60104
60312
|
SELECT * FROM read_csv(
|
|
60105
60313
|
${quote(opts.vertexPath)},
|
|
@@ -60135,7 +60343,7 @@ async function queryBacklinks(opts) {
|
|
|
60135
60343
|
const conn = await instance.connect();
|
|
60136
60344
|
let rows;
|
|
60137
60345
|
try {
|
|
60138
|
-
const reader = await conn.runAndReadAll(
|
|
60346
|
+
const reader = await conn.runAndReadAll(sql38);
|
|
60139
60347
|
rows = reader.getRowObjects();
|
|
60140
60348
|
} finally {
|
|
60141
60349
|
conn.disconnectSync?.();
|
|
@@ -62742,6 +62950,7 @@ function bindTrafficSyncSchedule(tx, projectId, sourceId, now, createIfMissing =
|
|
|
62742
62950
|
preset: null,
|
|
62743
62951
|
timezone: "UTC",
|
|
62744
62952
|
enabled: true,
|
|
62953
|
+
enabledAt: now,
|
|
62745
62954
|
providers: [],
|
|
62746
62955
|
sourceId,
|
|
62747
62956
|
createdAt: now,
|
|
@@ -67251,6 +67460,195 @@ var gscRecentDataCheck = {
|
|
|
67251
67460
|
};
|
|
67252
67461
|
var DATA_FRESHNESS_CHECKS = [gaRecentDataCheck, gscRecentDataCheck];
|
|
67253
67462
|
|
|
67463
|
+
// ../api-routes/src/doctor/checks/seo-refresh.ts
|
|
67464
|
+
import { and as and75, asc as asc20, desc as desc42, eq as eq89, gt as gt8, inArray as inArray39, sql as sql32 } from "drizzle-orm";
|
|
67465
|
+
var GRACE_DAYS = 2;
|
|
67466
|
+
var DAY_MS5 = 24 * 60 * 60 * 1e3;
|
|
67467
|
+
var FIRING_SPAN_MS = 6e4;
|
|
67468
|
+
var REASON_LIMIT = 300;
|
|
67469
|
+
function skipped2(code, summary3) {
|
|
67470
|
+
return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
|
|
67471
|
+
}
|
|
67472
|
+
function seoSyncRuns(projectId) {
|
|
67473
|
+
return and75(eq89(runs.projectId, projectId), eq89(runs.kind, RunKinds["seo-sync"]));
|
|
67474
|
+
}
|
|
67475
|
+
function scheduleTiming(schedule) {
|
|
67476
|
+
if (!schedule.recurrence) return { cronExpr: schedule.cronExpr, timezone: schedule.timezone, recurrence: null };
|
|
67477
|
+
const recurrence = calendarRecurrenceSchema.safeParse(schedule.recurrence).data;
|
|
67478
|
+
return recurrence ? { cronExpr: schedule.cronExpr, timezone: schedule.timezone, recurrence } : null;
|
|
67479
|
+
}
|
|
67480
|
+
function latestFiring(ctx, projectId, enabledSince) {
|
|
67481
|
+
const scheduled = and75(seoSyncRuns(projectId), eq89(runs.trigger, RunTriggers.scheduled), gt8(runs.createdAt, enabledSince));
|
|
67482
|
+
const newest = ctx.db.select({ createdAt: runs.createdAt }).from(runs).where(scheduled).orderBy(desc42(runs.createdAt)).limit(1).get();
|
|
67483
|
+
if (!newest) return [];
|
|
67484
|
+
const newestMs = Date.parse(newest.createdAt);
|
|
67485
|
+
const after = Number.isFinite(newestMs) ? new Date(newestMs - FIRING_SPAN_MS).toISOString() : newest.createdAt;
|
|
67486
|
+
return ctx.db.select({ id: runs.id, status: runs.status, error: runs.error, createdAt: runs.createdAt }).from(runs).where(and75(scheduled, gt8(runs.createdAt, after))).orderBy(asc20(runs.createdAt), asc20(runs.id)).all();
|
|
67487
|
+
}
|
|
67488
|
+
function lastCompleted(ctx, projectId) {
|
|
67489
|
+
const at = sql32`coalesce(${runs.finishedAt}, ${runs.createdAt})`;
|
|
67490
|
+
return ctx.db.select({ id: runs.id, at }).from(runs).where(and75(seoSyncRuns(projectId), eq89(runs.status, RunStatuses.completed))).orderBy(desc42(at)).limit(1).get() ?? null;
|
|
67491
|
+
}
|
|
67492
|
+
function isIncomplete(run) {
|
|
67493
|
+
return run.status === RunStatuses.failed || run.status === RunStatuses.partial;
|
|
67494
|
+
}
|
|
67495
|
+
function runProfiles(ctx, firing) {
|
|
67496
|
+
const jobs = firing.length === 0 ? [] : ctx.db.select({ runId: seoJobs.runId, profileId: seoJobs.profileId }).from(seoJobs).where(inArray39(seoJobs.runId, firing.map((run) => run.id))).all();
|
|
67497
|
+
const byRun = new Map(jobs.flatMap((job) => job.runId ? [[job.runId, job.profileId]] : []));
|
|
67498
|
+
return new Map(firing.map((run) => [run.id, byRun.get(run.id) ?? skippedRefreshProfile(run.error)]));
|
|
67499
|
+
}
|
|
67500
|
+
function unsettledRuns(ctx, projectId, firing, profiles) {
|
|
67501
|
+
const incomplete = firing.filter(isIncomplete);
|
|
67502
|
+
const firedAt = firing[firing.length - 1]?.createdAt;
|
|
67503
|
+
if (incomplete.length === 0 || !firedAt) return incomplete;
|
|
67504
|
+
const refreshed = new Set(ctx.db.select({ profileId: seoJobs.profileId }).from(runs).innerJoin(seoJobs, eq89(seoJobs.runId, runs.id)).where(and75(seoSyncRuns(projectId), eq89(runs.status, RunStatuses.completed), gt8(runs.createdAt, firedAt))).all().map((row) => row.profileId));
|
|
67505
|
+
return incomplete.filter((run) => {
|
|
67506
|
+
const profile = profiles.get(run.id) ?? null;
|
|
67507
|
+
return profile === null ? refreshed.size === 0 : !refreshed.has(profile);
|
|
67508
|
+
});
|
|
67509
|
+
}
|
|
67510
|
+
var COUNT_LIST = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
|
|
67511
|
+
function refreshReason(ctx, run) {
|
|
67512
|
+
const stored = parseRunError(run.error);
|
|
67513
|
+
if (stored) return formatRunErrorOneLine(stored);
|
|
67514
|
+
const tasks = currentSeoTasks(ctx.db.select({ id: seoTasks.id, retryOf: seoTasks.retryOf, status: seoTasks.status, error: seoTasks.error, request: seoTasks.request }).from(seoTasks).innerJoin(seoJobs, eq89(seoTasks.jobId, seoJobs.id)).where(eq89(seoJobs.runId, run.id)).all());
|
|
67515
|
+
const failed = tasks.filter((task) => task.status === SeoRunStatuses.failed || task.status === SeoRunStatuses["reconciliation-required"]);
|
|
67516
|
+
const partial = tasks.filter((task) => task.status === SeoRunStatuses.partial).length;
|
|
67517
|
+
const cancelled = tasks.filter((task) => task.status === SeoRunStatuses.cancelled).length;
|
|
67518
|
+
const errors = [...new Set(failed.flatMap((task) => task.error ? [task.error] : []))].join(" ");
|
|
67519
|
+
if (tasks.length <= 1) {
|
|
67520
|
+
if (errors) return errors;
|
|
67521
|
+
if (partial > 0) return "Some SEO results could not be collected.";
|
|
67522
|
+
return cancelled > 0 ? "The refresh was cancelled." : "No reason was recorded.";
|
|
67523
|
+
}
|
|
67524
|
+
const unit = tasks.every((task) => task.request.capability === SeoCapabilities["market-rankings"]) ? "searches" : "requests";
|
|
67525
|
+
const counts2 = [
|
|
67526
|
+
...failed.length > 0 ? [`${failed.length} failed`] : [],
|
|
67527
|
+
...partial > 0 ? [`${partial} returned only part of their results`] : [],
|
|
67528
|
+
...cancelled > 0 ? [`${cancelled} did not run`] : []
|
|
67529
|
+
];
|
|
67530
|
+
if (counts2.length === 0) return "No reason was recorded.";
|
|
67531
|
+
return `Of ${tasks.length} ${unit}, ${COUNT_LIST.format(counts2)}.${errors ? ` ${errors}` : ""}`;
|
|
67532
|
+
}
|
|
67533
|
+
function gradeSeoRefresh(ctx, project) {
|
|
67534
|
+
const schedule = ctx.db.select().from(schedules).where(and75(eq89(schedules.projectId, project.id), eq89(schedules.kind, SchedulableRunKinds["seo-sync"]))).get();
|
|
67535
|
+
if (!schedule) return skipped2("seo.data.not-scheduled", "No SEO refresh schedule is set for this project.");
|
|
67536
|
+
if (!schedule.enabled) return skipped2("seo.data.not-scheduled", "The SEO refresh schedule is disabled.");
|
|
67537
|
+
const name = project.name;
|
|
67538
|
+
const day = (iso) => formatIsoDateInTimeZone(iso, schedule.timezone);
|
|
67539
|
+
const now = Date.now();
|
|
67540
|
+
const timing = scheduleTiming(schedule);
|
|
67541
|
+
const last = lastCompleted(ctx, project.id);
|
|
67542
|
+
const enabledSince = schedule.enabledAt ?? schedule.createdAt;
|
|
67543
|
+
const reference = last && Date.parse(last.at) > Date.parse(enabledSince) ? last.at : enabledSince;
|
|
67544
|
+
const dueAt = timing ? nextRunFromSchedule(timing, new Date(reference)) : null;
|
|
67545
|
+
const deadlineAt = dueAt ? new Date(Date.parse(dueAt) + GRACE_DAYS * DAY_MS5).toISOString() : null;
|
|
67546
|
+
const firing = latestFiring(ctx, project.id, enabledSince);
|
|
67547
|
+
const profiles = runProfiles(ctx, firing);
|
|
67548
|
+
const details = {
|
|
67549
|
+
scheduleId: schedule.id,
|
|
67550
|
+
cronExpr: schedule.cronExpr || null,
|
|
67551
|
+
recurrence: schedule.recurrence ?? null,
|
|
67552
|
+
timezone: schedule.timezone,
|
|
67553
|
+
enabledSince,
|
|
67554
|
+
graceDays: GRACE_DAYS,
|
|
67555
|
+
lastCompletedAt: last?.at ?? null,
|
|
67556
|
+
lastCompletedRunId: last?.id ?? null,
|
|
67557
|
+
dueAt,
|
|
67558
|
+
deadlineAt,
|
|
67559
|
+
latestScheduledRuns: firing.map((run) => ({
|
|
67560
|
+
id: run.id,
|
|
67561
|
+
profileId: profiles.get(run.id) ?? null,
|
|
67562
|
+
status: run.status,
|
|
67563
|
+
createdAt: run.createdAt,
|
|
67564
|
+
reason: isIncomplete(run) ? refreshReason(ctx, run) : null
|
|
67565
|
+
}))
|
|
67566
|
+
};
|
|
67567
|
+
if (!timing || !dueAt || !deadlineAt) {
|
|
67568
|
+
return {
|
|
67569
|
+
status: CheckStatuses.warn,
|
|
67570
|
+
code: "seo.data.schedule-invalid",
|
|
67571
|
+
summary: "The SEO refresh schedule has no time it can run, so no scheduled SEO refresh will start.",
|
|
67572
|
+
remediation: `Set a schedule that can run, for example \`canonry schedule set ${name} --kind seo-sync --cron '0 7 2 * *' --timezone UTC\`.`,
|
|
67573
|
+
details,
|
|
67574
|
+
alert: {
|
|
67575
|
+
label: "SEO schedule invalid",
|
|
67576
|
+
line: "The SEO refresh schedule has no time it can run, so SEO data never refreshes.",
|
|
67577
|
+
next: `Set it again with canonry schedule set ${name} --kind seo-sync.`
|
|
67578
|
+
}
|
|
67579
|
+
};
|
|
67580
|
+
}
|
|
67581
|
+
const nextAt = nextRunFromSchedule(timing, new Date(now));
|
|
67582
|
+
const unsettled = unsettledRuns(ctx, project.id, firing, profiles);
|
|
67583
|
+
const newestFired = firing[firing.length - 1];
|
|
67584
|
+
if (newestFired && unsettled.length > 0) {
|
|
67585
|
+
const failed = unsettled.some((run) => run.status === RunStatuses.failed);
|
|
67586
|
+
const firedOn = day(newestFired.createdAt);
|
|
67587
|
+
const reasons = [...new Set(unsettled.map((run) => refreshReason(ctx, run)))].join(" ");
|
|
67588
|
+
const reason = reasons.length > REASON_LIMIT ? `${truncateUtf16(reasons, REASON_LIMIT)}...` : reasons;
|
|
67589
|
+
const outcome2 = firing.length === 1 ? `The scheduled SEO refresh on ${firedOn} ${failed ? "failed" : "only partly completed"}` : `${unsettled.length} of ${firing.length} profile refreshes in the scheduled SEO refresh on ${firedOn} did not fully complete`;
|
|
67590
|
+
const line = firing.length > 1 ? `${outcome2}.` : unsettled[0].error ? reason : `${outcome2}: ${reason}`;
|
|
67591
|
+
const next = nextAt ? ` on ${day(nextAt)}` : "";
|
|
67592
|
+
return {
|
|
67593
|
+
status: CheckStatuses.warn,
|
|
67594
|
+
code: failed ? "seo.data.refresh-failed" : "seo.data.refresh-partial",
|
|
67595
|
+
summary: `${outcome2}. Reason: ${reason} ${last ? `The last completed SEO refresh was on ${day(last.at)}.` : "No SEO refresh has completed yet."}`,
|
|
67596
|
+
remediation: `Fix what the reason names, then refresh now with \`canonry seo refresh-intent ${name} <profile-id>\` and \`canonry seo sync\`, or wait for the next scheduled refresh${next}. A completed refresh of the same profile clears this warning.`,
|
|
67597
|
+
details,
|
|
67598
|
+
alert: {
|
|
67599
|
+
label: failed ? "SEO refresh failed" : "SEO refresh incomplete",
|
|
67600
|
+
line: firstSentence(line) ?? line,
|
|
67601
|
+
next: `Fix the cause, then refresh by hand or wait for the next scheduled refresh${next}.`
|
|
67602
|
+
}
|
|
67603
|
+
};
|
|
67604
|
+
}
|
|
67605
|
+
if (now >= Date.parse(deadlineAt)) {
|
|
67606
|
+
const active = ctx.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and75(seoSyncRuns(project.id), inArray39(runs.status, [RunStatuses.queued, RunStatuses.running]))).orderBy(asc20(runs.createdAt)).limit(1).get();
|
|
67607
|
+
const since = last ? `No SEO refresh has completed since ${day(last.at)}.` : "No SEO refresh has completed since the schedule was enabled.";
|
|
67608
|
+
const missed = last ? `The scheduled refresh due ${day(dueAt)} did not complete within ${GRACE_DAYS} days.` : `Its first refresh, due ${day(dueAt)}, did not complete within ${GRACE_DAYS} days.`;
|
|
67609
|
+
return {
|
|
67610
|
+
status: CheckStatuses.warn,
|
|
67611
|
+
code: "seo.data.overdue",
|
|
67612
|
+
summary: `${since} ${missed}${active ? ` A refresh started ${day(active.createdAt)} has not finished, and scheduled refreshes wait for it.` : ""}`,
|
|
67613
|
+
remediation: active ? `Check that refresh with \`canonry seo run show ${name} ${active.id}\`, and cancel it with \`canonry seo run cancel ${name} ${active.id}\` if it is stuck.` : `Check that the server was running on ${day(dueAt)} and what \`canonry runs ${name} --kind seo-sync\` shows for that day. To refresh now, run \`canonry seo refresh-intent ${name} <profile-id>\`, then \`canonry seo sync\`.`,
|
|
67614
|
+
details: { ...details, activeRunId: active?.id ?? null },
|
|
67615
|
+
alert: {
|
|
67616
|
+
label: "SEO refresh overdue",
|
|
67617
|
+
line: since,
|
|
67618
|
+
next: active ? "Check the refresh that has not finished, and cancel it if it is stuck." : `Check that the server was up on ${day(dueAt)}, then refresh by hand.`
|
|
67619
|
+
}
|
|
67620
|
+
};
|
|
67621
|
+
}
|
|
67622
|
+
const pending = Date.parse(dueAt) <= now;
|
|
67623
|
+
if (!last) {
|
|
67624
|
+
return {
|
|
67625
|
+
status: CheckStatuses.ok,
|
|
67626
|
+
code: "seo.data.awaiting-first-refresh",
|
|
67627
|
+
summary: pending ? `No SEO refresh has completed yet. The first scheduled refresh, due ${day(dueAt)}, has until ${day(deadlineAt)} to complete.` : `No SEO refresh has completed yet. The first scheduled refresh is due ${day(dueAt)}.`,
|
|
67628
|
+
remediation: null,
|
|
67629
|
+
details
|
|
67630
|
+
};
|
|
67631
|
+
}
|
|
67632
|
+
return {
|
|
67633
|
+
status: CheckStatuses.ok,
|
|
67634
|
+
code: "seo.data.fresh",
|
|
67635
|
+
summary: `The last SEO refresh completed on ${day(last.at)}. ${pending ? `The scheduled refresh due ${day(dueAt)} has until ${day(deadlineAt)} to complete.` : `The next scheduled refresh is due ${day(dueAt)}.`}`,
|
|
67636
|
+
remediation: null,
|
|
67637
|
+
details
|
|
67638
|
+
};
|
|
67639
|
+
}
|
|
67640
|
+
var seoRecentRefreshCheck = {
|
|
67641
|
+
id: "seo.data.recent-refresh",
|
|
67642
|
+
category: CheckCategories.integrations,
|
|
67643
|
+
scope: CheckScopes.project,
|
|
67644
|
+
title: "SEO data still refreshing",
|
|
67645
|
+
run: (ctx) => {
|
|
67646
|
+
if (!ctx.project) return skipped2("seo.data.no-project", "Project context required.");
|
|
67647
|
+
return gradeSeoRefresh(ctx, ctx.project);
|
|
67648
|
+
}
|
|
67649
|
+
};
|
|
67650
|
+
var SEO_REFRESH_CHECKS = [seoRecentRefreshCheck];
|
|
67651
|
+
|
|
67254
67652
|
// ../api-routes/src/site-reachability.ts
|
|
67255
67653
|
import dns3 from "dns/promises";
|
|
67256
67654
|
import http3 from "http";
|
|
@@ -67454,7 +67852,7 @@ function probeHostFromDomain(value) {
|
|
|
67454
67852
|
if (!hostname || !hostname.includes(".") || hostname.endsWith(".localhost")) return null;
|
|
67455
67853
|
return hostname;
|
|
67456
67854
|
}
|
|
67457
|
-
var
|
|
67855
|
+
var skipped3 = (code, summary3) => ({ status: CheckStatuses.skipped, code, summary: summary3, remediation: null });
|
|
67458
67856
|
var siteReachabilityCheck = {
|
|
67459
67857
|
id: SITE_REACHABILITY_CHECK_ID,
|
|
67460
67858
|
category: CheckCategories.integrations,
|
|
@@ -67465,16 +67863,16 @@ var siteReachabilityCheck = {
|
|
|
67465
67863
|
// `canonry doctor --project` must not exit 1 on a single dropped packet.
|
|
67466
67864
|
optIn: true,
|
|
67467
67865
|
run: async (ctx) => {
|
|
67468
|
-
if (!ctx.project) return
|
|
67866
|
+
if (!ctx.project) return skipped3("site.reachability.no-project", "Project context required.");
|
|
67469
67867
|
const host = probeHostFromDomain(ctx.project.canonicalDomain);
|
|
67470
67868
|
if (!host) {
|
|
67471
|
-
return
|
|
67869
|
+
return skipped3("site.reachability.no-domain", `Project domain "${(ctx.project.canonicalDomain ?? "").trim()}" is not a probeable hostname.`);
|
|
67472
67870
|
}
|
|
67473
67871
|
const url = `https://${host}/`;
|
|
67474
67872
|
const probe = ctx.probeSiteReachability ?? probeSiteReachability;
|
|
67475
67873
|
const result = await probe(url);
|
|
67476
67874
|
if (result.state === "unavailable") {
|
|
67477
|
-
return
|
|
67875
|
+
return skipped3("site.reachability.probe-unavailable", `Could not probe ${url} from this host: ${result.reason}.`);
|
|
67478
67876
|
}
|
|
67479
67877
|
if (result.state === "up") {
|
|
67480
67878
|
return {
|
|
@@ -67515,7 +67913,7 @@ var siteReachabilityCheck = {
|
|
|
67515
67913
|
var SITE_REACHABILITY_CHECKS = [siteReachabilityCheck];
|
|
67516
67914
|
|
|
67517
67915
|
// ../api-routes/src/doctor/checks/api-hosts.ts
|
|
67518
|
-
import { eq as
|
|
67916
|
+
import { eq as eq91 } from "drizzle-orm";
|
|
67519
67917
|
|
|
67520
67918
|
// ../api-routes/src/api-host-probe.ts
|
|
67521
67919
|
import dns4 from "dns/promises";
|
|
@@ -67621,7 +68019,7 @@ async function probeApiHost(host, options = {}) {
|
|
|
67621
68019
|
}
|
|
67622
68020
|
|
|
67623
68021
|
// ../api-routes/src/doctor/checks/integration-sync.ts
|
|
67624
|
-
import { and as
|
|
68022
|
+
import { and as and76, count as count7, desc as desc43, eq as eq90, gte as gte23, gt as gt9, inArray as inArray40, isNull as isNull14, notInArray as notInArray3, or as or19 } from "drizzle-orm";
|
|
67625
68023
|
var SYNC_FAILURE_WINDOW = 3;
|
|
67626
68024
|
var SUMMARY_ERROR_LIMIT = 300;
|
|
67627
68025
|
var FINISHED_STATUSES = [RunStatuses.completed, RunStatuses.partial, RunStatuses.failed];
|
|
@@ -67642,7 +68040,7 @@ function gscConnection(ctx, project) {
|
|
|
67642
68040
|
function gbpConnection(ctx, project) {
|
|
67643
68041
|
if (!ctx.googleConnectionStore) return { state: "store-unavailable" };
|
|
67644
68042
|
if (!ctx.googleConnectionStore.getConnection(project.canonicalDomain, "gbp")) return { state: "not-connected" };
|
|
67645
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName }).from(gbpLocations).where(
|
|
68043
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName }).from(gbpLocations).where(and76(eq90(gbpLocations.projectId, project.id), eq90(gbpLocations.selected, true))).limit(1).get();
|
|
67646
68044
|
return selected ? CONNECTED : { state: "no-selected-locations" };
|
|
67647
68045
|
}
|
|
67648
68046
|
function gaConnection(ctx, project) {
|
|
@@ -67699,7 +68097,7 @@ var SPECS = [
|
|
|
67699
68097
|
causeCheckIds: ["google.auth.connection", "google.auth.property-access", "google.auth.scopes", "gsc.api.reachable"]
|
|
67700
68098
|
}
|
|
67701
68099
|
];
|
|
67702
|
-
function
|
|
68100
|
+
function skipped4(code, summary3) {
|
|
67703
68101
|
return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
|
|
67704
68102
|
}
|
|
67705
68103
|
function latestErrorText(raw) {
|
|
@@ -67707,16 +68105,16 @@ function latestErrorText(raw) {
|
|
|
67707
68105
|
return parsed ? formatRunErrorOneLine(parsed) : "no error was recorded";
|
|
67708
68106
|
}
|
|
67709
68107
|
function readSyncRunState(db, projectId, kind, options = {}) {
|
|
67710
|
-
const scope =
|
|
67711
|
-
|
|
67712
|
-
|
|
67713
|
-
...options.sourceId ? [
|
|
68108
|
+
const scope = and76(
|
|
68109
|
+
eq90(runs.projectId, projectId),
|
|
68110
|
+
eq90(runs.kind, kind),
|
|
68111
|
+
...options.sourceId ? [eq90(runs.sourceId, options.sourceId)] : [],
|
|
67714
68112
|
...options.nothingToSyncErrors?.length ? [or19(isNull14(runs.error), notInArray3(runs.error, [...options.nothingToSyncErrors]))] : []
|
|
67715
68113
|
);
|
|
67716
|
-
const graded = options.since ?
|
|
67717
|
-
const recent = db.select({ id: runs.id, status: runs.status, trigger: runs.trigger, error: runs.error, createdAt: runs.createdAt }).from(runs).where(
|
|
67718
|
-
const lastSuccess = db.select({ createdAt: runs.createdAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
67719
|
-
const failureCount = db.select({ count: count7() }).from(runs).where(
|
|
68114
|
+
const graded = options.since ? and76(scope, gte23(runs.createdAt, options.since)) : scope;
|
|
68115
|
+
const recent = db.select({ id: runs.id, status: runs.status, trigger: runs.trigger, error: runs.error, createdAt: runs.createdAt }).from(runs).where(and76(graded, inArray40(runs.status, FINISHED_STATUSES))).orderBy(desc43(runs.createdAt)).limit(SYNC_FAILURE_WINDOW).all();
|
|
68116
|
+
const lastSuccess = db.select({ createdAt: runs.createdAt, finishedAt: runs.finishedAt }).from(runs).where(and76(graded, inArray40(runs.status, SUCCESS_STATUSES))).orderBy(desc43(runs.createdAt)).limit(1).get();
|
|
68117
|
+
const failureCount = db.select({ count: count7() }).from(runs).where(and76(lastSuccess ? and76(graded, gt9(runs.createdAt, lastSuccess.createdAt)) : graded, eq90(runs.status, RunStatuses.failed))).get();
|
|
67720
68118
|
const failuresSinceSuccess = Number(failureCount?.count ?? 0);
|
|
67721
68119
|
const latest = recent[0];
|
|
67722
68120
|
return {
|
|
@@ -67733,21 +68131,21 @@ function readSyncRunState(db, projectId, kind, options = {}) {
|
|
|
67733
68131
|
function gradeSyncFailures(ctx, spec) {
|
|
67734
68132
|
const code = (suffix) => `${spec.prefix}.sync.${suffix}`;
|
|
67735
68133
|
const project = ctx.project;
|
|
67736
|
-
if (!project) return
|
|
68134
|
+
if (!project) return skipped4(code("no-project"), "Project context required.");
|
|
67737
68135
|
const connection = spec.connection(ctx, project);
|
|
67738
68136
|
if (connection.state === "store-unavailable") {
|
|
67739
|
-
return
|
|
68137
|
+
return skipped4(code("store-unavailable"), `No ${spec.label} connection store configured for this deployment.`);
|
|
67740
68138
|
}
|
|
67741
68139
|
if (connection.state === "not-connected") {
|
|
67742
|
-
return
|
|
68140
|
+
return skipped4(code("not-connected"), `${spec.label} is not connected for this project.`);
|
|
67743
68141
|
}
|
|
67744
68142
|
if (connection.state === "no-selected-locations") {
|
|
67745
|
-
return
|
|
68143
|
+
return skipped4(code("no-selected-locations"), `No ${spec.label} locations are selected for this project.`);
|
|
67746
68144
|
}
|
|
67747
68145
|
const nothingToSync = spec.nothingToSyncErrors ?? [];
|
|
67748
68146
|
const state = readSyncRunState(ctx.db, project.id, spec.kind, { since: connection.since, nothingToSyncErrors: nothingToSync });
|
|
67749
68147
|
if (state.latestStatus === null) {
|
|
67750
|
-
return
|
|
68148
|
+
return skipped4(code("no-runs"), `${spec.label} is connected but no sync has finished yet.`);
|
|
67751
68149
|
}
|
|
67752
68150
|
const latestFailed = state.latestStatus === RunStatuses.failed;
|
|
67753
68151
|
const details = {
|
|
@@ -67803,7 +68201,7 @@ var API_HOST_TABLE = {
|
|
|
67803
68201
|
ads: ["api.ads.openai.com"]
|
|
67804
68202
|
};
|
|
67805
68203
|
var API_HOST_EXCLUDED = { gsc: [new URL(INDEXING_API_BASE).hostname] };
|
|
67806
|
-
function
|
|
68204
|
+
function skipped5(code, summary3) {
|
|
67807
68205
|
return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
|
|
67808
68206
|
}
|
|
67809
68207
|
function connectionFor(prefix, ctx, project) {
|
|
@@ -67813,7 +68211,7 @@ function connectionFor(prefix, ctx, project) {
|
|
|
67813
68211
|
if (prefix === "google-ads") return googleMarketingConnection("googleAds")(ctx);
|
|
67814
68212
|
if (prefix === "gtm") return googleMarketingConnection("gtm")(ctx);
|
|
67815
68213
|
if (prefix === "bing") return ctx.bingConnectionStore?.getConnection(project.canonicalDomain) ? { state: "connected", since: null } : { state: "not-connected" };
|
|
67816
|
-
return ctx.db.select({ id: adsConnections.id }).from(adsConnections).where(
|
|
68214
|
+
return ctx.db.select({ id: adsConnections.id }).from(adsConnections).where(eq91(adsConnections.projectId, project.id)).limit(1).get() ? { state: "connected", since: null } : { state: "not-connected" };
|
|
67817
68215
|
}
|
|
67818
68216
|
function syncCommand(prefix) {
|
|
67819
68217
|
if (prefix === "gsc") return "google sync";
|
|
@@ -67827,9 +68225,9 @@ function syncCommand(prefix) {
|
|
|
67827
68225
|
async function grade2(prefix, ctx) {
|
|
67828
68226
|
const id = `${prefix}.api`;
|
|
67829
68227
|
const checkId = `${id}.reachable`;
|
|
67830
|
-
if (!ctx.project) return
|
|
68228
|
+
if (!ctx.project) return skipped5(`${id}.no-project`, "Project context required.");
|
|
67831
68229
|
const connection = connectionFor(prefix, ctx, ctx.project);
|
|
67832
|
-
if (connection.state !== "connected") return
|
|
68230
|
+
if (connection.state !== "connected") return skipped5(`${id}.not-connected`, `${prefix} is not connected for this project.`);
|
|
67833
68231
|
const probe = ctx.probeApiHost ?? probeApiHost;
|
|
67834
68232
|
const results = await Promise.all(API_HOST_TABLE[prefix].map((host) => probe(host)));
|
|
67835
68233
|
const details = { hosts: results };
|
|
@@ -67858,7 +68256,7 @@ function definition(prefix) {
|
|
|
67858
68256
|
var API_HOST_CHECKS = Object.keys(API_HOST_TABLE).map(definition);
|
|
67859
68257
|
|
|
67860
68258
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
67861
|
-
import { and as
|
|
68259
|
+
import { and as and77, eq as eq92 } from "drizzle-orm";
|
|
67862
68260
|
var RECENT_SYNC_WARN_DAYS = 4;
|
|
67863
68261
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
67864
68262
|
function skippedNoProject3() {
|
|
@@ -67878,7 +68276,7 @@ function storeUnavailable() {
|
|
|
67878
68276
|
};
|
|
67879
68277
|
}
|
|
67880
68278
|
function hasSelectedLocation(ctx, projectId) {
|
|
67881
|
-
return !!ctx.db.select({ id: gbpLocations.id }).from(gbpLocations).where(
|
|
68279
|
+
return !!ctx.db.select({ id: gbpLocations.id }).from(gbpLocations).where(and77(eq92(gbpLocations.projectId, projectId), eq92(gbpLocations.selected, true))).limit(1).get();
|
|
67882
68280
|
}
|
|
67883
68281
|
async function resolveGbpToken(ctx) {
|
|
67884
68282
|
if (!ctx.project) return { ok: false, output: skippedNoProject3() };
|
|
@@ -68138,7 +68536,7 @@ var recentSyncCheck = {
|
|
|
68138
68536
|
title: "GBP recent sync",
|
|
68139
68537
|
run: (ctx) => {
|
|
68140
68538
|
if (!ctx.project) return skippedNoProject3();
|
|
68141
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
68539
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and77(eq92(gbpLocations.projectId, ctx.project.id), eq92(gbpLocations.selected, true))).all();
|
|
68142
68540
|
if (selected.length === 0) {
|
|
68143
68541
|
return {
|
|
68144
68542
|
status: CheckStatuses.skipped,
|
|
@@ -68198,7 +68596,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
68198
68596
|
);
|
|
68199
68597
|
|
|
68200
68598
|
// ../api-routes/src/doctor/checks/places.ts
|
|
68201
|
-
import { eq as
|
|
68599
|
+
import { eq as eq93 } from "drizzle-orm";
|
|
68202
68600
|
var apiKeyCheck = {
|
|
68203
68601
|
id: "gbp.places.api-key",
|
|
68204
68602
|
category: CheckCategories.auth,
|
|
@@ -68243,7 +68641,7 @@ var apiKeyCheck = {
|
|
|
68243
68641
|
details: { tier: cfg.tier }
|
|
68244
68642
|
};
|
|
68245
68643
|
}
|
|
68246
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
68644
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq93(gbpLocations.projectId, ctx.project.id)).all();
|
|
68247
68645
|
const selected = rows.filter((r) => r.selected);
|
|
68248
68646
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
68249
68647
|
const details = {
|
|
@@ -68276,7 +68674,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
|
68276
68674
|
);
|
|
68277
68675
|
|
|
68278
68676
|
// ../api-routes/src/doctor/checks/gbp-reviews.ts
|
|
68279
|
-
import { and as
|
|
68677
|
+
import { and as and78, desc as desc44, eq as eq94, gte as gte24, inArray as inArray41, sql as sql33 } from "drizzle-orm";
|
|
68280
68678
|
var REVIEW_EVENTS = /* @__PURE__ */ new Set(["review.negative", "review.rating-dropped"]);
|
|
68281
68679
|
var V4_ACCESS_REMEDIATION = "Business Profile reviews use the legacy My Business API (mybusiness.googleapis.com), which Google enables per Cloud project only on request; the API Library and `gcloud services enable` cannot turn it on. Open the enable link in the API access approval email as the approved account, or reply to the access-request thread asking Google to enable mybusiness.googleapis.com for your project number, then run `canonry gbp sync`.";
|
|
68282
68680
|
var reviewsAccessCheck = {
|
|
@@ -68304,7 +68702,7 @@ var reviewsAccessCheck = {
|
|
|
68304
68702
|
reviewsAccess: gbpLocations.reviewsAccess,
|
|
68305
68703
|
reviewsAccessReason: gbpLocations.reviewsAccessReason,
|
|
68306
68704
|
reviewsCheckedAt: gbpLocations.reviewsCheckedAt
|
|
68307
|
-
}).from(gbpLocations).where(
|
|
68705
|
+
}).from(gbpLocations).where(and78(eq94(gbpLocations.projectId, project.id), eq94(gbpLocations.selected, true))).all();
|
|
68308
68706
|
if (locations.length === 0) {
|
|
68309
68707
|
return {
|
|
68310
68708
|
status: CheckStatuses.skipped,
|
|
@@ -68324,7 +68722,7 @@ var reviewsAccessCheck = {
|
|
|
68324
68722
|
const places = ctx.getPlacesConfig?.();
|
|
68325
68723
|
const placesFallback = places !== void 0 && places.tier === "atmosphere" && Boolean(places.apiKey);
|
|
68326
68724
|
const v4Worked = new Set(
|
|
68327
|
-
ctx.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(
|
|
68725
|
+
ctx.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and78(eq94(gbpReviewRatings.projectId, project.id), eq94(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
|
|
68328
68726
|
);
|
|
68329
68727
|
const coverageOf = (loc) => {
|
|
68330
68728
|
if (businessProfileReviewsCover(loc.reviewsAccess ?? null, v4Worked.has(loc.locationName))) return "full";
|
|
@@ -68358,7 +68756,7 @@ var reviewsAccessCheck = {
|
|
|
68358
68756
|
none > 0 ? `${none} have no review source` : null
|
|
68359
68757
|
].filter((part) => part !== null);
|
|
68360
68758
|
const summary3 = `Business Profile reviews are not available${why}. ${parts.join("; ")}.`;
|
|
68361
|
-
const subscribed = ctx.db.select({ config: notifications.config, enabled: notifications.enabled }).from(notifications).where(
|
|
68759
|
+
const subscribed = ctx.db.select({ config: notifications.config, enabled: notifications.enabled }).from(notifications).where(eq94(notifications.projectId, project.id)).all().some((n) => n.enabled && (n.config.events ?? []).some((event) => REVIEW_EVENTS.has(event)));
|
|
68362
68760
|
const placesHint = placesFallback ? "" : " For partial coverage meanwhile, set a Places API key (`GOOGLE_PLACES_API_KEY` or `places.apiKey`) with `places.tier: atmosphere`.";
|
|
68363
68761
|
if (none > 0 && subscribed) {
|
|
68364
68762
|
return {
|
|
@@ -68389,7 +68787,7 @@ var reviewDeliveryCheck = {
|
|
|
68389
68787
|
return { status: CheckStatuses.skipped, code: "gbp.reviews.delivery.no-project", summary: "Project context required.", remediation: null };
|
|
68390
68788
|
}
|
|
68391
68789
|
const since = new Date(Date.now() - UNDELIVERED_ATTENTION_DAYS * 864e5).toISOString();
|
|
68392
|
-
const lastTouched =
|
|
68790
|
+
const lastTouched = sql33`coalesce(${reviewWebhookDeliveries.attemptedAt}, ${reviewWebhookDeliveries.createdAt})`;
|
|
68393
68791
|
const rows = ctx.db.select({
|
|
68394
68792
|
id: reviewWebhookDeliveries.id,
|
|
68395
68793
|
notificationId: reviewWebhookDeliveries.notificationId,
|
|
@@ -68397,11 +68795,11 @@ var reviewDeliveryCheck = {
|
|
|
68397
68795
|
attempts: reviewWebhookDeliveries.attempts,
|
|
68398
68796
|
lastTouched,
|
|
68399
68797
|
payload: reviewWebhookDeliveries.payload
|
|
68400
|
-
}).from(reviewWebhookDeliveries).where(
|
|
68401
|
-
|
|
68402
|
-
|
|
68798
|
+
}).from(reviewWebhookDeliveries).where(and78(
|
|
68799
|
+
eq94(reviewWebhookDeliveries.projectId, ctx.project.id),
|
|
68800
|
+
inArray41(reviewWebhookDeliveries.state, [ReviewDeliveryStates.Uncertain, ReviewDeliveryStates.Failed]),
|
|
68403
68801
|
gte24(lastTouched, since)
|
|
68404
|
-
)).orderBy(
|
|
68802
|
+
)).orderBy(desc44(lastTouched)).all();
|
|
68405
68803
|
if (rows.length === 0) {
|
|
68406
68804
|
return {
|
|
68407
68805
|
status: CheckStatuses.ok,
|
|
@@ -68410,7 +68808,7 @@ var reviewDeliveryCheck = {
|
|
|
68410
68808
|
remediation: null
|
|
68411
68809
|
};
|
|
68412
68810
|
}
|
|
68413
|
-
const destinations = new Map(ctx.db.select({ id: notifications.id, config: notifications.config }).from(notifications).where(
|
|
68811
|
+
const destinations = new Map(ctx.db.select({ id: notifications.id, config: notifications.config }).from(notifications).where(eq94(notifications.projectId, ctx.project.id)).all().map((row) => [row.id, redactNotificationUrl(row.config.url).urlDisplay]));
|
|
68414
68812
|
const deliveries = rows.map((row) => {
|
|
68415
68813
|
const review = row.payload.reviews[0];
|
|
68416
68814
|
return {
|
|
@@ -68984,7 +69382,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
68984
69382
|
];
|
|
68985
69383
|
|
|
68986
69384
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
68987
|
-
import { and as
|
|
69385
|
+
import { and as and79, desc as desc45, eq as eq95, gte as gte25, inArray as inArray42, isNotNull as isNotNull9, ne as ne16, sql as sql34 } from "drizzle-orm";
|
|
68988
69386
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
68989
69387
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
68990
69388
|
function isCloudflareDirectPush(source) {
|
|
@@ -69023,8 +69421,8 @@ function skippedNoProject5() {
|
|
|
69023
69421
|
function loadProbes(ctx) {
|
|
69024
69422
|
if (!ctx.project) return [];
|
|
69025
69423
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
69026
|
-
|
|
69027
|
-
|
|
69424
|
+
and79(
|
|
69425
|
+
eq95(trafficSources.projectId, ctx.project.id),
|
|
69028
69426
|
ne16(trafficSources.status, TrafficSourceStatuses.archived)
|
|
69029
69427
|
)
|
|
69030
69428
|
).all();
|
|
@@ -69161,28 +69559,28 @@ var recentDataCheck = {
|
|
|
69161
69559
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
69162
69560
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
69163
69561
|
const recentCrawlers = Number(
|
|
69164
|
-
ctx.db.select({ total:
|
|
69165
|
-
|
|
69166
|
-
|
|
69167
|
-
|
|
69562
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
69563
|
+
and79(
|
|
69564
|
+
eq95(crawlerEventsHourly.projectId, ctx.project.id),
|
|
69565
|
+
inArray42(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
69168
69566
|
gte25(crawlerEventsHourly.tsHour, warnCutoff)
|
|
69169
69567
|
)
|
|
69170
69568
|
).get()?.total ?? 0
|
|
69171
69569
|
);
|
|
69172
69570
|
const recentReferrals = Number(
|
|
69173
|
-
ctx.db.select({ total:
|
|
69174
|
-
|
|
69175
|
-
|
|
69176
|
-
|
|
69571
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
69572
|
+
and79(
|
|
69573
|
+
eq95(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
69574
|
+
inArray42(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
69177
69575
|
gte25(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
69178
69576
|
)
|
|
69179
69577
|
).get()?.total ?? 0
|
|
69180
69578
|
);
|
|
69181
69579
|
const recentUserFetches = Number(
|
|
69182
|
-
ctx.db.select({ total:
|
|
69183
|
-
|
|
69184
|
-
|
|
69185
|
-
|
|
69580
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
69581
|
+
and79(
|
|
69582
|
+
eq95(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
69583
|
+
inArray42(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
69186
69584
|
gte25(aiUserFetchEventsHourly.tsHour, warnCutoff)
|
|
69187
69585
|
)
|
|
69188
69586
|
).get()?.total ?? 0
|
|
@@ -69205,28 +69603,28 @@ var recentDataCheck = {
|
|
|
69205
69603
|
};
|
|
69206
69604
|
}
|
|
69207
69605
|
const olderCrawlers = Number(
|
|
69208
|
-
ctx.db.select({ total:
|
|
69209
|
-
|
|
69210
|
-
|
|
69211
|
-
|
|
69606
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
69607
|
+
and79(
|
|
69608
|
+
eq95(crawlerEventsHourly.projectId, ctx.project.id),
|
|
69609
|
+
inArray42(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
69212
69610
|
gte25(crawlerEventsHourly.tsHour, failCutoff)
|
|
69213
69611
|
)
|
|
69214
69612
|
).get()?.total ?? 0
|
|
69215
69613
|
);
|
|
69216
69614
|
const olderReferrals = Number(
|
|
69217
|
-
ctx.db.select({ total:
|
|
69218
|
-
|
|
69219
|
-
|
|
69220
|
-
|
|
69615
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
69616
|
+
and79(
|
|
69617
|
+
eq95(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
69618
|
+
inArray42(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
69221
69619
|
gte25(aiReferralEventsHourly.tsHour, failCutoff)
|
|
69222
69620
|
)
|
|
69223
69621
|
).get()?.total ?? 0
|
|
69224
69622
|
);
|
|
69225
69623
|
const olderUserFetches = Number(
|
|
69226
|
-
ctx.db.select({ total:
|
|
69227
|
-
|
|
69228
|
-
|
|
69229
|
-
|
|
69624
|
+
ctx.db.select({ total: sql34`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
69625
|
+
and79(
|
|
69626
|
+
eq95(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
69627
|
+
inArray42(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
69230
69628
|
gte25(aiUserFetchEventsHourly.tsHour, failCutoff)
|
|
69231
69629
|
)
|
|
69232
69630
|
).get()?.total ?? 0
|
|
@@ -69291,7 +69689,7 @@ async function runValidator(source, validator, fallbackId, fallbackLabel) {
|
|
|
69291
69689
|
function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
|
|
69292
69690
|
const failed = results.filter((r) => r.output.status === CheckStatuses.fail);
|
|
69293
69691
|
const warned = results.filter((r) => r.output.status === CheckStatuses.warn);
|
|
69294
|
-
const
|
|
69692
|
+
const skipped6 = results.filter((r) => r.output.status === CheckStatuses.skipped);
|
|
69295
69693
|
const ok = results.filter((r) => r.output.status === CheckStatuses.ok);
|
|
69296
69694
|
const detail = {
|
|
69297
69695
|
sources: results.map((r) => ({
|
|
@@ -69325,7 +69723,7 @@ function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
|
|
|
69325
69723
|
return {
|
|
69326
69724
|
status: CheckStatuses.ok,
|
|
69327
69725
|
code: `traffic.${fallbackId}.ok`,
|
|
69328
|
-
summary: `${ok.length} source(s) passed ${fallbackLabel} validation${
|
|
69726
|
+
summary: `${ok.length} source(s) passed ${fallbackLabel} validation${skipped6.length > 0 ? ` (${skipped6.length} skipped)` : ""}.`,
|
|
69329
69727
|
details: detail
|
|
69330
69728
|
};
|
|
69331
69729
|
}
|
|
@@ -69333,7 +69731,7 @@ function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
|
|
|
69333
69731
|
status: CheckStatuses.skipped,
|
|
69334
69732
|
code: `traffic.${fallbackId}.all-skipped`,
|
|
69335
69733
|
summary: `No source-type validator was available for any of the ${results.length} connected source(s).`,
|
|
69336
|
-
remediation:
|
|
69734
|
+
remediation: skipped6.find((result) => result.output.remediation)?.output.remediation,
|
|
69337
69735
|
details: detail
|
|
69338
69736
|
};
|
|
69339
69737
|
}
|
|
@@ -69552,12 +69950,12 @@ var syncLagCheck = {
|
|
|
69552
69950
|
}
|
|
69553
69951
|
};
|
|
69554
69952
|
function lastQueueStopReason(ctx, sourceId) {
|
|
69555
|
-
const row = ctx.db.select({ syncSummary: runs.syncSummary }).from(runs).where(
|
|
69556
|
-
|
|
69557
|
-
|
|
69558
|
-
|
|
69953
|
+
const row = ctx.db.select({ syncSummary: runs.syncSummary }).from(runs).where(and79(
|
|
69954
|
+
eq95(runs.projectId, ctx.project.id),
|
|
69955
|
+
eq95(runs.kind, RunKinds["traffic-sync"]),
|
|
69956
|
+
eq95(runs.sourceId, sourceId),
|
|
69559
69957
|
isNotNull9(runs.syncSummary)
|
|
69560
|
-
)).orderBy(
|
|
69958
|
+
)).orderBy(desc45(runs.createdAt)).limit(1).get();
|
|
69561
69959
|
return row?.syncSummary?.type === "cloudflare-queue" ? row.syncSummary.stopReason : null;
|
|
69562
69960
|
}
|
|
69563
69961
|
var QUEUE_LIMIT_STOPS = [
|
|
@@ -69847,7 +70245,7 @@ var GTM_REQUIRED_OAUTH_SCOPES = [
|
|
|
69847
70245
|
];
|
|
69848
70246
|
var FRESH_SNAPSHOT_WARN_DAYS = 7;
|
|
69849
70247
|
var FRESH_SNAPSHOT_FAIL_DAYS = 30;
|
|
69850
|
-
var
|
|
70248
|
+
var DAY_MS6 = 24 * 60 * 60 * 1e3;
|
|
69851
70249
|
function unavailableStatus() {
|
|
69852
70250
|
return {
|
|
69853
70251
|
status: CheckStatuses.skipped,
|
|
@@ -69934,7 +70332,7 @@ function snapshotOutput(provider, latestSnapshotAt, now) {
|
|
|
69934
70332
|
details: { latestSnapshotAt }
|
|
69935
70333
|
};
|
|
69936
70334
|
}
|
|
69937
|
-
const ageDays2 = (now.getTime() - snapshotMs) /
|
|
70335
|
+
const ageDays2 = (now.getTime() - snapshotMs) / DAY_MS6;
|
|
69938
70336
|
const details = { latestSnapshotAt, ageDays: Math.max(0, Math.round(ageDays2)) };
|
|
69939
70337
|
if (ageDays2 < 0) {
|
|
69940
70338
|
return {
|
|
@@ -70157,6 +70555,7 @@ var ALL_CHECKS = [
|
|
|
70157
70555
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
70158
70556
|
...GA_AUTH_CHECKS,
|
|
70159
70557
|
...DATA_FRESHNESS_CHECKS,
|
|
70558
|
+
...SEO_REFRESH_CHECKS,
|
|
70160
70559
|
reportSweepsCheck,
|
|
70161
70560
|
reportModelsCheck,
|
|
70162
70561
|
reportDailyDataCheck,
|
|
@@ -70354,7 +70753,7 @@ async function doctorRoutes(app, opts) {
|
|
|
70354
70753
|
}
|
|
70355
70754
|
|
|
70356
70755
|
// ../api-routes/src/status.ts
|
|
70357
|
-
import { and as
|
|
70756
|
+
import { and as and80, desc as desc46, eq as eq96, isNull as isNull15 } from "drizzle-orm";
|
|
70358
70757
|
function ageDays(iso) {
|
|
70359
70758
|
if (!iso) return null;
|
|
70360
70759
|
const ms = Date.now() - Date.parse(iso);
|
|
@@ -70404,7 +70803,7 @@ function makeRow(integration, label, runKind, state, staleAfterDays, dataThrough
|
|
|
70404
70803
|
};
|
|
70405
70804
|
}
|
|
70406
70805
|
function latestDate2(db, table, projectId) {
|
|
70407
|
-
const row = db.select({ date: table.date }).from(table).where(
|
|
70806
|
+
const row = db.select({ date: table.date }).from(table).where(eq96(table.projectId, projectId)).orderBy(desc46(table.date)).limit(1).get();
|
|
70408
70807
|
return row?.date ?? null;
|
|
70409
70808
|
}
|
|
70410
70809
|
function projectRows(opts, project) {
|
|
@@ -70419,7 +70818,7 @@ function projectRows(opts, project) {
|
|
|
70419
70818
|
const rows = [];
|
|
70420
70819
|
const gsc = gscConnection(ctx, info);
|
|
70421
70820
|
if (connected(gsc)) {
|
|
70422
|
-
const watermarkRow = opts.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(
|
|
70821
|
+
const watermarkRow = opts.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(eq96(gscDataWatermarks.projectId, project.id)).limit(1).get();
|
|
70423
70822
|
const state = readSyncRunState(opts.db, project.id, RunKinds["gsc-sync"], { since: gsc.since });
|
|
70424
70823
|
const dataThrough = watermarkRow?.dataThroughDate ?? latestDate2(opts.db, gscDailyTotals, project.id);
|
|
70425
70824
|
rows.push(makeRow("gsc", "Search Console", RunKinds["gsc-sync"], state, SYNC_IDLE_DAYS, dataThrough, null, ["gsc.sync.recent-failures"], GSC_DATA_AGING_DAYS));
|
|
@@ -70432,12 +70831,12 @@ function projectRows(opts, project) {
|
|
|
70432
70831
|
const gbp = gbpConnection(ctx, info);
|
|
70433
70832
|
if (connected(gbp)) rows.push(makeRow("gbp", "Google Business Profile", RunKinds["gbp-sync"], readSyncRunState(opts.db, project.id, RunKinds["gbp-sync"], { nothingToSyncErrors: GBP_NOTHING_TO_SYNC_ERRORS }), RECENT_SYNC_WARN_DAYS, null, null, ["gbp.sync.recent-failures"]));
|
|
70434
70833
|
if (opts.bingConnectionStore?.getConnection(project.canonicalDomain)) rows.push(makeRow("bing", "Bing Webmaster Tools", RunKinds["bing-inspect-sitemap"], readSyncRunState(opts.db, project.id, RunKinds["bing-inspect-sitemap"]), null));
|
|
70435
|
-
const ads = opts.db.select({ id: adsConnections.id, lastSyncedAt: adsConnections.lastSyncedAt }).from(adsConnections).where(
|
|
70834
|
+
const ads = opts.db.select({ id: adsConnections.id, lastSyncedAt: adsConnections.lastSyncedAt }).from(adsConnections).where(eq96(adsConnections.projectId, project.id)).limit(1).get();
|
|
70436
70835
|
if (ads) rows.push(makeRow("ads", "OpenAI Ads", RunKinds["ads-sync"], readSyncRunState(opts.db, project.id, RunKinds["ads-sync"]), ADS_RECENT_SYNC_WARN_DAYS, null, null, ["ads.data.recent-sync"], ADS_RECENT_SYNC_WARN_DAYS, ads.lastSyncedAt));
|
|
70437
70836
|
const marketing = opts.getGoogleMarketingDoctorInput?.(ctx);
|
|
70438
70837
|
if (marketing?.googleAds) rows.push(makeRow("google-ads", "Google Ads", RunKinds["google-ads-sync"], readSyncRunState(opts.db, project.id, RunKinds["google-ads-sync"]), FRESH_SNAPSHOT_WARN_DAYS, null, null, ["google-ads.sync.recent-failures"], FRESH_SNAPSHOT_WARN_DAYS, marketing.googleAds.latestSnapshotAt));
|
|
70439
70838
|
if (marketing?.gtm) rows.push(makeRow("gtm", "Google Tag Manager", RunKinds["gtm-sync"], readSyncRunState(opts.db, project.id, RunKinds["gtm-sync"]), FRESH_SNAPSHOT_WARN_DAYS, null, null, ["gtm.sync.recent-failures"], FRESH_SNAPSHOT_WARN_DAYS, marketing.gtm.latestSnapshotAt));
|
|
70440
|
-
const sources = opts.db.select().from(trafficSources).where(
|
|
70839
|
+
const sources = opts.db.select().from(trafficSources).where(and80(eq96(trafficSources.projectId, project.id), isNull15(trafficSources.archivedAt))).all();
|
|
70441
70840
|
for (const source of sources) {
|
|
70442
70841
|
const row = makeRow("traffic", source.displayName, RunKinds["traffic-sync"], readSyncRunState(opts.db, project.id, RunKinds["traffic-sync"], { sourceId: source.id }), null, null, source.id, ["traffic.source.sync-lag", "traffic.source.recent-data"]);
|
|
70443
70842
|
const lag = source.lastSyncedAt ? Date.now() - Date.parse(source.lastSyncedAt) : Number.POSITIVE_INFINITY;
|
|
@@ -70449,7 +70848,7 @@ function projectRows(opts, project) {
|
|
|
70449
70848
|
}
|
|
70450
70849
|
function projectStatus(opts, project) {
|
|
70451
70850
|
const integrations = projectRows(opts, project);
|
|
70452
|
-
const healthRow = opts.db.select().from(doctorHealthState).where(
|
|
70851
|
+
const healthRow = opts.db.select().from(doctorHealthState).where(eq96(doctorHealthState.projectId, project.id)).limit(1).get();
|
|
70453
70852
|
const health = healthRow ? { status: healthRow.status, code: healthRow.code, summary: healthRow.summary, checkedAt: healthRow.checkedAt, notifiedAt: healthRow.notifiedAt } : null;
|
|
70454
70853
|
const rank = { ok: 0, "never-synced": 1, warning: 2, stale: 3, failing: 4 };
|
|
70455
70854
|
const healthState = health?.status === "fail" ? "failing" : health?.status === "warn" ? "warning" : "ok";
|
|
@@ -70469,7 +70868,7 @@ async function statusRoutes(app, opts) {
|
|
|
70469
70868
|
if (request.query.problems === "true") result.projects = result.projects.map((item2) => ({ ...item2, integrations: item2.integrations.filter((row) => row.state !== "ok") }));
|
|
70470
70869
|
return instanceStatusDtoSchema.parse(result);
|
|
70471
70870
|
}
|
|
70472
|
-
const projectsRows = (scopedId ? opts.db.select().from(projects).where(
|
|
70871
|
+
const projectsRows = (scopedId ? opts.db.select().from(projects).where(eq96(projects.id, scopedId)) : opts.db.select().from(projects)).all();
|
|
70473
70872
|
const output = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...opts.getMcpHealth ? { mcp: opts.getMcpHealth() } : {}, summary: { projects: projectsRows.length, ok: 0, failing: 0, warning: 0, stale: 0, neverSynced: 0 }, projects: projectsRows.map((project) => projectStatus(opts, project)) };
|
|
70474
70873
|
for (const project of output.projects) {
|
|
70475
70874
|
const key = project.state === "never-synced" ? "neverSynced" : project.state;
|
|
@@ -70482,7 +70881,7 @@ async function statusRoutes(app, opts) {
|
|
|
70482
70881
|
|
|
70483
70882
|
// ../api-routes/src/discovery/routes.ts
|
|
70484
70883
|
import crypto54 from "crypto";
|
|
70485
|
-
import { and as
|
|
70884
|
+
import { and as and81, desc as desc47, eq as eq97, gte as gte26, inArray as inArray43, isNull as isNull16, or as or20 } from "drizzle-orm";
|
|
70486
70885
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
70487
70886
|
async function discoveryRoutes(app, opts) {
|
|
70488
70887
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -70516,13 +70915,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
70516
70915
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
70517
70916
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
70518
70917
|
const decision = app.db.transaction((tx) => {
|
|
70519
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
70520
|
-
|
|
70521
|
-
|
|
70918
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and81(
|
|
70919
|
+
eq97(discoverySessions.projectId, project.id),
|
|
70920
|
+
eq97(discoverySessions.icpDescription, icpDescription),
|
|
70522
70921
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
70523
70922
|
// semantics, so a request with a different (or no) buyer must start
|
|
70524
70923
|
// its own session, never adopt another buyer's probes.
|
|
70525
|
-
parsed.data.buyerDescription == null ? isNull16(discoverySessions.buyerDescription) :
|
|
70924
|
+
parsed.data.buyerDescription == null ? isNull16(discoverySessions.buyerDescription) : eq97(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
70526
70925
|
// Locations are identity too: a different service-area subset seeds
|
|
70527
70926
|
// and probes a different geo, so it must never reuse another geo's
|
|
70528
70927
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -70532,18 +70931,18 @@ async function discoveryRoutes(app, opts) {
|
|
|
70532
70931
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
70533
70932
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
70534
70933
|
// upgrade, never a wrong reuse.
|
|
70535
|
-
locations.length === 0 ? or20(isNull16(discoverySessions.locations),
|
|
70934
|
+
locations.length === 0 ? or20(isNull16(discoverySessions.locations), eq97(discoverySessions.locations, locations)) : eq97(discoverySessions.locations, locations),
|
|
70536
70935
|
// Seed provider set is identity: a different phrasing distribution
|
|
70537
70936
|
// must never reuse another set's session. Null (= Gemini-only
|
|
70538
70937
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
70539
|
-
seedProviders == null ? isNull16(discoverySessions.seedProviders) :
|
|
70540
|
-
|
|
70938
|
+
seedProviders == null ? isNull16(discoverySessions.seedProviders) : eq97(discoverySessions.seedProviders, seedProviders),
|
|
70939
|
+
inArray43(discoverySessions.status, [
|
|
70541
70940
|
DiscoverySessionStatuses.queued,
|
|
70542
70941
|
DiscoverySessionStatuses.seeding,
|
|
70543
70942
|
DiscoverySessionStatuses.probing
|
|
70544
70943
|
]),
|
|
70545
70944
|
gte26(discoverySessions.createdAt, ageFloorIso)
|
|
70546
|
-
)).orderBy(
|
|
70945
|
+
)).orderBy(desc47(discoverySessions.createdAt)).get();
|
|
70547
70946
|
if (existing && existing.runId) {
|
|
70548
70947
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
70549
70948
|
}
|
|
@@ -70612,7 +71011,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70612
71011
|
const project = resolveProject(app.db, request.params.name);
|
|
70613
71012
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
70614
71013
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
70615
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
71014
|
+
const rows = app.db.select().from(discoverySessions).where(eq97(discoverySessions.projectId, project.id)).orderBy(desc47(discoverySessions.createdAt)).limit(limit).all();
|
|
70616
71015
|
return reply.send(rows.map(serializeSession));
|
|
70617
71016
|
}
|
|
70618
71017
|
);
|
|
@@ -70620,11 +71019,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
70620
71019
|
"/projects/:name/discover/sessions/:id",
|
|
70621
71020
|
async (request, reply) => {
|
|
70622
71021
|
const project = resolveProject(app.db, request.params.name);
|
|
70623
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
71022
|
+
const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
|
|
70624
71023
|
if (!session || session.projectId !== project.id) {
|
|
70625
71024
|
throw notFound("Discovery session", request.params.id);
|
|
70626
71025
|
}
|
|
70627
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
71026
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
|
|
70628
71027
|
const detail = {
|
|
70629
71028
|
...serializeSession(session),
|
|
70630
71029
|
probes: probeRows.map(serializeProbe)
|
|
@@ -70636,7 +71035,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70636
71035
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
70637
71036
|
async (request, reply) => {
|
|
70638
71037
|
const project = resolveProject(app.db, request.params.name);
|
|
70639
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
71038
|
+
const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
|
|
70640
71039
|
if (!session || session.projectId !== project.id) {
|
|
70641
71040
|
throw notFound("Discovery session", request.params.id);
|
|
70642
71041
|
}
|
|
@@ -70644,7 +71043,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70644
71043
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
70645
71044
|
const applyAnchor = request.query.anchor !== "false";
|
|
70646
71045
|
const provider = session.seedProvider ?? "gemini";
|
|
70647
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
71046
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
|
|
70648
71047
|
const extract = opts.harvestSearchQueries;
|
|
70649
71048
|
const probesWithQueries = probeRows.map((row) => {
|
|
70650
71049
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -70655,7 +71054,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70655
71054
|
return { searchQueries: [] };
|
|
70656
71055
|
}
|
|
70657
71056
|
});
|
|
70658
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
71057
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq97(queries.projectId, project.id)).all().map((r) => r.query);
|
|
70659
71058
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
70660
71059
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
70661
71060
|
effectiveDomains(project)
|
|
@@ -70702,11 +71101,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
70702
71101
|
"/projects/:name/discover/sessions/:id/promote",
|
|
70703
71102
|
async (request, reply) => {
|
|
70704
71103
|
const project = resolveProject(app.db, request.params.name);
|
|
70705
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
71104
|
+
const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
|
|
70706
71105
|
if (!session || session.projectId !== project.id) {
|
|
70707
71106
|
throw notFound("Discovery session", request.params.id);
|
|
70708
71107
|
}
|
|
70709
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
71108
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
|
|
70710
71109
|
const cited = /* @__PURE__ */ new Set();
|
|
70711
71110
|
const aspirational = /* @__PURE__ */ new Set();
|
|
70712
71111
|
const wasted = /* @__PURE__ */ new Set();
|
|
@@ -70741,7 +71140,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70741
71140
|
);
|
|
70742
71141
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
70743
71142
|
const project = resolveProject(app.db, request.params.name);
|
|
70744
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
71143
|
+
const session = app.db.select().from(discoverySessions).where(eq97(discoverySessions.id, request.params.id)).get();
|
|
70745
71144
|
if (!session || session.projectId !== project.id) {
|
|
70746
71145
|
throw notFound("Discovery session", request.params.id);
|
|
70747
71146
|
}
|
|
@@ -70764,7 +71163,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70764
71163
|
const bucketSet = new Set(buckets);
|
|
70765
71164
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
70766
71165
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
70767
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
71166
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq97(discoveryProbes.sessionId, session.id)).all();
|
|
70768
71167
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
70769
71168
|
for (const probe of probeRows) {
|
|
70770
71169
|
if (!probe.bucket) continue;
|
|
@@ -70772,7 +71171,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
70772
71171
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
70773
71172
|
}
|
|
70774
71173
|
const existingQueries = new Set(
|
|
70775
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
71174
|
+
app.db.select({ query: queries.query }).from(queries).where(eq97(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
70776
71175
|
);
|
|
70777
71176
|
const promotedQueries = [];
|
|
70778
71177
|
const skippedQueries = [];
|
|
@@ -70944,21 +71343,21 @@ function planCompetitorPromotion(input) {
|
|
|
70944
71343
|
const preferred = new Set(input.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES);
|
|
70945
71344
|
const eligible = groupCompetitorMap(input.competitorMap, input.probes, preferred).filter((group) => group.hits >= DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS).filter((group) => !typeFilter || group.sources.some((source) => typeFilter.has(source.competitorType))).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain));
|
|
70946
71345
|
const tracked = new Set(input.tracked.map((row) => normalizeCompetitorDomain(row.domain)));
|
|
70947
|
-
const
|
|
71346
|
+
const skipped6 = [];
|
|
70948
71347
|
const candidates = [];
|
|
70949
71348
|
for (const group of eligible) {
|
|
70950
71349
|
const projectDomain = competitorDomainProjectClaim(group.domain, input.project.domains);
|
|
70951
71350
|
const subdomains = [...new Set(group.sources.map((source) => hostOf(source.domain) ?? source.domain))].filter((host) => host !== group.domain);
|
|
70952
71351
|
if (projectDomain) {
|
|
70953
|
-
|
|
71352
|
+
skipped6.push({
|
|
70954
71353
|
...group,
|
|
70955
71354
|
reason: DiscoveryPromoteCompetitorSkipReasons["project-domain"],
|
|
70956
71355
|
message: `${group.domain} overlaps the project domain ${projectDomain}, so every citation of the project would count for it`
|
|
70957
71356
|
});
|
|
70958
71357
|
} else if (tracked.has(group.domain)) {
|
|
70959
|
-
|
|
71358
|
+
skipped6.push({ ...group, reason: DiscoveryPromoteCompetitorSkipReasons["already-tracked"], message: `${group.domain} is already tracked` });
|
|
70960
71359
|
} else if (subdomains.length > 1) {
|
|
70961
|
-
|
|
71360
|
+
skipped6.push({
|
|
70962
71361
|
...group,
|
|
70963
71362
|
reason: DiscoveryPromoteCompetitorSkipReasons["shared-host"],
|
|
70964
71363
|
message: `${group.domain} was cited as ${subdomains.length} different subdomains (${subdomains.join(", ")}), which may be different sites on one shared host, so it is not merged into one competitor; if they are one competitor, add it with canonry competitor add <project> ${group.domain}`
|
|
@@ -70972,7 +71371,7 @@ function planCompetitorPromotion(input) {
|
|
|
70972
71371
|
for (const group of candidates) {
|
|
70973
71372
|
const rejections = claimed.get(group.domain);
|
|
70974
71373
|
if (rejections) {
|
|
70975
|
-
|
|
71374
|
+
skipped6.push({
|
|
70976
71375
|
...group,
|
|
70977
71376
|
reason: DiscoveryPromoteCompetitorSkipReasons["claimed-by-alias"],
|
|
70978
71377
|
message: rejections.map((rejection) => `${group.domain} ${describeCompetitorAliasRejection(rejection)}`).join("; ")
|
|
@@ -70981,12 +71380,12 @@ function planCompetitorPromotion(input) {
|
|
|
70981
71380
|
promote.push(group);
|
|
70982
71381
|
}
|
|
70983
71382
|
}
|
|
70984
|
-
return { promote, skipped:
|
|
71383
|
+
return { promote, skipped: skipped6 };
|
|
70985
71384
|
}
|
|
70986
71385
|
|
|
70987
71386
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
70988
71387
|
import crypto55 from "crypto";
|
|
70989
|
-
import { eq as
|
|
71388
|
+
import { eq as eq98 } from "drizzle-orm";
|
|
70990
71389
|
var DEFAULT_MAX_PROBES = 100;
|
|
70991
71390
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
70992
71391
|
function classifyProbeBucket(input) {
|
|
@@ -71072,7 +71471,7 @@ async function executeDiscovery(opts) {
|
|
|
71072
71471
|
status: DiscoverySessionStatuses.seeding,
|
|
71073
71472
|
dedupThreshold,
|
|
71074
71473
|
startedAt
|
|
71075
|
-
}).where(
|
|
71474
|
+
}).where(eq98(discoverySessions.id, opts.sessionId)).run();
|
|
71076
71475
|
const seedResult = await opts.deps.seed({
|
|
71077
71476
|
project: opts.project,
|
|
71078
71477
|
icpDescription: opts.icpDescription,
|
|
@@ -71133,7 +71532,7 @@ async function executeDiscovery(opts) {
|
|
|
71133
71532
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
71134
71533
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
71135
71534
|
warning
|
|
71136
|
-
}).where(
|
|
71535
|
+
}).where(eq98(discoverySessions.id, opts.sessionId)).run();
|
|
71137
71536
|
const probeLocation = opts.locations?.[0];
|
|
71138
71537
|
const probeResults = await mapWithConcurrency(
|
|
71139
71538
|
probedCanonicals,
|
|
@@ -71187,7 +71586,7 @@ async function executeDiscovery(opts) {
|
|
|
71187
71586
|
wastedCount: buckets["wasted-surface"],
|
|
71188
71587
|
competitorMap,
|
|
71189
71588
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
71190
|
-
}).where(
|
|
71589
|
+
}).where(eq98(discoverySessions.id, opts.sessionId)).run();
|
|
71191
71590
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
71192
71591
|
return {
|
|
71193
71592
|
buckets,
|
|
@@ -71227,7 +71626,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
71227
71626
|
status: DiscoverySessionStatuses.failed,
|
|
71228
71627
|
error,
|
|
71229
71628
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
71230
|
-
}).where(
|
|
71629
|
+
}).where(eq98(discoverySessions.id, sessionId)).run();
|
|
71231
71630
|
}
|
|
71232
71631
|
function dedupeStrings(input) {
|
|
71233
71632
|
const seen2 = /* @__PURE__ */ new Set();
|
|
@@ -71245,12 +71644,12 @@ function dedupeStrings(input) {
|
|
|
71245
71644
|
|
|
71246
71645
|
// ../api-routes/src/technical-aeo.ts
|
|
71247
71646
|
import crypto56 from "crypto";
|
|
71248
|
-
import { and as
|
|
71647
|
+
import { and as and83, asc as asc22, count as count8, desc as desc48, eq as eq100, gte as gte27, inArray as inArray44, isNotNull as isNotNull10, isNull as isNull17, lt as lt14, or as or21, sql as sql36 } from "drizzle-orm";
|
|
71249
71648
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
71250
71649
|
|
|
71251
71650
|
// ../api-routes/src/technical-aeo-issues.ts
|
|
71252
71651
|
import { createHash as createHash10 } from "crypto";
|
|
71253
|
-
import { and as
|
|
71652
|
+
import { and as and82, asc as asc21, eq as eq99, gt as gt10, sql as sql35 } from "drizzle-orm";
|
|
71254
71653
|
var ADVISORY_CODES = /* @__PURE__ */ new Set([
|
|
71255
71654
|
"entity-consistency.title.short",
|
|
71256
71655
|
"site.duplicate-title",
|
|
@@ -71395,12 +71794,12 @@ function readSiteAuditIssues(input) {
|
|
|
71395
71794
|
}
|
|
71396
71795
|
}
|
|
71397
71796
|
if (snapshot.detailsAvailable && snapshot.attemptId) {
|
|
71398
|
-
const scope =
|
|
71399
|
-
|
|
71400
|
-
|
|
71401
|
-
|
|
71797
|
+
const scope = and82(
|
|
71798
|
+
eq99(siteCrawlPages.projectId, project.id),
|
|
71799
|
+
eq99(siteCrawlPages.runId, snapshot.runId),
|
|
71800
|
+
eq99(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
71402
71801
|
);
|
|
71403
|
-
const size = db.select({ bytes:
|
|
71802
|
+
const size = db.select({ bytes: sql35`coalesce(sum(length(cast(${siteCrawlPages.auditFields} as blob)) + length(cast(${siteCrawlPages.url} as blob))), 0)` }).from(siteCrawlPages).where(scope).get()?.bytes ?? 0;
|
|
71404
71803
|
if (size + crossBytes > MAX_EVIDENCE_BYTES) {
|
|
71405
71804
|
throw validationError("Stored audit exceeds the 128 MiB issue-evidence read budget");
|
|
71406
71805
|
}
|
|
@@ -71411,7 +71810,7 @@ function readSiteAuditIssues(input) {
|
|
|
71411
71810
|
url: siteCrawlPages.url,
|
|
71412
71811
|
auditScore: siteCrawlPages.auditScore,
|
|
71413
71812
|
auditFields: siteCrawlPages.auditFields
|
|
71414
|
-
}).from(siteCrawlPages).where(
|
|
71813
|
+
}).from(siteCrawlPages).where(and82(scope, after === void 0 ? void 0 : gt10(siteCrawlPages.nodeKey, after))).orderBy(asc21(siteCrawlPages.nodeKey)).limit(200).all();
|
|
71415
71814
|
for (const page3 of pages) {
|
|
71416
71815
|
evidenceHash.update(JSON.stringify(page3));
|
|
71417
71816
|
const fields = record(page3.auditFields);
|
|
@@ -71773,13 +72172,13 @@ function parseLinkKind(value) {
|
|
|
71773
72172
|
return allowed.data;
|
|
71774
72173
|
}
|
|
71775
72174
|
function linkKindFilter(linkKind) {
|
|
71776
|
-
if (linkKind === SiteHealthLinkKinds.content) return
|
|
71777
|
-
if (linkKind === SiteHealthLinkKinds.template) return
|
|
72175
|
+
if (linkKind === SiteHealthLinkKinds.content) return eq100(siteCrawlEdges.isTemplate, false);
|
|
72176
|
+
if (linkKind === SiteHealthLinkKinds.template) return eq100(siteCrawlEdges.isTemplate, true);
|
|
71778
72177
|
return void 0;
|
|
71779
72178
|
}
|
|
71780
72179
|
var graphSourceNode = alias(siteCrawlGraphNodes, "site_crawl_graph_source_node");
|
|
71781
72180
|
var graphTargetNode = alias(siteCrawlGraphNodes, "site_crawl_graph_target_node");
|
|
71782
|
-
var PAGE_CHANGE_PREDICATE =
|
|
72181
|
+
var PAGE_CHANGE_PREDICATE = sql36`(
|
|
71783
72182
|
current.url IS NOT previous.url
|
|
71784
72183
|
OR current.final_url IS NOT previous.final_url
|
|
71785
72184
|
OR current.path IS NOT previous.path
|
|
@@ -71801,7 +72200,7 @@ var PAGE_CHANGE_PREDICATE = sql35`(
|
|
|
71801
72200
|
OR current.link_score_raw IS NOT previous.link_score_raw
|
|
71802
72201
|
OR current.link_score_normalized IS NOT previous.link_score_normalized
|
|
71803
72202
|
)`;
|
|
71804
|
-
var LINK_CHANGE_PREDICATE =
|
|
72203
|
+
var LINK_CHANGE_PREDICATE = sql36`(
|
|
71805
72204
|
current.source_node_key IS NOT previous.source_node_key
|
|
71806
72205
|
OR current.source_url IS NOT previous.source_url
|
|
71807
72206
|
OR current.target_node_key IS NOT previous.target_node_key
|
|
@@ -71878,20 +72277,20 @@ function changedFields(before, after, fields) {
|
|
|
71878
72277
|
}
|
|
71879
72278
|
async function technicalAeoRoutes(app, opts) {
|
|
71880
72279
|
const visibleCrawlFilters = (projectId) => [
|
|
71881
|
-
|
|
71882
|
-
|
|
71883
|
-
|
|
71884
|
-
|
|
72280
|
+
eq100(siteCrawlSnapshots.projectId, projectId),
|
|
72281
|
+
eq100(runs.projectId, projectId),
|
|
72282
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
72283
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
71885
72284
|
notProbeRun()
|
|
71886
72285
|
];
|
|
71887
72286
|
const availableScanDatesFor = (projectId, requestedDate) => {
|
|
71888
72287
|
if (!requestedDate) return void 0;
|
|
71889
|
-
const date =
|
|
72288
|
+
const date = sql36`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`;
|
|
71890
72289
|
const filters = visibleCrawlFilters(projectId);
|
|
71891
|
-
const allDates =
|
|
71892
|
-
const matchingDates =
|
|
71893
|
-
const readDates = (where) => app.db.select({ date }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
71894
|
-
const countDates = (where) => app.db.select({ value:
|
|
72290
|
+
const allDates = and83(...filters);
|
|
72291
|
+
const matchingDates = and83(...filters, eq100(sql36`substr(${siteCrawlSnapshots.createdAt}, 6, 5)`, requestedDate.slice(5)));
|
|
72292
|
+
const readDates = (where) => app.db.select({ date }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(where).groupBy(date).orderBy(desc48(date)).limit(10).all().map((row) => row.date);
|
|
72293
|
+
const countDates = (where) => app.db.select({ value: sql36`count(distinct ${date})` }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(where).get()?.value ?? 0;
|
|
71895
72294
|
return {
|
|
71896
72295
|
recentDates: readDates(allDates),
|
|
71897
72296
|
totalDates: countDates(allDates),
|
|
@@ -71907,34 +72306,34 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
71907
72306
|
nextDate2.setUTCDate(nextDate2.getUTCDate() + 1);
|
|
71908
72307
|
filters.push(gte27(siteCrawlSnapshots.createdAt, requestedDate), lt14(siteCrawlSnapshots.createdAt, nextDate2.toISOString()));
|
|
71909
72308
|
}
|
|
71910
|
-
const selectionFilters = scope.scoredOnly ? [...filters,
|
|
71911
|
-
const selected = app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
71912
|
-
|
|
71913
|
-
|
|
71914
|
-
|
|
71915
|
-
|
|
71916
|
-
|
|
72309
|
+
const selectionFilters = scope.scoredOnly ? [...filters, sql36`exists (select 1 from ${siteAuditSnapshots} where ${siteAuditSnapshots.projectId} = ${siteCrawlSnapshots.projectId} and ${siteAuditSnapshots.runId} = ${siteCrawlSnapshots.runId})`] : filters;
|
|
72310
|
+
const selected = app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(...selectionFilters, runId ? eq100(siteCrawlSnapshots.runId, runId) : void 0)).orderBy(
|
|
72311
|
+
desc48(sql36`substr(${siteCrawlSnapshots.createdAt}, 1, 10)`),
|
|
72312
|
+
desc48(siteCrawlSnapshots.complete),
|
|
72313
|
+
desc48(siteCrawlSnapshots.pagesFetched),
|
|
72314
|
+
desc48(siteCrawlSnapshots.createdAt),
|
|
72315
|
+
desc48(siteCrawlSnapshots.runId)
|
|
71917
72316
|
).limit(1).get();
|
|
71918
72317
|
if (!selected) return void 0;
|
|
71919
72318
|
const date = selected.snapshot.createdAt.slice(0, 10);
|
|
71920
72319
|
const nextDate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
71921
72320
|
nextDate.setUTCDate(nextDate.getUTCDate() + 1);
|
|
71922
|
-
const sameDate =
|
|
72321
|
+
const sameDate = and83(
|
|
71923
72322
|
...filters,
|
|
71924
72323
|
gte27(siteCrawlSnapshots.createdAt, date),
|
|
71925
72324
|
lt14(siteCrawlSnapshots.createdAt, nextDate.toISOString())
|
|
71926
72325
|
);
|
|
71927
|
-
const sameDateRunCount = app.db.select({ value: count8() }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
72326
|
+
const sameDateRunCount = app.db.select({ value: count8() }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
|
|
71928
72327
|
const candidates = app.db.select({
|
|
71929
72328
|
runId: siteCrawlSnapshots.runId,
|
|
71930
72329
|
createdAt: siteCrawlSnapshots.createdAt,
|
|
71931
72330
|
complete: siteCrawlSnapshots.complete,
|
|
71932
72331
|
pages: siteCrawlSnapshots.pagesFetched
|
|
71933
|
-
}).from(siteCrawlSnapshots).innerJoin(runs,
|
|
71934
|
-
|
|
71935
|
-
|
|
71936
|
-
|
|
71937
|
-
|
|
72332
|
+
}).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(sameDate).orderBy(
|
|
72333
|
+
desc48(siteCrawlSnapshots.complete),
|
|
72334
|
+
desc48(siteCrawlSnapshots.pagesFetched),
|
|
72335
|
+
desc48(siteCrawlSnapshots.createdAt),
|
|
72336
|
+
desc48(siteCrawlSnapshots.runId)
|
|
71938
72337
|
).limit(10).all();
|
|
71939
72338
|
const runSelection = {
|
|
71940
72339
|
reason: runId ? "explicit-run" : scope.scoredOnly ? "latest-scored-scan" : requestedDate ? selected.snapshot.complete ? "requested-date-complete" : "requested-date-most-pages" : selected.snapshot.complete ? "latest-date-complete" : "latest-date-most-pages",
|
|
@@ -71948,20 +72347,20 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
71948
72347
|
};
|
|
71949
72348
|
const resolveAudit = (projectId, runId, recordedBefore) => {
|
|
71950
72349
|
const filters = [
|
|
71951
|
-
|
|
71952
|
-
|
|
71953
|
-
|
|
71954
|
-
|
|
72350
|
+
eq100(siteAuditSnapshots.projectId, projectId),
|
|
72351
|
+
eq100(runs.projectId, projectId),
|
|
72352
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
72353
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
71955
72354
|
notProbeRun(),
|
|
71956
72355
|
...recordedBefore ? [lt14(siteAuditSnapshots.createdAt, recordedBefore)] : []
|
|
71957
72356
|
];
|
|
71958
|
-
const complete =
|
|
71959
|
-
const selectAudit = (pinnedRunId) => app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
71960
|
-
|
|
71961
|
-
|
|
71962
|
-
|
|
71963
|
-
|
|
71964
|
-
|
|
72357
|
+
const complete = sql36`case when ${runs.status} = ${RunStatuses.completed} then 1 else 0 end`;
|
|
72358
|
+
const selectAudit = (pinnedRunId) => app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(...filters, pinnedRunId ? eq100(siteAuditSnapshots.runId, pinnedRunId) : void 0)).orderBy(
|
|
72359
|
+
desc48(sql36`substr(${siteAuditSnapshots.createdAt}, 1, 10)`),
|
|
72360
|
+
desc48(complete),
|
|
72361
|
+
desc48(siteAuditSnapshots.pagesAudited),
|
|
72362
|
+
desc48(siteAuditSnapshots.createdAt),
|
|
72363
|
+
desc48(siteAuditSnapshots.runId)
|
|
71965
72364
|
).limit(1).get();
|
|
71966
72365
|
const auditOfCrawl = (target) => {
|
|
71967
72366
|
const selected2 = target ? selectAudit(target.snapshot.runId) : void 0;
|
|
@@ -71977,22 +72376,22 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
71977
72376
|
const date = selected.snap.createdAt.slice(0, 10);
|
|
71978
72377
|
const nextDate = /* @__PURE__ */ new Date(`${date}T00:00:00.000Z`);
|
|
71979
72378
|
nextDate.setUTCDate(nextDate.getUTCDate() + 1);
|
|
71980
|
-
const sameDate =
|
|
72379
|
+
const sameDate = and83(
|
|
71981
72380
|
...filters,
|
|
71982
72381
|
gte27(siteAuditSnapshots.createdAt, date),
|
|
71983
72382
|
lt14(siteAuditSnapshots.createdAt, nextDate.toISOString())
|
|
71984
72383
|
);
|
|
71985
|
-
const sameDateRunCount = app.db.select({ value: count8() }).from(siteAuditSnapshots).innerJoin(runs,
|
|
72384
|
+
const sameDateRunCount = app.db.select({ value: count8() }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(sameDate).get()?.value ?? 0;
|
|
71986
72385
|
const candidates = app.db.select({
|
|
71987
72386
|
runId: siteAuditSnapshots.runId,
|
|
71988
72387
|
createdAt: siteAuditSnapshots.createdAt,
|
|
71989
72388
|
complete,
|
|
71990
72389
|
pages: siteAuditSnapshots.pagesAudited
|
|
71991
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
71992
|
-
|
|
71993
|
-
|
|
71994
|
-
|
|
71995
|
-
|
|
72390
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(sameDate).orderBy(
|
|
72391
|
+
desc48(complete),
|
|
72392
|
+
desc48(siteAuditSnapshots.pagesAudited),
|
|
72393
|
+
desc48(siteAuditSnapshots.createdAt),
|
|
72394
|
+
desc48(siteAuditSnapshots.runId)
|
|
71996
72395
|
).limit(10).all().map((candidate) => ({ ...candidate, complete: Boolean(candidate.complete) }));
|
|
71997
72396
|
const runSelection = {
|
|
71998
72397
|
// A crawl exists here only when none stored a scorecard: this score is a fallback.
|
|
@@ -72007,9 +72406,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72007
72406
|
};
|
|
72008
72407
|
const detailScopeFor = (projectId, snapshot) => snapshot.detailsAvailable && snapshot.attemptId ? { projectId, runId: snapshot.runId, attemptId: snapshot.attemptId } : null;
|
|
72009
72408
|
const crawlReasonGroups = (where) => {
|
|
72010
|
-
const canonicalOther =
|
|
72409
|
+
const canonicalOther = sql36`case when ${siteCrawlPages.canonicalNodeKey} is not null
|
|
72011
72410
|
and ${siteCrawlPages.canonicalNodeKey} <> ${siteCrawlPages.nodeKey} then 1 else 0 end`;
|
|
72012
|
-
const canonicalReason =
|
|
72411
|
+
const canonicalReason = sql36`exists (select 1 from json_each(${siteCrawlPages.indexabilityReasons})
|
|
72013
72412
|
where value = ${SiteCrawlIndexabilityReasons.canonicalToOther})`;
|
|
72014
72413
|
const groups = app.db.select({
|
|
72015
72414
|
fetchState: siteCrawlPages.fetchState,
|
|
@@ -72018,7 +72417,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72018
72417
|
canonicalOther,
|
|
72019
72418
|
canonicalReason,
|
|
72020
72419
|
pages: count8(),
|
|
72021
|
-
exampleUrl:
|
|
72420
|
+
exampleUrl: sql36`min(${siteCrawlPages.url})`
|
|
72022
72421
|
}).from(siteCrawlPages).where(where).groupBy(
|
|
72023
72422
|
siteCrawlPages.fetchState,
|
|
72024
72423
|
siteCrawlPages.indexabilityState,
|
|
@@ -72054,10 +72453,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72054
72453
|
};
|
|
72055
72454
|
const inventorySummaryFor = (scope) => {
|
|
72056
72455
|
if (!scope) return null;
|
|
72057
|
-
const groups = crawlReasonGroups(
|
|
72058
|
-
|
|
72059
|
-
|
|
72060
|
-
|
|
72456
|
+
const groups = crawlReasonGroups(and83(
|
|
72457
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
72458
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
72459
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId)
|
|
72061
72460
|
));
|
|
72062
72461
|
const total = groups.reduce((sum, group) => sum + group.pages, 0);
|
|
72063
72462
|
const eligible = groups.reduce((sum, group) => sum + (group.inventoryEligible ? group.pages : 0), 0);
|
|
@@ -72077,34 +72476,34 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72077
72476
|
const NO_CRAWL_COMPLETENESS = { complete: false, termination: null };
|
|
72078
72477
|
const pageInScope = (scope, selector) => {
|
|
72079
72478
|
if (!selector.nodeKey && !selector.url) return null;
|
|
72080
|
-
return app.db.select().from(siteCrawlPages).where(
|
|
72081
|
-
|
|
72082
|
-
|
|
72083
|
-
|
|
72084
|
-
selector.nodeKey ?
|
|
72085
|
-
)).orderBy(
|
|
72086
|
-
};
|
|
72087
|
-
const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(
|
|
72088
|
-
|
|
72089
|
-
|
|
72090
|
-
|
|
72091
|
-
|
|
72092
|
-
)).orderBy(
|
|
72093
|
-
const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(
|
|
72094
|
-
|
|
72095
|
-
|
|
72096
|
-
|
|
72097
|
-
|
|
72479
|
+
return app.db.select().from(siteCrawlPages).where(and83(
|
|
72480
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
72481
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
72482
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
72483
|
+
selector.nodeKey ? eq100(siteCrawlPages.nodeKey, selector.nodeKey) : eq100(siteCrawlPages.url, selector.url)
|
|
72484
|
+
)).orderBy(asc22(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
|
|
72485
|
+
};
|
|
72486
|
+
const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(and83(
|
|
72487
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
72488
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
72489
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
72490
|
+
eq100(siteCrawlPages.depth, 0)
|
|
72491
|
+
)).orderBy(asc22(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
|
|
72492
|
+
const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(and83(
|
|
72493
|
+
eq100(runs.id, runId),
|
|
72494
|
+
eq100(runs.projectId, projectId),
|
|
72495
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
72496
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
72098
72497
|
notProbeRun()
|
|
72099
72498
|
)).limit(1).get());
|
|
72100
72499
|
const assertKnownAuditRun = (projectId, runId) => {
|
|
72101
72500
|
if (runId && !isSurfaceableAuditRun(projectId, runId)) throw notFound("Site crawl run", runId);
|
|
72102
72501
|
};
|
|
72103
|
-
const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs,
|
|
72104
|
-
|
|
72105
|
-
|
|
72106
|
-
|
|
72107
|
-
|
|
72502
|
+
const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(
|
|
72503
|
+
eq100(siteAuditSnapshots.projectId, projectId),
|
|
72504
|
+
eq100(runs.projectId, projectId),
|
|
72505
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
72506
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
72108
72507
|
notProbeRun()
|
|
72109
72508
|
)).limit(1).get());
|
|
72110
72509
|
const emptyCrawlSummary = (projectName, legacyAuditAvailable) => ({
|
|
@@ -72205,14 +72604,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72205
72604
|
};
|
|
72206
72605
|
}
|
|
72207
72606
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
72208
|
-
const conds = [
|
|
72209
|
-
if (statusFilter) conds.push(
|
|
72210
|
-
const where =
|
|
72607
|
+
const conds = [eq100(siteAuditPages.projectId, project.id), eq100(siteAuditPages.runId, latest.snap.runId)];
|
|
72608
|
+
if (statusFilter) conds.push(eq100(siteAuditPages.status, statusFilter));
|
|
72609
|
+
const where = and83(...conds);
|
|
72211
72610
|
const totalRow = app.db.select({ value: count8() }).from(siteAuditPages).where(where).get();
|
|
72212
72611
|
const total = totalRow?.value ?? 0;
|
|
72213
72612
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
72214
72613
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
72215
|
-
const orderBy = request.query.sort === "score-desc" ?
|
|
72614
|
+
const orderBy = request.query.sort === "score-desc" ? desc48(siteAuditPages.overallScore) : request.query.sort === "url" ? asc22(siteAuditPages.url) : asc22(siteAuditPages.overallScore);
|
|
72216
72615
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
72217
72616
|
const pages = rows.map((row) => ({
|
|
72218
72617
|
url: row.url,
|
|
@@ -72231,13 +72630,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72231
72630
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
72232
72631
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
72233
72632
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
72234
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
72235
|
-
|
|
72236
|
-
|
|
72237
|
-
|
|
72238
|
-
|
|
72633
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq100(siteAuditSnapshots.runId, runs.id)).where(and83(
|
|
72634
|
+
eq100(siteAuditSnapshots.projectId, project.id),
|
|
72635
|
+
eq100(runs.projectId, project.id),
|
|
72636
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
72637
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
72239
72638
|
notProbeRun()
|
|
72240
|
-
)).orderBy(
|
|
72639
|
+
)).orderBy(desc48(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
72241
72640
|
return { project: project.name, points: rows.reverse() };
|
|
72242
72641
|
});
|
|
72243
72642
|
app.get("/projects/:name/technical-aeo/crawl", async (request) => {
|
|
@@ -72337,10 +72736,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72337
72736
|
{ projectId: project.id, runId: snapshot.runId, attemptId: snapshot.attemptId },
|
|
72338
72737
|
snapshot.rootUrl
|
|
72339
72738
|
)?.nodeKey ?? null;
|
|
72340
|
-
const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(
|
|
72341
|
-
|
|
72342
|
-
|
|
72343
|
-
|
|
72739
|
+
const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(and83(
|
|
72740
|
+
eq100(siteCrawlGraphLayouts.projectId, project.id),
|
|
72741
|
+
eq100(siteCrawlGraphLayouts.runId, snapshot.runId),
|
|
72742
|
+
eq100(siteCrawlGraphLayouts.attemptId, snapshot.attemptId)
|
|
72344
72743
|
)).limit(1).get();
|
|
72345
72744
|
if (!persistedLayout) {
|
|
72346
72745
|
return {
|
|
@@ -72393,9 +72792,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72393
72792
|
const maxNodes = parseBoundedLimit(request.query.maxNodes, SITE_CRAWL_GRAPH_DEFAULT_MAX_NODES, SITE_CRAWL_GRAPH_MAX_NODES);
|
|
72394
72793
|
const maxEdges = parseBoundedLimit(request.query.maxEdges, SITE_CRAWL_GRAPH_DEFAULT_MAX_EDGES, SITE_CRAWL_GRAPH_MAX_EDGES);
|
|
72395
72794
|
const graphScope = [
|
|
72396
|
-
|
|
72397
|
-
|
|
72398
|
-
|
|
72795
|
+
eq100(siteCrawlGraphNodes.projectId, project.id),
|
|
72796
|
+
eq100(siteCrawlGraphNodes.runId, snapshot.runId),
|
|
72797
|
+
eq100(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
|
|
72399
72798
|
];
|
|
72400
72799
|
const nodeRows = app.db.select({
|
|
72401
72800
|
nodeKey: siteCrawlPages.nodeKey,
|
|
@@ -72414,18 +72813,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72414
72813
|
linkScoreNormalized: siteCrawlPages.linkScoreNormalized,
|
|
72415
72814
|
x: siteCrawlGraphNodes.x,
|
|
72416
72815
|
y: siteCrawlGraphNodes.y
|
|
72417
|
-
}).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages,
|
|
72418
|
-
|
|
72419
|
-
|
|
72420
|
-
|
|
72421
|
-
|
|
72422
|
-
)).where(
|
|
72816
|
+
}).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages, and83(
|
|
72817
|
+
eq100(siteCrawlPages.projectId, siteCrawlGraphNodes.projectId),
|
|
72818
|
+
eq100(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
|
|
72819
|
+
eq100(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
|
|
72820
|
+
eq100(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
|
|
72821
|
+
)).where(and83(...graphScope, lt14(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc22(siteCrawlGraphNodes.sampleRank)).all();
|
|
72423
72822
|
const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
|
|
72424
72823
|
...row,
|
|
72425
72824
|
healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey }),
|
|
72426
72825
|
healthReason: deriveSiteHealthReason({ ...row, indexabilityReasons, canonicalNodeKey })
|
|
72427
72826
|
}));
|
|
72428
|
-
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 :
|
|
72827
|
+
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 : eq100(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
|
|
72429
72828
|
const edges = app.db.select({
|
|
72430
72829
|
edgeKey: siteCrawlGraphEdges.edgeKey,
|
|
72431
72830
|
sourceNodeKey: siteCrawlGraphEdges.sourceNodeKey,
|
|
@@ -72433,25 +72832,25 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72433
72832
|
followable: siteCrawlGraphEdges.followable,
|
|
72434
72833
|
occurrences: siteCrawlGraphEdges.occurrences,
|
|
72435
72834
|
isTemplate: siteCrawlGraphEdges.isTemplate
|
|
72436
|
-
}).from(siteCrawlGraphEdges).innerJoin(graphSourceNode,
|
|
72437
|
-
|
|
72438
|
-
|
|
72439
|
-
|
|
72440
|
-
|
|
72835
|
+
}).from(siteCrawlGraphEdges).innerJoin(graphSourceNode, and83(
|
|
72836
|
+
eq100(graphSourceNode.projectId, siteCrawlGraphEdges.projectId),
|
|
72837
|
+
eq100(graphSourceNode.runId, siteCrawlGraphEdges.runId),
|
|
72838
|
+
eq100(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
72839
|
+
eq100(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
|
|
72441
72840
|
lt14(graphSourceNode.sampleRank, maxNodes)
|
|
72442
|
-
)).innerJoin(graphTargetNode,
|
|
72443
|
-
|
|
72444
|
-
|
|
72445
|
-
|
|
72446
|
-
|
|
72841
|
+
)).innerJoin(graphTargetNode, and83(
|
|
72842
|
+
eq100(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
|
|
72843
|
+
eq100(graphTargetNode.runId, siteCrawlGraphEdges.runId),
|
|
72844
|
+
eq100(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
72845
|
+
eq100(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
|
|
72447
72846
|
lt14(graphTargetNode.sampleRank, maxNodes)
|
|
72448
|
-
)).where(
|
|
72449
|
-
|
|
72450
|
-
|
|
72451
|
-
|
|
72847
|
+
)).where(and83(
|
|
72848
|
+
eq100(siteCrawlGraphEdges.projectId, project.id),
|
|
72849
|
+
eq100(siteCrawlGraphEdges.runId, snapshot.runId),
|
|
72850
|
+
eq100(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
|
|
72452
72851
|
lt14(siteCrawlGraphEdges.sampleRank, maxEdges),
|
|
72453
72852
|
graphLinkKindFilter
|
|
72454
|
-
)).orderBy(
|
|
72853
|
+
)).orderBy(asc22(siteCrawlGraphEdges.sampleRank)).all();
|
|
72455
72854
|
const totalNodes = persistedLayout.totalNodes;
|
|
72456
72855
|
const totalEdges = persistedLayout.totalEdges;
|
|
72457
72856
|
const totalTemplateEdges = persistedLayout.totalTemplateEdges;
|
|
@@ -72543,17 +72942,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72543
72942
|
let frontier = [focus.nodeKey];
|
|
72544
72943
|
let queryTruncated = false;
|
|
72545
72944
|
for (let distance = 0; distance < hops && frontier.length > 0; distance += 1) {
|
|
72546
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
72547
|
-
|
|
72548
|
-
|
|
72549
|
-
|
|
72550
|
-
|
|
72945
|
+
const candidates = app.db.select().from(siteCrawlEdges).where(and83(
|
|
72946
|
+
eq100(siteCrawlEdges.projectId, scope.projectId),
|
|
72947
|
+
eq100(siteCrawlEdges.runId, scope.runId),
|
|
72948
|
+
eq100(siteCrawlEdges.attemptId, scope.attemptId),
|
|
72949
|
+
eq100(siteCrawlEdges.internal, true),
|
|
72551
72950
|
isNotNull10(siteCrawlEdges.targetNodeKey),
|
|
72552
72951
|
or21(
|
|
72553
|
-
|
|
72554
|
-
|
|
72952
|
+
inArray44(siteCrawlEdges.sourceNodeKey, frontier),
|
|
72953
|
+
inArray44(siteCrawlEdges.targetNodeKey, frontier)
|
|
72555
72954
|
)
|
|
72556
|
-
)).orderBy(
|
|
72955
|
+
)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
|
|
72557
72956
|
if (candidates.length > maxEdges + maxNodes) queryTruncated = true;
|
|
72558
72957
|
const next = /* @__PURE__ */ new Set();
|
|
72559
72958
|
for (const edge of candidates.slice(0, maxEdges + maxNodes)) {
|
|
@@ -72584,22 +72983,22 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72584
72983
|
frontier = [...next].sort();
|
|
72585
72984
|
}
|
|
72586
72985
|
const nodeKeys = [...distances.keys()];
|
|
72587
|
-
const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
72588
|
-
|
|
72589
|
-
|
|
72590
|
-
|
|
72591
|
-
|
|
72986
|
+
const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and83(
|
|
72987
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
72988
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
72989
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
72990
|
+
inArray44(siteCrawlPages.nodeKey, nodeKeys)
|
|
72592
72991
|
)).all();
|
|
72593
72992
|
const persistedNodeKeys = new Set(pageRows.map((row) => row.nodeKey));
|
|
72594
72993
|
const outermostNodeKeys = pageRows.filter((row) => distances.get(row.nodeKey) === hops).map((row) => row.nodeKey);
|
|
72595
|
-
const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(
|
|
72596
|
-
|
|
72597
|
-
|
|
72598
|
-
|
|
72599
|
-
|
|
72994
|
+
const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(and83(
|
|
72995
|
+
eq100(siteCrawlEdges.projectId, scope.projectId),
|
|
72996
|
+
eq100(siteCrawlEdges.runId, scope.runId),
|
|
72997
|
+
eq100(siteCrawlEdges.attemptId, scope.attemptId),
|
|
72998
|
+
eq100(siteCrawlEdges.internal, true),
|
|
72600
72999
|
isNotNull10(siteCrawlEdges.targetNodeKey),
|
|
72601
|
-
|
|
72602
|
-
|
|
73000
|
+
inArray44(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
|
|
73001
|
+
inArray44(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
|
|
72603
73002
|
)).limit(1).get();
|
|
72604
73003
|
if (omittedOutermostEdge && !seenEdgeKeys.has(omittedOutermostEdge.edgeKey)) {
|
|
72605
73004
|
omittedEdgeKeys.add(omittedOutermostEdge.edgeKey);
|
|
@@ -72692,23 +73091,23 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72692
73091
|
let found = fromPage.nodeKey === toPage.nodeKey;
|
|
72693
73092
|
let truncated = false;
|
|
72694
73093
|
for (let depth = 0; depth < maxDepth && frontier.length > 0 && !found; depth += 1) {
|
|
72695
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
72696
|
-
|
|
72697
|
-
|
|
72698
|
-
|
|
72699
|
-
|
|
72700
|
-
|
|
72701
|
-
|
|
73094
|
+
const candidates = app.db.select().from(siteCrawlEdges).where(and83(
|
|
73095
|
+
eq100(siteCrawlEdges.projectId, scope.projectId),
|
|
73096
|
+
eq100(siteCrawlEdges.runId, scope.runId),
|
|
73097
|
+
eq100(siteCrawlEdges.attemptId, scope.attemptId),
|
|
73098
|
+
eq100(siteCrawlEdges.internal, true),
|
|
73099
|
+
eq100(siteCrawlEdges.followable, true),
|
|
73100
|
+
eq100(siteCrawlEdges.relation, "anchor"),
|
|
72702
73101
|
isNotNull10(siteCrawlEdges.targetNodeKey),
|
|
72703
|
-
|
|
72704
|
-
)).orderBy(
|
|
73102
|
+
inArray44(siteCrawlEdges.sourceNodeKey, frontier)
|
|
73103
|
+
)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
|
|
72705
73104
|
if (candidates.length > SITE_HEALTH_PATH_MAX_VISITED_NODES) truncated = true;
|
|
72706
73105
|
const candidateTargetKeys = [...new Set(candidates.map((edge) => edge.targetNodeKey).filter((nodeKey) => nodeKey != null))];
|
|
72707
|
-
const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(
|
|
72708
|
-
|
|
72709
|
-
|
|
72710
|
-
|
|
72711
|
-
|
|
73106
|
+
const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(and83(
|
|
73107
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
73108
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
73109
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
73110
|
+
inArray44(siteCrawlPages.nodeKey, candidateTargetKeys)
|
|
72712
73111
|
)).all().map((row) => row.nodeKey));
|
|
72713
73112
|
const next = /* @__PURE__ */ new Set();
|
|
72714
73113
|
for (const edge of candidates.slice(0, SITE_HEALTH_PATH_MAX_VISITED_NODES)) {
|
|
@@ -72749,11 +73148,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72749
73148
|
pathEdges.unshift(step.edge);
|
|
72750
73149
|
pathKeys.unshift(step.nodeKey);
|
|
72751
73150
|
}
|
|
72752
|
-
const pathRows = app.db.select().from(siteCrawlPages).where(
|
|
72753
|
-
|
|
72754
|
-
|
|
72755
|
-
|
|
72756
|
-
|
|
73151
|
+
const pathRows = app.db.select().from(siteCrawlPages).where(and83(
|
|
73152
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
73153
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
73154
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
73155
|
+
inArray44(siteCrawlPages.nodeKey, pathKeys)
|
|
72757
73156
|
)).all();
|
|
72758
73157
|
const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
|
|
72759
73158
|
const pathDetection = templateDetectionOf(snapshot.templateDetection);
|
|
@@ -72781,25 +73180,25 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72781
73180
|
throw validationError("change must be all, added, removed, or changed");
|
|
72782
73181
|
}
|
|
72783
73182
|
const completeSnapshotFilters = [
|
|
72784
|
-
|
|
72785
|
-
|
|
72786
|
-
|
|
72787
|
-
|
|
72788
|
-
|
|
73183
|
+
eq100(siteCrawlSnapshots.projectId, project.id),
|
|
73184
|
+
eq100(runs.projectId, project.id),
|
|
73185
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
73186
|
+
eq100(runs.status, RunStatuses.completed),
|
|
73187
|
+
eq100(siteCrawlSnapshots.complete, true),
|
|
72789
73188
|
notProbeRun()
|
|
72790
73189
|
];
|
|
72791
|
-
const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
73190
|
+
const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(...completeSnapshotFilters)).orderBy(desc48(siteCrawlSnapshots.createdAt), desc48(siteCrawlSnapshots.runId)).limit(1).get();
|
|
72792
73191
|
if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
|
|
72793
|
-
const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
73192
|
+
const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(
|
|
72794
73193
|
...completeSnapshotFilters,
|
|
72795
73194
|
or21(
|
|
72796
73195
|
lt14(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
72797
|
-
|
|
72798
|
-
|
|
73196
|
+
and83(
|
|
73197
|
+
eq100(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
72799
73198
|
lt14(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
|
|
72800
73199
|
)
|
|
72801
73200
|
)
|
|
72802
|
-
)).orderBy(
|
|
73201
|
+
)).orderBy(desc48(siteCrawlSnapshots.createdAt), desc48(siteCrawlSnapshots.runId)).limit(1).get() : void 0;
|
|
72803
73202
|
if (request.query.fromRunId && !beforeTarget) throw notFound("Site crawl run", request.query.fromRunId);
|
|
72804
73203
|
if (!afterTarget) {
|
|
72805
73204
|
const latestCrawl = resolveCrawl(project.id);
|
|
@@ -72883,9 +73282,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72883
73282
|
const summary4 = { pages: zero2(), links: zero2() };
|
|
72884
73283
|
if (scopeFilter !== "links") {
|
|
72885
73284
|
if (includeAdded || includeChanged) {
|
|
72886
|
-
const addedExpression = includeAdded ?
|
|
72887
|
-
const changedExpression = includeChanged ?
|
|
72888
|
-
const [counts2 = { added: 0, changed: 0 }] = app.db.all(
|
|
73285
|
+
const addedExpression = includeAdded ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql36`0`;
|
|
73286
|
+
const changedExpression = includeChanged ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${PAGE_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql36`0`;
|
|
73287
|
+
const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql36`
|
|
72889
73288
|
SELECT ${addedExpression} AS added, ${changedExpression} AS changed
|
|
72890
73289
|
FROM site_crawl_pages AS current
|
|
72891
73290
|
LEFT JOIN site_crawl_pages AS previous
|
|
@@ -72901,7 +73300,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72901
73300
|
summary4.pages.changed = Number(counts2.changed);
|
|
72902
73301
|
}
|
|
72903
73302
|
if (includeRemoved) {
|
|
72904
|
-
const [counts2 = { removed: 0 }] = app.db.all(
|
|
73303
|
+
const [counts2 = { removed: 0 }] = app.db.all(sql36`
|
|
72905
73304
|
SELECT COUNT(*) AS removed
|
|
72906
73305
|
FROM site_crawl_pages AS previous
|
|
72907
73306
|
LEFT JOIN site_crawl_pages AS current
|
|
@@ -72919,9 +73318,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72919
73318
|
}
|
|
72920
73319
|
if (scopeFilter !== "pages") {
|
|
72921
73320
|
if (includeAdded || includeChanged) {
|
|
72922
|
-
const addedExpression = includeAdded ?
|
|
72923
|
-
const changedExpression = includeChanged ?
|
|
72924
|
-
const [counts2 = { added: 0, changed: 0 }] = app.db.all(
|
|
73321
|
+
const addedExpression = includeAdded ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NULL THEN 1 ELSE 0 END), 0)` : sql36`0`;
|
|
73322
|
+
const changedExpression = includeChanged ? sql36`COALESCE(SUM(CASE WHEN previous.id IS NOT NULL AND ${LINK_CHANGE_PREDICATE} THEN 1 ELSE 0 END), 0)` : sql36`0`;
|
|
73323
|
+
const [counts2 = { added: 0, changed: 0 }] = app.db.all(sql36`
|
|
72925
73324
|
SELECT ${addedExpression} AS added, ${changedExpression} AS changed
|
|
72926
73325
|
FROM site_crawl_edges AS current
|
|
72927
73326
|
LEFT JOIN site_crawl_edges AS previous
|
|
@@ -72939,7 +73338,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72939
73338
|
summary4.links.changed = Number(counts2.changed);
|
|
72940
73339
|
}
|
|
72941
73340
|
if (includeRemoved) {
|
|
72942
|
-
const [counts2 = { removed: 0 }] = app.db.all(
|
|
73341
|
+
const [counts2 = { removed: 0 }] = app.db.all(sql36`
|
|
72943
73342
|
SELECT COUNT(*) AS removed
|
|
72944
73343
|
FROM site_crawl_edges AS previous
|
|
72945
73344
|
LEFT JOIN site_crawl_edges AS current
|
|
@@ -72969,48 +73368,48 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
72969
73368
|
const includeRemoved = changeFilter === "all" || changeFilter === "removed";
|
|
72970
73369
|
const table = entity === "page" ? "site_crawl_pages" : "site_crawl_edges";
|
|
72971
73370
|
const keyColumn = entity === "page" ? "node_key" : "edge_key";
|
|
72972
|
-
const internalCurrent = entity === "link" ?
|
|
72973
|
-
const internalPreviousJoin = entity === "link" ?
|
|
72974
|
-
const internalPrevious = entity === "link" ?
|
|
72975
|
-
const internalCurrentJoin = entity === "link" ?
|
|
73371
|
+
const internalCurrent = entity === "link" ? sql36`AND current.internal = 1` : sql36``;
|
|
73372
|
+
const internalPreviousJoin = entity === "link" ? sql36`AND previous.internal = 1` : sql36``;
|
|
73373
|
+
const internalPrevious = entity === "link" ? sql36`AND previous.internal = 1` : sql36``;
|
|
73374
|
+
const internalCurrentJoin = entity === "link" ? sql36`AND current.internal = 1` : sql36``;
|
|
72976
73375
|
const difference2 = entity === "page" ? PAGE_CHANGE_PREDICATE : LINK_CHANGE_PREDICATE;
|
|
72977
|
-
const afterKind = changeFilter === "added" ?
|
|
72978
|
-
const afterRows = includeAfter ? app.db.all(
|
|
73376
|
+
const afterKind = changeFilter === "added" ? sql36`previous.id IS NULL` : changeFilter === "changed" ? sql36`previous.id IS NOT NULL AND ${difference2}` : sql36`previous.id IS NULL OR (previous.id IS NOT NULL AND ${difference2})`;
|
|
73377
|
+
const afterRows = includeAfter ? app.db.all(sql36`
|
|
72979
73378
|
SELECT ${entity} AS entity,
|
|
72980
73379
|
CASE WHEN previous.id IS NULL THEN 'added' ELSE 'changed' END AS change,
|
|
72981
|
-
current.${
|
|
72982
|
-
FROM ${
|
|
72983
|
-
LEFT JOIN ${
|
|
73380
|
+
current.${sql36.raw(keyColumn)} AS key
|
|
73381
|
+
FROM ${sql36.raw(table)} AS current
|
|
73382
|
+
LEFT JOIN ${sql36.raw(table)} AS previous
|
|
72984
73383
|
ON previous.project_id = ${beforeScope.projectId}
|
|
72985
73384
|
AND previous.run_id = ${beforeScope.runId}
|
|
72986
73385
|
AND previous.attempt_id = ${beforeScope.attemptId}
|
|
72987
73386
|
${internalPreviousJoin}
|
|
72988
|
-
AND previous.${
|
|
73387
|
+
AND previous.${sql36.raw(keyColumn)} = current.${sql36.raw(keyColumn)}
|
|
72989
73388
|
WHERE current.project_id = ${afterScope.projectId}
|
|
72990
73389
|
AND current.run_id = ${afterScope.runId}
|
|
72991
73390
|
AND current.attempt_id = ${afterScope.attemptId}
|
|
72992
73391
|
${internalCurrent}
|
|
72993
|
-
AND current.${
|
|
73392
|
+
AND current.${sql36.raw(keyColumn)} > ${afterKey}
|
|
72994
73393
|
AND (${afterKind})
|
|
72995
|
-
ORDER BY current.${
|
|
73394
|
+
ORDER BY current.${sql36.raw(keyColumn)} ASC
|
|
72996
73395
|
LIMIT ${wanted2}
|
|
72997
73396
|
`) : [];
|
|
72998
|
-
const removedRows = includeRemoved ? app.db.all(
|
|
72999
|
-
SELECT ${entity} AS entity, 'removed' AS change, previous.${
|
|
73000
|
-
FROM ${
|
|
73001
|
-
LEFT JOIN ${
|
|
73397
|
+
const removedRows = includeRemoved ? app.db.all(sql36`
|
|
73398
|
+
SELECT ${entity} AS entity, 'removed' AS change, previous.${sql36.raw(keyColumn)} AS key
|
|
73399
|
+
FROM ${sql36.raw(table)} AS previous
|
|
73400
|
+
LEFT JOIN ${sql36.raw(table)} AS current
|
|
73002
73401
|
ON current.project_id = ${afterScope.projectId}
|
|
73003
73402
|
AND current.run_id = ${afterScope.runId}
|
|
73004
73403
|
AND current.attempt_id = ${afterScope.attemptId}
|
|
73005
73404
|
${internalCurrentJoin}
|
|
73006
|
-
AND current.${
|
|
73405
|
+
AND current.${sql36.raw(keyColumn)} = previous.${sql36.raw(keyColumn)}
|
|
73007
73406
|
WHERE previous.project_id = ${beforeScope.projectId}
|
|
73008
73407
|
AND previous.run_id = ${beforeScope.runId}
|
|
73009
73408
|
AND previous.attempt_id = ${beforeScope.attemptId}
|
|
73010
73409
|
${internalPrevious}
|
|
73011
|
-
AND previous.${
|
|
73410
|
+
AND previous.${sql36.raw(keyColumn)} > ${afterKey}
|
|
73012
73411
|
AND current.id IS NULL
|
|
73013
|
-
ORDER BY previous.${
|
|
73412
|
+
ORDER BY previous.${sql36.raw(keyColumn)} ASC
|
|
73014
73413
|
LIMIT ${wanted2}
|
|
73015
73414
|
`) : [];
|
|
73016
73415
|
const merged = [];
|
|
@@ -73040,18 +73439,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73040
73439
|
const visibleKeys = keyRows.slice(0, limit);
|
|
73041
73440
|
const pageKeys = visibleKeys.filter((row) => row.entity === "page").map((row) => row.key);
|
|
73042
73441
|
const linkKeys = visibleKeys.filter((row) => row.entity === "link").map((row) => row.key);
|
|
73043
|
-
const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
73044
|
-
|
|
73045
|
-
|
|
73046
|
-
|
|
73047
|
-
|
|
73442
|
+
const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and83(
|
|
73443
|
+
eq100(siteCrawlPages.projectId, scope.projectId),
|
|
73444
|
+
eq100(siteCrawlPages.runId, scope.runId),
|
|
73445
|
+
eq100(siteCrawlPages.attemptId, scope.attemptId),
|
|
73446
|
+
inArray44(siteCrawlPages.nodeKey, pageKeys)
|
|
73048
73447
|
)).all();
|
|
73049
|
-
const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(
|
|
73050
|
-
|
|
73051
|
-
|
|
73052
|
-
|
|
73053
|
-
|
|
73054
|
-
|
|
73448
|
+
const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(and83(
|
|
73449
|
+
eq100(siteCrawlEdges.projectId, scope.projectId),
|
|
73450
|
+
eq100(siteCrawlEdges.runId, scope.runId),
|
|
73451
|
+
eq100(siteCrawlEdges.attemptId, scope.attemptId),
|
|
73452
|
+
eq100(siteCrawlEdges.internal, true),
|
|
73453
|
+
inArray44(siteCrawlEdges.edgeKey, linkKeys)
|
|
73055
73454
|
)).all();
|
|
73056
73455
|
const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
73057
73456
|
const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
@@ -73224,16 +73623,16 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73224
73623
|
}
|
|
73225
73624
|
const inventorySummary = inventorySummaryFor(detailScopeFor(project.id, snapshot));
|
|
73226
73625
|
const filters = [
|
|
73227
|
-
|
|
73228
|
-
|
|
73229
|
-
|
|
73626
|
+
eq100(siteCrawlPages.projectId, project.id),
|
|
73627
|
+
eq100(siteCrawlPages.runId, snapshot.runId),
|
|
73628
|
+
eq100(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
73230
73629
|
];
|
|
73231
|
-
if (request.query.nodeKey) filters.push(
|
|
73630
|
+
if (request.query.nodeKey) filters.push(eq100(siteCrawlPages.nodeKey, request.query.nodeKey));
|
|
73232
73631
|
const inventoryEligible = parseBoolean2(request.query.inventoryEligible);
|
|
73233
|
-
if (inventoryEligible != null) filters.push(
|
|
73234
|
-
if (request.query.fetchState) filters.push(
|
|
73235
|
-
if (request.query.indexabilityState) filters.push(
|
|
73236
|
-
if (request.query.auditState) filters.push(
|
|
73632
|
+
if (inventoryEligible != null) filters.push(eq100(siteCrawlPages.inventoryEligible, inventoryEligible));
|
|
73633
|
+
if (request.query.fetchState) filters.push(eq100(siteCrawlPages.fetchState, request.query.fetchState));
|
|
73634
|
+
if (request.query.indexabilityState) filters.push(eq100(siteCrawlPages.indexabilityState, request.query.indexabilityState));
|
|
73635
|
+
if (request.query.auditState) filters.push(eq100(siteCrawlPages.auditState, request.query.auditState));
|
|
73237
73636
|
const healthState = parseSiteHealthState(request.query.healthState);
|
|
73238
73637
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
73239
73638
|
const sort = ["score-desc", "score-asc", "path"].includes(request.query.sort ?? "") ? request.query.sort : "url";
|
|
@@ -73251,17 +73650,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73251
73650
|
sort
|
|
73252
73651
|
})).digest("base64url");
|
|
73253
73652
|
const offset = decodeCrawlPageCursor(request.query.cursor, selectionKey);
|
|
73254
|
-
const orderBy = sort === "score-desc" ? [
|
|
73653
|
+
const orderBy = sort === "score-desc" ? [desc48(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)] : sort === "score-asc" ? [asc22(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)] : sort === "path" ? [asc22(siteCrawlPages.path), asc22(siteCrawlPages.nodeKey)] : [asc22(siteCrawlPages.url), asc22(siteCrawlPages.nodeKey)];
|
|
73255
73654
|
let healthStateFilter = null;
|
|
73256
73655
|
if (healthState) {
|
|
73257
|
-
const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(
|
|
73258
|
-
|
|
73259
|
-
|
|
73260
|
-
|
|
73656
|
+
const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(and83(
|
|
73657
|
+
eq100(siteCrawlPages.projectId, project.id),
|
|
73658
|
+
eq100(siteCrawlPages.runId, snapshot.runId),
|
|
73659
|
+
eq100(siteCrawlPages.attemptId, snapshot.attemptId),
|
|
73261
73660
|
isNull17(siteCrawlPages.healthState)
|
|
73262
73661
|
)).limit(1).get();
|
|
73263
73662
|
healthStateFilter = legacyRow ? "unavailable-legacy-scan" : "applied";
|
|
73264
|
-
if (healthStateFilter === "applied") filters.push(
|
|
73663
|
+
if (healthStateFilter === "applied") filters.push(eq100(siteCrawlPages.healthState, healthState));
|
|
73265
73664
|
}
|
|
73266
73665
|
if (healthStateFilter === "unavailable-legacy-scan") {
|
|
73267
73666
|
return {
|
|
@@ -73278,7 +73677,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73278
73677
|
pages: []
|
|
73279
73678
|
};
|
|
73280
73679
|
}
|
|
73281
|
-
const where =
|
|
73680
|
+
const where = and83(...filters);
|
|
73282
73681
|
const total = app.db.select({ value: count8() }).from(siteCrawlPages).where(where).get()?.value ?? 0;
|
|
73283
73682
|
const rows = app.db.select().from(siteCrawlPages).where(where).orderBy(...orderBy).limit(limit).offset(offset).all();
|
|
73284
73683
|
const nextOffset = offset + rows.length;
|
|
@@ -73313,10 +73712,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73313
73712
|
url: siteCrawlPages.url,
|
|
73314
73713
|
inventoryEligible: siteCrawlPages.inventoryEligible,
|
|
73315
73714
|
fetchState: siteCrawlPages.fetchState
|
|
73316
|
-
}).from(siteCrawlPages).where(
|
|
73317
|
-
|
|
73318
|
-
|
|
73319
|
-
|
|
73715
|
+
}).from(siteCrawlPages).where(and83(
|
|
73716
|
+
eq100(siteCrawlPages.projectId, project.id),
|
|
73717
|
+
eq100(siteCrawlPages.runId, snapshot.runId),
|
|
73718
|
+
eq100(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
73320
73719
|
)).limit(MAX_STRUCTURE_SOURCE_ROWS + 1).all();
|
|
73321
73720
|
if (sourceRows.length > MAX_STRUCTURE_SOURCE_ROWS) {
|
|
73322
73721
|
throw validationError(`Persisted crawl exceeds the ${MAX_STRUCTURE_SOURCE_ROWS}-page structure limit`);
|
|
@@ -73398,21 +73797,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73398
73797
|
};
|
|
73399
73798
|
}
|
|
73400
73799
|
const filters = [
|
|
73401
|
-
|
|
73402
|
-
|
|
73403
|
-
|
|
73404
|
-
|
|
73800
|
+
eq100(siteCrawlEdges.projectId, project.id),
|
|
73801
|
+
eq100(siteCrawlEdges.runId, snapshot.runId),
|
|
73802
|
+
eq100(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
73803
|
+
eq100(siteCrawlEdges.internal, true),
|
|
73405
73804
|
linkKindFilter(linkKind)
|
|
73406
73805
|
];
|
|
73407
|
-
if (request.query.sourceUrl) filters.push(
|
|
73408
|
-
if (request.query.targetUrl) filters.push(
|
|
73806
|
+
if (request.query.sourceUrl) filters.push(eq100(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
|
|
73807
|
+
if (request.query.targetUrl) filters.push(eq100(siteCrawlEdges.targetUrl, request.query.targetUrl));
|
|
73409
73808
|
const followable = parseBoolean2(request.query.followable);
|
|
73410
|
-
if (followable != null) filters.push(
|
|
73411
|
-
const where =
|
|
73809
|
+
if (followable != null) filters.push(eq100(siteCrawlEdges.followable, followable));
|
|
73810
|
+
const where = and83(...filters);
|
|
73412
73811
|
const total = app.db.select({ value: count8() }).from(siteCrawlEdges).where(where).get()?.value ?? 0;
|
|
73413
73812
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
73414
73813
|
const offset = decodeCursor(request.query.cursor);
|
|
73415
|
-
const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(
|
|
73814
|
+
const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
|
|
73416
73815
|
const nextOffset = offset + rows.length;
|
|
73417
73816
|
return {
|
|
73418
73817
|
project: project.name,
|
|
@@ -73469,17 +73868,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73469
73868
|
};
|
|
73470
73869
|
}
|
|
73471
73870
|
const scope = [
|
|
73472
|
-
|
|
73473
|
-
|
|
73474
|
-
|
|
73475
|
-
|
|
73871
|
+
eq100(siteCrawlEdges.projectId, project.id),
|
|
73872
|
+
eq100(siteCrawlEdges.runId, snapshot.runId),
|
|
73873
|
+
eq100(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
73874
|
+
eq100(siteCrawlEdges.internal, true),
|
|
73476
73875
|
linkKindFilter(linkKind)
|
|
73477
73876
|
];
|
|
73478
|
-
const inboundMatch = request.query.nodeKey && request.query.url ? or21(
|
|
73479
|
-
const outboundMatch = request.query.nodeKey && request.query.url ? or21(
|
|
73877
|
+
const inboundMatch = request.query.nodeKey && request.query.url ? or21(eq100(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq100(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq100(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq100(siteCrawlEdges.targetUrl, request.query.url);
|
|
73878
|
+
const outboundMatch = request.query.nodeKey && request.query.url ? or21(eq100(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq100(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq100(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq100(siteCrawlEdges.sourceUrl, request.query.url);
|
|
73480
73879
|
const limit = parseBoundedLimit(request.query.limit, 50, 100);
|
|
73481
|
-
const inboundRows = app.db.select().from(siteCrawlEdges).where(
|
|
73482
|
-
const outboundRows = app.db.select().from(siteCrawlEdges).where(
|
|
73880
|
+
const inboundRows = app.db.select().from(siteCrawlEdges).where(and83(...scope, inboundMatch)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
73881
|
+
const outboundRows = app.db.select().from(siteCrawlEdges).where(and83(...scope, outboundMatch)).orderBy(asc22(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
73483
73882
|
return {
|
|
73484
73883
|
project: project.name,
|
|
73485
73884
|
hasCrawlData: true,
|
|
@@ -73513,11 +73912,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73513
73912
|
if (!snapshot.attemptId) {
|
|
73514
73913
|
return { project: project.name, runId: snapshot.runId, state: "partial", checkDeadLinks: true, checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound, unverified: snapshot.deadLinksUnverified, total: 0, nextCursor: null, deadLinks: [], runSelection: target.runSelection };
|
|
73515
73914
|
}
|
|
73516
|
-
const where =
|
|
73517
|
-
|
|
73518
|
-
|
|
73519
|
-
|
|
73520
|
-
|
|
73915
|
+
const where = and83(
|
|
73916
|
+
eq100(siteCrawlFindings.projectId, project.id),
|
|
73917
|
+
eq100(siteCrawlFindings.runId, snapshot.runId),
|
|
73918
|
+
eq100(siteCrawlFindings.attemptId, snapshot.attemptId),
|
|
73919
|
+
eq100(siteCrawlFindings.findingType, "dead-link"),
|
|
73521
73920
|
// Belt and braces on the client-facing surface: a row without a status
|
|
73522
73921
|
// code is not evidence of a broken link, and this response is where such
|
|
73523
73922
|
// a row would be read as one. Migration 140 removed every stored row that
|
|
@@ -73527,12 +73926,12 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73527
73926
|
// a fabricated broken link shown to a client is not worth the saved
|
|
73528
73927
|
// predicate. Applied to `total` as well as the rows, or the count would
|
|
73529
73928
|
// promise a page the list cannot fill.
|
|
73530
|
-
isNotNull10(
|
|
73929
|
+
isNotNull10(sql36`json_extract(${siteCrawlFindings.evidence}, '$.statusCode')`)
|
|
73531
73930
|
);
|
|
73532
73931
|
const total = app.db.select({ value: count8() }).from(siteCrawlFindings).where(where).get()?.value ?? 0;
|
|
73533
73932
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
73534
73933
|
const offset = decodeCursor(request.query.cursor);
|
|
73535
|
-
const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(
|
|
73934
|
+
const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(asc22(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
|
|
73536
73935
|
const nextOffset = offset + rows.length;
|
|
73537
73936
|
const deadLinks = rows.map((row) => ({
|
|
73538
73937
|
findingKey: row.findingKey,
|
|
@@ -73567,19 +73966,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73567
73966
|
createdAt: runs.createdAt,
|
|
73568
73967
|
startedAt: runs.startedAt,
|
|
73569
73968
|
finishedAt: runs.finishedAt
|
|
73570
|
-
}).from(runs).where(
|
|
73571
|
-
|
|
73572
|
-
|
|
73969
|
+
}).from(runs).where(and83(
|
|
73970
|
+
eq100(runs.projectId, project.id),
|
|
73971
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
73573
73972
|
notProbeRun()
|
|
73574
|
-
)).orderBy(
|
|
73973
|
+
)).orderBy(desc48(runs.createdAt), desc48(runs.id)).limit(limit).all();
|
|
73575
73974
|
const runIds = rows.map((row) => row.runId);
|
|
73576
|
-
const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
73577
|
-
|
|
73578
|
-
|
|
73579
|
-
|
|
73580
|
-
|
|
73975
|
+
const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs, eq100(siteCrawlSnapshots.runId, runs.id)).where(and83(
|
|
73976
|
+
eq100(siteCrawlSnapshots.projectId, project.id),
|
|
73977
|
+
eq100(runs.projectId, project.id),
|
|
73978
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
73979
|
+
inArray44(runs.status, SURFACEABLE_STATUSES),
|
|
73581
73980
|
notProbeRun(),
|
|
73582
|
-
|
|
73981
|
+
inArray44(siteCrawlSnapshots.runId, runIds)
|
|
73583
73982
|
)).all().map((row) => row.runId));
|
|
73584
73983
|
return {
|
|
73585
73984
|
project: project.name,
|
|
@@ -73593,21 +73992,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73593
73992
|
});
|
|
73594
73993
|
app.get("/projects/:name/technical-aeo/runs/:runId/progress", async (request) => {
|
|
73595
73994
|
const project = resolveProject(app.db, request.params.name);
|
|
73596
|
-
const run = app.db.select().from(runs).where(
|
|
73597
|
-
|
|
73598
|
-
|
|
73599
|
-
|
|
73995
|
+
const run = app.db.select().from(runs).where(and83(
|
|
73996
|
+
eq100(runs.id, request.params.runId),
|
|
73997
|
+
eq100(runs.projectId, project.id),
|
|
73998
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
73600
73999
|
notProbeRun()
|
|
73601
74000
|
)).get();
|
|
73602
74001
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
73603
|
-
const attempt = app.db.select().from(siteCrawlAttempts).where(
|
|
73604
|
-
|
|
73605
|
-
|
|
73606
|
-
)).orderBy(
|
|
73607
|
-
const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(
|
|
73608
|
-
|
|
73609
|
-
|
|
73610
|
-
|
|
74002
|
+
const attempt = app.db.select().from(siteCrawlAttempts).where(and83(
|
|
74003
|
+
eq100(siteCrawlAttempts.projectId, project.id),
|
|
74004
|
+
eq100(siteCrawlAttempts.runId, run.id)
|
|
74005
|
+
)).orderBy(desc48(siteCrawlAttempts.attemptNumber), desc48(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
74006
|
+
const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(and83(
|
|
74007
|
+
eq100(siteCrawlGraphLayouts.projectId, project.id),
|
|
74008
|
+
eq100(siteCrawlGraphLayouts.runId, run.id),
|
|
74009
|
+
eq100(siteCrawlGraphLayouts.attemptId, attempt.id)
|
|
73611
74010
|
)).get() : void 0;
|
|
73612
74011
|
const layoutState = persistedLayout?.state === "ready" || persistedLayout?.state === "unavailable" ? persistedLayout.state : run.status === RunStatuses.completed || run.status === RunStatuses.partial ? "unavailable" : "pending";
|
|
73613
74012
|
return {
|
|
@@ -73638,19 +74037,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73638
74037
|
};
|
|
73639
74038
|
});
|
|
73640
74039
|
app.get("/projects/:name/technical-aeo/runs/:runId/page-health-preview", async (request) => app.db.transaction((tx) => {
|
|
73641
|
-
const project = tx.select().from(projects).where(
|
|
74040
|
+
const project = tx.select().from(projects).where(eq100(projects.name, request.params.name)).get();
|
|
73642
74041
|
if (!project) throw notFound("Project", request.params.name);
|
|
73643
|
-
const run = tx.select().from(runs).where(
|
|
73644
|
-
|
|
73645
|
-
|
|
73646
|
-
|
|
74042
|
+
const run = tx.select().from(runs).where(and83(
|
|
74043
|
+
eq100(runs.id, request.params.runId),
|
|
74044
|
+
eq100(runs.projectId, project.id),
|
|
74045
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
73647
74046
|
notProbeRun()
|
|
73648
74047
|
)).get();
|
|
73649
74048
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
73650
|
-
const attempt = tx.select().from(siteCrawlAttempts).where(
|
|
73651
|
-
|
|
73652
|
-
|
|
73653
|
-
)).orderBy(
|
|
74049
|
+
const attempt = tx.select().from(siteCrawlAttempts).where(and83(
|
|
74050
|
+
eq100(siteCrawlAttempts.projectId, project.id),
|
|
74051
|
+
eq100(siteCrawlAttempts.runId, run.id)
|
|
74052
|
+
)).orderBy(desc48(siteCrawlAttempts.attemptNumber), desc48(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
73654
74053
|
const state = run.status === RunStatuses.queued ? "waiting" : run.status === RunStatuses.running ? "collecting" : "terminal";
|
|
73655
74054
|
const base = {
|
|
73656
74055
|
project: project.name,
|
|
@@ -73663,11 +74062,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73663
74062
|
if (!attempt) {
|
|
73664
74063
|
return { ...base, pagesAudited: 0, examples: [] };
|
|
73665
74064
|
}
|
|
73666
|
-
const auditedWhere =
|
|
73667
|
-
|
|
73668
|
-
|
|
73669
|
-
|
|
73670
|
-
|
|
74065
|
+
const auditedWhere = and83(
|
|
74066
|
+
eq100(siteCrawlPages.projectId, project.id),
|
|
74067
|
+
eq100(siteCrawlPages.runId, run.id),
|
|
74068
|
+
eq100(siteCrawlPages.attemptId, attempt.id),
|
|
74069
|
+
eq100(siteCrawlPages.auditState, "success")
|
|
73671
74070
|
);
|
|
73672
74071
|
const pagesAudited = tx.select({ value: count8() }).from(siteCrawlPages).where(auditedWhere).get()?.value ?? 0;
|
|
73673
74072
|
if (state !== "collecting") {
|
|
@@ -73678,11 +74077,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73678
74077
|
url: siteCrawlPages.url,
|
|
73679
74078
|
auditScore: siteCrawlPages.auditScore,
|
|
73680
74079
|
auditFields: siteCrawlPages.auditFields
|
|
73681
|
-
}).from(siteCrawlPages).where(
|
|
74080
|
+
}).from(siteCrawlPages).where(and83(
|
|
73682
74081
|
auditedWhere,
|
|
73683
74082
|
isNotNull10(siteCrawlPages.auditScore),
|
|
73684
74083
|
lt14(siteCrawlPages.auditScore, 70)
|
|
73685
|
-
)).orderBy(
|
|
74084
|
+
)).orderBy(asc22(siteCrawlPages.auditScore), asc22(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
|
|
73686
74085
|
const examples = candidates.flatMap((candidate) => {
|
|
73687
74086
|
if (candidate.auditScore == null) return [];
|
|
73688
74087
|
const checksNeedingAttention = countLivePageHealthChecks(candidate.auditFields);
|
|
@@ -73714,13 +74113,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73714
74113
|
status: runs.status,
|
|
73715
74114
|
identityKey: siteCrawlRunRequests.identityKey,
|
|
73716
74115
|
effectiveOptions: siteCrawlRunRequests.effectiveOptions
|
|
73717
|
-
}).from(runs).leftJoin(siteCrawlRunRequests,
|
|
73718
|
-
|
|
73719
|
-
|
|
73720
|
-
)).where(
|
|
73721
|
-
|
|
73722
|
-
|
|
73723
|
-
|
|
74116
|
+
}).from(runs).leftJoin(siteCrawlRunRequests, and83(
|
|
74117
|
+
eq100(siteCrawlRunRequests.projectId, runs.projectId),
|
|
74118
|
+
eq100(siteCrawlRunRequests.runId, runs.id)
|
|
74119
|
+
)).where(and83(
|
|
74120
|
+
eq100(runs.projectId, project.id),
|
|
74121
|
+
eq100(runs.kind, RunKinds["site-audit"]),
|
|
74122
|
+
inArray44(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
73724
74123
|
)).get();
|
|
73725
74124
|
if (existing) {
|
|
73726
74125
|
if (existing.identityKey === identityKey) {
|
|
@@ -73776,7 +74175,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
73776
74175
|
// ../api-routes/src/research.ts
|
|
73777
74176
|
import crypto57 from "crypto";
|
|
73778
74177
|
import { z as z7 } from "zod";
|
|
73779
|
-
import { and as
|
|
74178
|
+
import { and as and84, count as count9, desc as desc49, eq as eq101, gte as gte28, lt as lt15, or as or22, sql as sql37 } from "drizzle-orm";
|
|
73780
74179
|
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
73781
74180
|
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
73782
74181
|
async function researchRoutes(app, opts) {
|
|
@@ -73795,7 +74194,7 @@ async function researchRoutes(app, opts) {
|
|
|
73795
74194
|
const input = parsed.data;
|
|
73796
74195
|
if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
73797
74196
|
const requestHash2 = directResearchRequestHash(input);
|
|
73798
|
-
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(
|
|
74197
|
+
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and84(eq101(researchRuns.projectId, project.id), eq101(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
|
|
73799
74198
|
if (existingReceipt) {
|
|
73800
74199
|
assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
|
|
73801
74200
|
const result2 = getDetail(app, project.id, existingReceipt.id);
|
|
@@ -73830,7 +74229,7 @@ async function researchRoutes(app, opts) {
|
|
|
73830
74229
|
const initiatedBy = researchPrincipal(request);
|
|
73831
74230
|
const decision = app.db.transaction((tx) => {
|
|
73832
74231
|
if (input.idempotencyKey) {
|
|
73833
|
-
const existing = tx.select().from(researchRuns).where(
|
|
74232
|
+
const existing = tx.select().from(researchRuns).where(and84(eq101(researchRuns.projectId, project.id), eq101(researchRuns.idempotencyKey, input.idempotencyKey))).get();
|
|
73834
74233
|
if (existing) {
|
|
73835
74234
|
assertSameDirectResearchRequest(existing, input, requestHash2);
|
|
73836
74235
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
@@ -73838,11 +74237,11 @@ async function researchRoutes(app, opts) {
|
|
|
73838
74237
|
}
|
|
73839
74238
|
if (initiatedBy?.limited) {
|
|
73840
74239
|
const { start, end, date } = utcDayBounds(now);
|
|
73841
|
-
const used = tx.select({ value: count9() }).from(researchRuns).where(
|
|
73842
|
-
|
|
74240
|
+
const used = tx.select({ value: count9() }).from(researchRuns).where(and84(
|
|
74241
|
+
eq101(researchRuns.projectId, project.id),
|
|
73843
74242
|
gte28(researchRuns.createdAt, start),
|
|
73844
74243
|
lt15(researchRuns.createdAt, end),
|
|
73845
|
-
|
|
74244
|
+
sql37`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
73846
74245
|
)).get()?.value ?? 0;
|
|
73847
74246
|
if (used >= viewerDailyRunLimit) {
|
|
73848
74247
|
throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
@@ -73870,15 +74269,15 @@ async function researchRoutes(app, opts) {
|
|
|
73870
74269
|
if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
73871
74270
|
const requestHash2 = researchBatchRequestHash(input);
|
|
73872
74271
|
const receiptKeys = input.runs.map((_run, index) => batchChildReceiptKey(input.idempotencyKey, index));
|
|
73873
|
-
const firstReceipt = app.db.select().from(researchRuns).where(
|
|
73874
|
-
|
|
73875
|
-
|
|
74272
|
+
const firstReceipt = app.db.select().from(researchRuns).where(and84(
|
|
74273
|
+
eq101(researchRuns.projectId, project.id),
|
|
74274
|
+
eq101(researchRuns.idempotencyKey, receiptKeys[0])
|
|
73876
74275
|
)).get();
|
|
73877
74276
|
if (firstReceipt) {
|
|
73878
74277
|
if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
73879
|
-
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(
|
|
73880
|
-
|
|
73881
|
-
|
|
74278
|
+
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and84(
|
|
74279
|
+
eq101(researchRuns.projectId, project.id),
|
|
74280
|
+
eq101(researchRuns.idempotencyKey, key)
|
|
73882
74281
|
)).get());
|
|
73883
74282
|
if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
73884
74283
|
const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
|
|
@@ -73894,11 +74293,11 @@ async function researchRoutes(app, opts) {
|
|
|
73894
74293
|
const decision = app.db.transaction((tx) => {
|
|
73895
74294
|
if (initiatedBy?.limited) {
|
|
73896
74295
|
const { start, end, date } = utcDayBounds(now);
|
|
73897
|
-
const used = tx.select({ value: count9() }).from(researchRuns).where(
|
|
73898
|
-
|
|
74296
|
+
const used = tx.select({ value: count9() }).from(researchRuns).where(and84(
|
|
74297
|
+
eq101(researchRuns.projectId, project.id),
|
|
73899
74298
|
gte28(researchRuns.createdAt, start),
|
|
73900
74299
|
lt15(researchRuns.createdAt, end),
|
|
73901
|
-
|
|
74300
|
+
sql37`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
73902
74301
|
)).get()?.value ?? 0;
|
|
73903
74302
|
if (used + prepared.length > viewerDailyRunLimit) throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
73904
74303
|
}
|
|
@@ -73949,10 +74348,10 @@ async function researchRoutes(app, opts) {
|
|
|
73949
74348
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
73950
74349
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
73951
74350
|
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
73952
|
-
const rows = app.db.select().from(researchRuns).where(
|
|
73953
|
-
|
|
73954
|
-
cursor ? or22(lt15(researchRuns.createdAt, cursor.createdAt),
|
|
73955
|
-
)).orderBy(
|
|
74351
|
+
const rows = app.db.select().from(researchRuns).where(and84(
|
|
74352
|
+
eq101(researchRuns.projectId, project.id),
|
|
74353
|
+
cursor ? or22(lt15(researchRuns.createdAt, cursor.createdAt), and84(eq101(researchRuns.createdAt, cursor.createdAt), lt15(researchRuns.id, cursor.id))) : void 0
|
|
74354
|
+
)).orderBy(desc49(researchRuns.createdAt), desc49(researchRuns.id)).limit(limit + 1).all();
|
|
73956
74355
|
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
73957
74356
|
const last = runs2.at(-1);
|
|
73958
74357
|
const nextCursor = rows.length > limit && last ? Buffer.from(JSON.stringify({ projectId: project.id, createdAt: last.createdAt, id: last.id })).toString("base64url") : null;
|
|
@@ -74101,9 +74500,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
|
|
|
74101
74500
|
return stored;
|
|
74102
74501
|
}
|
|
74103
74502
|
function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
74104
|
-
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(
|
|
74105
|
-
|
|
74106
|
-
|
|
74503
|
+
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and84(
|
|
74504
|
+
eq101(measurementQueryTemplates.projectId, projectId),
|
|
74505
|
+
eq101(measurementQueryTemplates.id, selection.templateId)
|
|
74107
74506
|
)).get();
|
|
74108
74507
|
if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
|
|
74109
74508
|
const templateVersion = savedTemplate.updatedAt;
|
|
@@ -74112,7 +74511,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
|
74112
74511
|
}
|
|
74113
74512
|
const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
|
|
74114
74513
|
if (bindingLocation) {
|
|
74115
|
-
const project = app.db.select({ locations: projects.locations }).from(projects).where(
|
|
74514
|
+
const project = app.db.select({ locations: projects.locations }).from(projects).where(eq101(projects.id, projectId)).get();
|
|
74116
74515
|
if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
|
|
74117
74516
|
}
|
|
74118
74517
|
const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
|
|
@@ -74125,9 +74524,9 @@ function classifyResearchQueries(project, plan, queries2) {
|
|
|
74125
74524
|
return queries2.map((query) => classifier?.classify(query) ?? null);
|
|
74126
74525
|
}
|
|
74127
74526
|
function getDetail(app, projectId, id) {
|
|
74128
|
-
const row = app.db.select().from(researchRuns).where(
|
|
74527
|
+
const row = app.db.select().from(researchRuns).where(and84(eq101(researchRuns.id, id), eq101(researchRuns.projectId, projectId))).get();
|
|
74129
74528
|
if (!row) throw notFound("Research run", id);
|
|
74130
|
-
const queries2 = app.db.select().from(researchRunQueries).where(
|
|
74529
|
+
const queries2 = app.db.select().from(researchRunQueries).where(eq101(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
|
|
74131
74530
|
return { ...serializeRun(row), queries: queries2 };
|
|
74132
74531
|
}
|
|
74133
74532
|
function serializeRun(row) {
|
|
@@ -74182,7 +74581,7 @@ function utcDayBounds(now) {
|
|
|
74182
74581
|
|
|
74183
74582
|
// ../api-routes/src/simple-measurement-definitions.ts
|
|
74184
74583
|
import crypto58 from "crypto";
|
|
74185
|
-
import { and as
|
|
74584
|
+
import { and as and85, eq as eq102 } from "drizzle-orm";
|
|
74186
74585
|
function captureSimpleMeasurementDefinition(db, input) {
|
|
74187
74586
|
return db.transaction((tx) => {
|
|
74188
74587
|
const run = tx.select({
|
|
@@ -74190,7 +74589,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
74190
74589
|
trigger: runs.trigger,
|
|
74191
74590
|
status: runs.status,
|
|
74192
74591
|
measurementPlanVersionId: runs.measurementPlanVersionId
|
|
74193
|
-
}).from(runs).where(
|
|
74592
|
+
}).from(runs).where(and85(eq102(runs.projectId, input.projectId), eq102(runs.id, input.runId))).get();
|
|
74194
74593
|
if (!run) throw notFound("Run", input.runId);
|
|
74195
74594
|
if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
|
|
74196
74595
|
return null;
|
|
@@ -74211,7 +74610,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
74211
74610
|
throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
|
|
74212
74611
|
}
|
|
74213
74612
|
}
|
|
74214
|
-
const existing = tx.select().from(simpleMeasurementDefinitions).where(
|
|
74613
|
+
const existing = tx.select().from(simpleMeasurementDefinitions).where(and85(eq102(simpleMeasurementDefinitions.projectId, input.projectId), eq102(simpleMeasurementDefinitions.runId, input.runId))).get();
|
|
74215
74614
|
if (existing) {
|
|
74216
74615
|
const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
|
|
74217
74616
|
const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition2.identity;
|
|
@@ -74237,15 +74636,15 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
74237
74636
|
if (run.status !== RunStatuses.running) {
|
|
74238
74637
|
throw validationError("Only a running run can capture a new measurement definition.");
|
|
74239
74638
|
}
|
|
74240
|
-
if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(
|
|
74639
|
+
if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq102(querySnapshots.runId, input.runId)).limit(1).get()) {
|
|
74241
74640
|
throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
|
|
74242
74641
|
}
|
|
74243
|
-
const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(
|
|
74642
|
+
const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq102(queries.projectId, input.projectId)).all().map((query) => query.id));
|
|
74244
74643
|
if (definition2.queries.some((query) => !projectQueryIds.has(query.queryId))) {
|
|
74245
74644
|
throw validationError("Every captured query must belong to the run project.");
|
|
74246
74645
|
}
|
|
74247
74646
|
if (definition2.competitors !== void 0) {
|
|
74248
|
-
const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(
|
|
74647
|
+
const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq102(competitors.projectId, input.projectId)).orderBy(competitors.domain).all().map((competitor) => competitor.domain));
|
|
74249
74648
|
const capturedDomains = new Set(definition2.competitors.map((competitor) => competitor.domain));
|
|
74250
74649
|
if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
|
|
74251
74650
|
throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
|