@ainyc/canonry 4.80.0 → 4.81.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/canonry/references/canonry-cli.md +20 -12
- package/assets/assets/{BacklinksPage-dRc62jAY.js → BacklinksPage-DHShKKpo.js} +1 -1
- package/assets/assets/{ChartPrimitives-D2_IvTkk.js → ChartPrimitives-udHScxjY.js} +1 -1
- package/assets/assets/ProjectPage-BsS1anh7.js +6 -0
- package/assets/assets/{RunRow-C0MA3yuQ.js → RunRow-CXyPHMVQ.js} +1 -1
- package/assets/assets/{RunsPage-4uxTYgGy.js → RunsPage-BpQ_NpFt.js} +1 -1
- package/assets/assets/{SettingsPage-3-SLhcJ7.js → SettingsPage-1ep4ch7n.js} +1 -1
- package/assets/assets/{TrafficPage-DZ50qwme.js → TrafficPage-C3Hx-sE7.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-B26n2R6G.js +1 -0
- package/assets/assets/{arrow-left-BaZIkAXX.js → arrow-left-Dc_IPJxw.js} +1 -1
- package/assets/assets/{extract-error-message-cpvfuFqW.js → extract-error-message-B3PoKkHW.js} +1 -1
- package/assets/assets/{index-EnY_OBRd.js → index-DhdFTQkU.js} +86 -86
- package/assets/assets/{trash-2-JpcztiS5.js → trash-2-BQ69cGl0.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-2QBSRHSN.js → chunk-6XOZSS3Y.js} +72 -8
- package/dist/{chunk-AVN6Q6LM.js → chunk-GMT3YPLT.js} +76 -2
- package/dist/{chunk-CXIGHPBE.js → chunk-UAQ42NVJ.js} +440 -123
- package/dist/{chunk-LCABGFYN.js → chunk-VX5C7DK7.js} +404 -242
- package/dist/cli.js +103 -8
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-ZWW3I3NL.js → intelligence-service-CAAQAKPN.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/assets/assets/ProjectPage-DSuvRUIf.js +0 -6
- package/assets/assets/TrafficSourceDetailPage-CzK5TMFp.js +0 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AI_ENGINE_DOMAINS,
|
|
4
4
|
AI_PROVIDER_INFRA_DOMAINS,
|
|
5
5
|
AppError,
|
|
6
|
+
BacklinkSources,
|
|
6
7
|
CcReleaseSyncStatuses,
|
|
7
8
|
CheckCategories,
|
|
8
9
|
CheckScopes,
|
|
@@ -52,6 +53,8 @@ import {
|
|
|
52
53
|
authRequired,
|
|
53
54
|
backlinkHistoryEntrySchema,
|
|
54
55
|
backlinkListResponseSchema,
|
|
56
|
+
backlinkSourceSchema,
|
|
57
|
+
backlinkSourcesResponseSchema,
|
|
55
58
|
backlinkSummaryDtoSchema,
|
|
56
59
|
backlinksInstallResultDtoSchema,
|
|
57
60
|
backlinksInstallStatusDtoSchema,
|
|
@@ -242,10 +245,10 @@ import {
|
|
|
242
245
|
wordpressSchemaDeployResultDtoSchema,
|
|
243
246
|
wordpressSchemaStatusResultDtoSchema,
|
|
244
247
|
wordpressStatusDtoSchema
|
|
245
|
-
} from "./chunk-
|
|
248
|
+
} from "./chunk-GMT3YPLT.js";
|
|
246
249
|
|
|
247
250
|
// src/intelligence-service.ts
|
|
248
|
-
import { eq as
|
|
251
|
+
import { eq as eq36, desc as desc17, asc as asc5, and as and26, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
249
252
|
|
|
250
253
|
// ../db/src/client.ts
|
|
251
254
|
import { mkdirSync } from "fs";
|
|
@@ -850,7 +853,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
|
|
|
850
853
|
var backlinkDomains = sqliteTable("backlink_domains", {
|
|
851
854
|
id: text("id").primaryKey(),
|
|
852
855
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
853
|
-
|
|
856
|
+
// Nullable: Bing Webmaster backlink rows have no Common Crawl release sync.
|
|
857
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
858
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
854
859
|
release: text("release").notNull(),
|
|
855
860
|
targetDomain: text("target_domain").notNull(),
|
|
856
861
|
linkingDomain: text("linking_domain").notNull(),
|
|
@@ -861,12 +866,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
|
|
|
861
866
|
index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
|
|
862
867
|
index("idx_backlink_domains_project_release").on(table.projectId, table.release),
|
|
863
868
|
index("idx_backlink_domains_hosts").on(table.numHosts),
|
|
864
|
-
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.release, table.linkingDomain)
|
|
869
|
+
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.source, table.release, table.linkingDomain)
|
|
865
870
|
]);
|
|
866
871
|
var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
867
872
|
id: text("id").primaryKey(),
|
|
868
873
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
869
|
-
|
|
874
|
+
// Nullable: Bing Webmaster summaries have no Common Crawl release sync.
|
|
875
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
876
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
870
877
|
release: text("release").notNull(),
|
|
871
878
|
targetDomain: text("target_domain").notNull(),
|
|
872
879
|
totalLinkingDomains: integer("total_linking_domains").notNull(),
|
|
@@ -875,7 +882,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
|
875
882
|
queriedAt: text("queried_at").notNull(),
|
|
876
883
|
createdAt: text("created_at").notNull()
|
|
877
884
|
}, (table) => [
|
|
878
|
-
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.release),
|
|
885
|
+
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.source, table.release),
|
|
879
886
|
index("idx_backlink_summaries_project").on(table.projectId)
|
|
880
887
|
]);
|
|
881
888
|
var agentMemory = sqliteTable("agent_memory", {
|
|
@@ -3051,8 +3058,86 @@ var MIGRATION_VERSIONS = [
|
|
|
3051
3058
|
`CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
|
|
3052
3059
|
`CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
|
|
3053
3060
|
]
|
|
3061
|
+
},
|
|
3062
|
+
{
|
|
3063
|
+
// Bing Webmaster inbound links land in the SAME backlink store as Common
|
|
3064
|
+
// Crawl, tagged by a `source` discriminator (commoncrawl | bing-webmaster).
|
|
3065
|
+
// Bing rows have no `cc_release_syncs` row, so `release_sync_id` becomes
|
|
3066
|
+
// nullable and the per-window UNIQUE gains `source`. SQLite can't drop a
|
|
3067
|
+
// NOT NULL or rewrite a UNIQUE in place — canonical table rebuild (the
|
|
3068
|
+
// v58/v60 pattern). Guarded on the `source` column's absence so a replay
|
|
3069
|
+
// over the already-migrated schema is a no-op (the hardcoded
|
|
3070
|
+
// `source='commoncrawl'` backfill must never clobber real bing rows).
|
|
3071
|
+
version: 78,
|
|
3072
|
+
name: "backlinks-source-discriminator",
|
|
3073
|
+
statements: [],
|
|
3074
|
+
run: (tx) => {
|
|
3075
|
+
addBacklinkSourceDiscriminator(tx);
|
|
3076
|
+
}
|
|
3054
3077
|
}
|
|
3055
3078
|
];
|
|
3079
|
+
function rebuildBacklinkTableWithSource(tx, table) {
|
|
3080
|
+
if (!tableExists(tx, table)) return;
|
|
3081
|
+
if (columnExists(tx, table, "source")) return;
|
|
3082
|
+
if (table === "backlink_domains") {
|
|
3083
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
|
|
3084
|
+
tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
|
|
3085
|
+
id TEXT PRIMARY KEY,
|
|
3086
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3087
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3088
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3089
|
+
release TEXT NOT NULL,
|
|
3090
|
+
target_domain TEXT NOT NULL,
|
|
3091
|
+
linking_domain TEXT NOT NULL,
|
|
3092
|
+
num_hosts INTEGER NOT NULL,
|
|
3093
|
+
created_at TEXT NOT NULL
|
|
3094
|
+
)`));
|
|
3095
|
+
tx.run(sql.raw(`INSERT INTO backlink_domains_v78
|
|
3096
|
+
(id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
|
|
3097
|
+
SELECT bd.id, bd.project_id,
|
|
3098
|
+
CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
|
|
3099
|
+
'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
|
|
3100
|
+
FROM backlink_domains bd
|
|
3101
|
+
WHERE bd.project_id IN (SELECT id FROM projects)`));
|
|
3102
|
+
tx.run(sql.raw(`DROP TABLE backlink_domains`));
|
|
3103
|
+
tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
|
|
3104
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
|
|
3105
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
|
|
3106
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
|
|
3107
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
|
|
3108
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
|
|
3112
|
+
tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
|
|
3113
|
+
id TEXT PRIMARY KEY,
|
|
3114
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3115
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3116
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3117
|
+
release TEXT NOT NULL,
|
|
3118
|
+
target_domain TEXT NOT NULL,
|
|
3119
|
+
total_linking_domains INTEGER NOT NULL,
|
|
3120
|
+
total_hosts INTEGER NOT NULL,
|
|
3121
|
+
top_10_hosts_share TEXT NOT NULL,
|
|
3122
|
+
queried_at TEXT NOT NULL,
|
|
3123
|
+
created_at TEXT NOT NULL
|
|
3124
|
+
)`));
|
|
3125
|
+
tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
|
|
3126
|
+
(id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
|
|
3127
|
+
SELECT bs.id, bs.project_id,
|
|
3128
|
+
CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
|
|
3129
|
+
'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
|
|
3130
|
+
FROM backlink_summaries bs
|
|
3131
|
+
WHERE bs.project_id IN (SELECT id FROM projects)`));
|
|
3132
|
+
tx.run(sql.raw(`DROP TABLE backlink_summaries`));
|
|
3133
|
+
tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
|
|
3134
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
|
|
3135
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
|
|
3136
|
+
}
|
|
3137
|
+
function addBacklinkSourceDiscriminator(tx) {
|
|
3138
|
+
rebuildBacklinkTableWithSource(tx, "backlink_domains");
|
|
3139
|
+
rebuildBacklinkTableWithSource(tx, "backlink_summaries");
|
|
3140
|
+
}
|
|
3056
3141
|
function isDuplicateColumnError(err) {
|
|
3057
3142
|
if (!(err instanceof Error)) return false;
|
|
3058
3143
|
if (err.message.includes("duplicate column name")) return true;
|
|
@@ -13206,6 +13291,7 @@ var SCHEMA_TABLE = {
|
|
|
13206
13291
|
AuditLogEntry: auditLogEntrySchema,
|
|
13207
13292
|
BacklinkHistoryEntry: backlinkHistoryEntrySchema,
|
|
13208
13293
|
BacklinkListResponse: backlinkListResponseSchema,
|
|
13294
|
+
BacklinkSourcesResponse: backlinkSourcesResponseSchema,
|
|
13209
13295
|
BacklinkSummaryDto: backlinkSummaryDtoSchema,
|
|
13210
13296
|
BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
|
|
13211
13297
|
BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
|
|
@@ -16666,7 +16752,8 @@ var routeCatalog = [
|
|
|
16666
16752
|
tags: ["backlinks"],
|
|
16667
16753
|
parameters: [
|
|
16668
16754
|
nameParameter,
|
|
16669
|
-
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema }
|
|
16755
|
+
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16756
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16670
16757
|
],
|
|
16671
16758
|
responses: {
|
|
16672
16759
|
200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
|
|
@@ -16684,7 +16771,8 @@ var routeCatalog = [
|
|
|
16684
16771
|
nameParameter,
|
|
16685
16772
|
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16686
16773
|
{ name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
|
|
16687
|
-
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema }
|
|
16774
|
+
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema },
|
|
16775
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16688
16776
|
],
|
|
16689
16777
|
responses: {
|
|
16690
16778
|
200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
|
|
@@ -16696,12 +16784,44 @@ var routeCatalog = [
|
|
|
16696
16784
|
path: "/api/v1/projects/{name}/backlinks/history",
|
|
16697
16785
|
summary: "Get per-release backlink summaries for a project",
|
|
16698
16786
|
tags: ["backlinks"],
|
|
16699
|
-
parameters: [
|
|
16787
|
+
parameters: [
|
|
16788
|
+
nameParameter,
|
|
16789
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16790
|
+
],
|
|
16700
16791
|
responses: {
|
|
16701
16792
|
200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
|
|
16702
16793
|
404: errorResponse("Project not found.")
|
|
16703
16794
|
}
|
|
16704
16795
|
},
|
|
16796
|
+
{
|
|
16797
|
+
method: "get",
|
|
16798
|
+
path: "/api/v1/projects/{name}/backlinks/sources",
|
|
16799
|
+
summary: "Report per-source backlink availability for a project",
|
|
16800
|
+
description: "Returns connection + data availability for every backlink source (commoncrawl, bing-webmaster) so callers can degrade gracefully across CC-only / Bing-only / both / neither.",
|
|
16801
|
+
tags: ["backlinks"],
|
|
16802
|
+
parameters: [
|
|
16803
|
+
nameParameter,
|
|
16804
|
+
{ name: "excludeCrawlers", in: "query", description: 'When "1"/"true", count linking domains excluding crawler/proxy hosts (matches the dashboard). Default off.', schema: stringSchema }
|
|
16805
|
+
],
|
|
16806
|
+
responses: {
|
|
16807
|
+
200: jsonResponse("Per-source availability returned.", "BacklinkSourcesResponse"),
|
|
16808
|
+
404: errorResponse("Project not found.")
|
|
16809
|
+
}
|
|
16810
|
+
},
|
|
16811
|
+
{
|
|
16812
|
+
method: "post",
|
|
16813
|
+
path: "/api/v1/projects/{name}/backlinks/bing-sync",
|
|
16814
|
+
summary: "Sync a project's inbound links from Bing Webmaster Tools",
|
|
16815
|
+
description: 'Creates a tracking run and pulls inbound links live from the connected Bing Webmaster account, writing source="bing-webmaster" backlink rows. Requires a Bing connection for the project domain.',
|
|
16816
|
+
tags: ["backlinks"],
|
|
16817
|
+
parameters: [nameParameter],
|
|
16818
|
+
responses: {
|
|
16819
|
+
201: jsonResponse("Bing sync run queued.", "RunDto"),
|
|
16820
|
+
400: errorResponse("No Bing Webmaster connection for this project."),
|
|
16821
|
+
404: errorResponse("Project not found."),
|
|
16822
|
+
422: errorResponse("Bing backlinks sync is not available on this deployment.")
|
|
16823
|
+
}
|
|
16824
|
+
},
|
|
16705
16825
|
{
|
|
16706
16826
|
method: "post",
|
|
16707
16827
|
path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
|
|
@@ -20901,6 +21021,7 @@ var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
|
20901
21021
|
var BING_SUBMIT_URL_BATCH_LIMIT = 500;
|
|
20902
21022
|
var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
|
|
20903
21023
|
var BING_REQUEST_TIMEOUT_MS = 3e4;
|
|
21024
|
+
var BING_LINKS_MAX_PAGES = 20;
|
|
20904
21025
|
|
|
20905
21026
|
// ../integration-bing/src/types.ts
|
|
20906
21027
|
var BingApiError = class extends Error {
|
|
@@ -21064,6 +21185,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
|
|
|
21064
21185
|
const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
|
|
21065
21186
|
return data ?? [];
|
|
21066
21187
|
}
|
|
21188
|
+
async function getLinkCounts(apiKey, siteUrl, opts = {}) {
|
|
21189
|
+
validateApiKey(apiKey);
|
|
21190
|
+
validateSiteUrl2(siteUrl);
|
|
21191
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21192
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21193
|
+
const out = [];
|
|
21194
|
+
let page = 0;
|
|
21195
|
+
let totalPages = 1;
|
|
21196
|
+
while (page < totalPages && page < maxPages) {
|
|
21197
|
+
const data = await bingFetch(apiKey, `GetLinkCounts?siteUrl=${encodedSite}&page=${page}`);
|
|
21198
|
+
for (const link of data?.Links ?? []) {
|
|
21199
|
+
if (link && typeof link.Url === "string") {
|
|
21200
|
+
out.push({ Url: link.Url, Count: Number(link.Count ?? 0) });
|
|
21201
|
+
}
|
|
21202
|
+
}
|
|
21203
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21204
|
+
page++;
|
|
21205
|
+
}
|
|
21206
|
+
return out;
|
|
21207
|
+
}
|
|
21208
|
+
async function getUrlLinks(apiKey, siteUrl, link, opts = {}) {
|
|
21209
|
+
validateApiKey(apiKey);
|
|
21210
|
+
validateSiteUrl2(siteUrl);
|
|
21211
|
+
validateUrl2(link);
|
|
21212
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21213
|
+
const encodedLink = encodeURIComponent(link);
|
|
21214
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21215
|
+
const out = [];
|
|
21216
|
+
let page = 0;
|
|
21217
|
+
let totalPages = 1;
|
|
21218
|
+
while (page < totalPages && page < maxPages) {
|
|
21219
|
+
const data = await bingFetch(
|
|
21220
|
+
apiKey,
|
|
21221
|
+
`GetUrlLinks?siteUrl=${encodedSite}&link=${encodedLink}&page=${page}`
|
|
21222
|
+
);
|
|
21223
|
+
for (const detail of data?.Details ?? []) {
|
|
21224
|
+
if (detail && typeof detail.Url === "string") {
|
|
21225
|
+
out.push({ Url: detail.Url, AnchorText: detail.AnchorText });
|
|
21226
|
+
}
|
|
21227
|
+
}
|
|
21228
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21229
|
+
page++;
|
|
21230
|
+
}
|
|
21231
|
+
return out;
|
|
21232
|
+
}
|
|
21067
21233
|
|
|
21068
21234
|
// ../api-routes/src/bing.ts
|
|
21069
21235
|
function parseBingDate(value) {
|
|
@@ -24662,9 +24828,18 @@ function mapSummaryRow(row) {
|
|
|
24662
24828
|
totalLinkingDomains: row.totalLinkingDomains,
|
|
24663
24829
|
totalHosts: row.totalHosts,
|
|
24664
24830
|
top10HostsShare: row.top10HostsShare,
|
|
24665
|
-
queriedAt: row.queriedAt
|
|
24831
|
+
queriedAt: row.queriedAt,
|
|
24832
|
+
source: row.source
|
|
24666
24833
|
};
|
|
24667
24834
|
}
|
|
24835
|
+
function parseSourceParam(value) {
|
|
24836
|
+
if (value === void 0 || value === "") return BacklinkSources.commoncrawl;
|
|
24837
|
+
const parsed = backlinkSourceSchema.safeParse(value);
|
|
24838
|
+
if (!parsed.success) {
|
|
24839
|
+
throw validationError(`Invalid source "${value}". Expected one of: ${Object.values(BacklinkSources).join(", ")}.`);
|
|
24840
|
+
}
|
|
24841
|
+
return parsed.data;
|
|
24842
|
+
}
|
|
24668
24843
|
function mapRunRow(row) {
|
|
24669
24844
|
return {
|
|
24670
24845
|
id: row.id,
|
|
@@ -24679,9 +24854,13 @@ function mapRunRow(row) {
|
|
|
24679
24854
|
createdAt: row.createdAt
|
|
24680
24855
|
};
|
|
24681
24856
|
}
|
|
24682
|
-
function latestSummaryForProject(db, projectId, release) {
|
|
24683
|
-
const
|
|
24684
|
-
|
|
24857
|
+
function latestSummaryForProject(db, projectId, source, release) {
|
|
24858
|
+
const conditions = [
|
|
24859
|
+
eq25(backlinkSummaries.projectId, projectId),
|
|
24860
|
+
eq25(backlinkSummaries.source, source)
|
|
24861
|
+
];
|
|
24862
|
+
if (release) conditions.push(eq25(backlinkSummaries.release, release));
|
|
24863
|
+
return db.select().from(backlinkSummaries).where(and19(...conditions)).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24685
24864
|
}
|
|
24686
24865
|
function parseExcludeCrawlers(value) {
|
|
24687
24866
|
if (!value) return false;
|
|
@@ -24691,6 +24870,7 @@ function parseExcludeCrawlers(value) {
|
|
|
24691
24870
|
function computeFilteredSummary(db, base) {
|
|
24692
24871
|
const baseDomainCondition = and19(
|
|
24693
24872
|
eq25(backlinkDomains.projectId, base.projectId),
|
|
24873
|
+
eq25(backlinkDomains.source, base.source),
|
|
24694
24874
|
eq25(backlinkDomains.release, base.release)
|
|
24695
24875
|
);
|
|
24696
24876
|
const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
@@ -24717,10 +24897,48 @@ function computeFilteredSummary(db, base) {
|
|
|
24717
24897
|
totalHosts,
|
|
24718
24898
|
top10HostsShare: top10Share.toFixed(6),
|
|
24719
24899
|
queriedAt: base.queriedAt,
|
|
24900
|
+
source: base.source,
|
|
24720
24901
|
excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
|
|
24721
24902
|
excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
|
|
24722
24903
|
};
|
|
24723
24904
|
}
|
|
24905
|
+
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
24906
|
+
const summary = db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.source, source))).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24907
|
+
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
24908
|
+
if (summary && excludeCrawlers) {
|
|
24909
|
+
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and19(
|
|
24910
|
+
eq25(backlinkDomains.projectId, projectId),
|
|
24911
|
+
eq25(backlinkDomains.source, source),
|
|
24912
|
+
eq25(backlinkDomains.release, summary.release),
|
|
24913
|
+
backlinkCrawlerExclusionClause()
|
|
24914
|
+
)).get();
|
|
24915
|
+
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
24916
|
+
}
|
|
24917
|
+
return {
|
|
24918
|
+
source,
|
|
24919
|
+
connected,
|
|
24920
|
+
hasData: !!summary,
|
|
24921
|
+
latestRelease: summary?.release ?? null,
|
|
24922
|
+
totalLinkingDomains,
|
|
24923
|
+
lastSyncedAt: summary?.queriedAt ?? null
|
|
24924
|
+
};
|
|
24925
|
+
}
|
|
24926
|
+
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
24927
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
24928
|
+
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
24929
|
+
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
24930
|
+
const sources = [
|
|
24931
|
+
buildSourceAvailability(db, project.id, BacklinkSources.commoncrawl, ccConnected, excludeCrawlers),
|
|
24932
|
+
buildSourceAvailability(db, project.id, BacklinkSources["bing-webmaster"], bingConnected, excludeCrawlers)
|
|
24933
|
+
];
|
|
24934
|
+
return {
|
|
24935
|
+
projectId: project.id,
|
|
24936
|
+
targetDomain: project.canonicalDomain,
|
|
24937
|
+
sources,
|
|
24938
|
+
anyConnected: sources.some((s) => s.connected),
|
|
24939
|
+
anyData: sources.some((s) => s.hasData)
|
|
24940
|
+
};
|
|
24941
|
+
}
|
|
24724
24942
|
async function backlinksRoutes(app, opts) {
|
|
24725
24943
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
24726
24944
|
if (!opts.getBacklinksStatus) {
|
|
@@ -24852,7 +25070,8 @@ async function backlinksRoutes(app, opts) {
|
|
|
24852
25070
|
"/projects/:name/backlinks/summary",
|
|
24853
25071
|
async (request, reply) => {
|
|
24854
25072
|
const project = resolveProject(app.db, request.params.name);
|
|
24855
|
-
const
|
|
25073
|
+
const source = parseSourceParam(request.query.source);
|
|
25074
|
+
const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24856
25075
|
if (!row) return reply.send(null);
|
|
24857
25076
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24858
25077
|
return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
|
|
@@ -24860,10 +25079,11 @@ async function backlinksRoutes(app, opts) {
|
|
|
24860
25079
|
);
|
|
24861
25080
|
app.get("/projects/:name/backlinks/domains", async (request, reply) => {
|
|
24862
25081
|
const project = resolveProject(app.db, request.params.name);
|
|
24863
|
-
const
|
|
25082
|
+
const source = parseSourceParam(request.query.source);
|
|
25083
|
+
const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24864
25084
|
const targetRelease = request.query.release ?? summaryRow?.release;
|
|
24865
25085
|
if (!targetRelease) {
|
|
24866
|
-
const response2 = { summary: null, total: 0, rows: [] };
|
|
25086
|
+
const response2 = { source, summary: null, total: 0, rows: [] };
|
|
24867
25087
|
return reply.send(response2);
|
|
24868
25088
|
}
|
|
24869
25089
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
@@ -24871,19 +25091,22 @@ async function backlinksRoutes(app, opts) {
|
|
|
24871
25091
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24872
25092
|
const baseDomainCondition = and19(
|
|
24873
25093
|
eq25(backlinkDomains.projectId, project.id),
|
|
25094
|
+
eq25(backlinkDomains.source, source),
|
|
24874
25095
|
eq25(backlinkDomains.release, targetRelease)
|
|
24875
25096
|
);
|
|
24876
25097
|
const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
24877
25098
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
24878
25099
|
const rows = app.db.select({
|
|
24879
25100
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
24880
|
-
numHosts: backlinkDomains.numHosts
|
|
25101
|
+
numHosts: backlinkDomains.numHosts,
|
|
25102
|
+
source: backlinkDomains.source
|
|
24881
25103
|
}).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
24882
25104
|
let summary = null;
|
|
24883
25105
|
if (summaryRow) {
|
|
24884
25106
|
summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
|
|
24885
25107
|
}
|
|
24886
25108
|
const response = {
|
|
25109
|
+
source,
|
|
24887
25110
|
summary,
|
|
24888
25111
|
total: Number(totalRow?.count ?? 0),
|
|
24889
25112
|
rows
|
|
@@ -24894,17 +25117,58 @@ async function backlinksRoutes(app, opts) {
|
|
|
24894
25117
|
"/projects/:name/backlinks/history",
|
|
24895
25118
|
async (request, reply) => {
|
|
24896
25119
|
const project = resolveProject(app.db, request.params.name);
|
|
24897
|
-
const
|
|
25120
|
+
const source = parseSourceParam(request.query.source);
|
|
25121
|
+
const rows = app.db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, project.id), eq25(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
24898
25122
|
const response = rows.map((r) => ({
|
|
24899
25123
|
release: r.release,
|
|
24900
25124
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
24901
25125
|
totalHosts: r.totalHosts,
|
|
24902
25126
|
top10HostsShare: r.top10HostsShare,
|
|
24903
|
-
queriedAt: r.queriedAt
|
|
25127
|
+
queriedAt: r.queriedAt,
|
|
25128
|
+
source: r.source
|
|
24904
25129
|
}));
|
|
24905
25130
|
return reply.send(response);
|
|
24906
25131
|
}
|
|
24907
25132
|
);
|
|
25133
|
+
app.get(
|
|
25134
|
+
"/projects/:name/backlinks/sources",
|
|
25135
|
+
async (request, reply) => {
|
|
25136
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25137
|
+
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25138
|
+
const response = computeSourceAvailability(app.db, project, opts.bingConnectionStore, excludeCrawlers);
|
|
25139
|
+
return reply.send(response);
|
|
25140
|
+
}
|
|
25141
|
+
);
|
|
25142
|
+
app.post(
|
|
25143
|
+
"/projects/:name/backlinks/bing-sync",
|
|
25144
|
+
async (request, reply) => {
|
|
25145
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25146
|
+
if (!opts.onBingBacklinkSyncRequested) {
|
|
25147
|
+
throw missingDependency(
|
|
25148
|
+
"Bing backlinks sync is only available from a local canonry install with Bing Webmaster connected."
|
|
25149
|
+
);
|
|
25150
|
+
}
|
|
25151
|
+
const conn = opts.bingConnectionStore?.getConnection(project.canonicalDomain);
|
|
25152
|
+
if (!conn) {
|
|
25153
|
+
throw validationError(
|
|
25154
|
+
`No Bing Webmaster connection for "${project.name}". Run \`canonry bing connect ${project.name} --api-key <key>\` first.`
|
|
25155
|
+
);
|
|
25156
|
+
}
|
|
25157
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25158
|
+
const runId = crypto22.randomUUID();
|
|
25159
|
+
app.db.insert(runs).values({
|
|
25160
|
+
id: runId,
|
|
25161
|
+
projectId: project.id,
|
|
25162
|
+
kind: RunKinds["backlink-extract"],
|
|
25163
|
+
status: RunStatuses.queued,
|
|
25164
|
+
trigger: RunTriggers.manual,
|
|
25165
|
+
createdAt: now
|
|
25166
|
+
}).run();
|
|
25167
|
+
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25168
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
25169
|
+
return reply.status(201).send(mapRunRow(run));
|
|
25170
|
+
}
|
|
25171
|
+
);
|
|
24908
25172
|
}
|
|
24909
25173
|
|
|
24910
25174
|
// ../api-routes/src/traffic.ts
|
|
@@ -30300,6 +30564,54 @@ function readInstalledManifest(skillDir) {
|
|
|
30300
30564
|
}
|
|
30301
30565
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
30302
30566
|
|
|
30567
|
+
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
30568
|
+
import { and as and21, eq as eq27 } from "drizzle-orm";
|
|
30569
|
+
function skippedNoProject() {
|
|
30570
|
+
return {
|
|
30571
|
+
status: CheckStatuses.skipped,
|
|
30572
|
+
code: "backlinks.source.no-project",
|
|
30573
|
+
summary: "Project context required."
|
|
30574
|
+
};
|
|
30575
|
+
}
|
|
30576
|
+
var BACKLINKS_CHECKS = [
|
|
30577
|
+
{
|
|
30578
|
+
id: "backlinks.source.connected",
|
|
30579
|
+
category: CheckCategories.integrations,
|
|
30580
|
+
scope: CheckScopes.project,
|
|
30581
|
+
title: "Backlinks source connected",
|
|
30582
|
+
run: (ctx) => {
|
|
30583
|
+
if (!ctx.project) return skippedNoProject();
|
|
30584
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq27(projects.id, ctx.project.id)).get();
|
|
30585
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
30586
|
+
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
30587
|
+
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
30588
|
+
const connected = [];
|
|
30589
|
+
if (ccConnected) connected.push(BacklinkSources.commoncrawl);
|
|
30590
|
+
if (bingConnected) connected.push(BacklinkSources["bing-webmaster"]);
|
|
30591
|
+
if (connected.length === 0) {
|
|
30592
|
+
return {
|
|
30593
|
+
status: CheckStatuses.warn,
|
|
30594
|
+
code: "backlinks.source.none",
|
|
30595
|
+
summary: `No backlink source is set up for ${ctx.project.name}.`,
|
|
30596
|
+
remediation: `Enable Common Crawl (set autoExtractBacklinks on the project + run \`canonry backlinks sync\`) or connect Bing Webmaster (\`canonry bing connect ${ctx.project.name} --api-key <key>\` then \`canonry backlinks bing-sync ${ctx.project.name}\`).`,
|
|
30597
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
30598
|
+
};
|
|
30599
|
+
}
|
|
30600
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and21(
|
|
30601
|
+
eq27(backlinkSummaries.projectId, ctx.project.id),
|
|
30602
|
+
eq27(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
30603
|
+
)).limit(1).get() : false;
|
|
30604
|
+
return {
|
|
30605
|
+
status: CheckStatuses.ok,
|
|
30606
|
+
code: "backlinks.source.connected",
|
|
30607
|
+
summary: `${connected.length} backlink source${connected.length === 1 ? "" : "s"} set up: ${connected.join(", ")}.`,
|
|
30608
|
+
remediation: ccConnected && !ccHasData ? `Common Crawl is ready but no backlinks have been extracted for ${ctx.project.name} yet \u2014 run \`canonry backlinks extract ${ctx.project.name}\`.` : null,
|
|
30609
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected, connected, commoncrawlHasData: ccHasData }
|
|
30610
|
+
};
|
|
30611
|
+
}
|
|
30612
|
+
}
|
|
30613
|
+
];
|
|
30614
|
+
|
|
30303
30615
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
30304
30616
|
var BING_AUTH_CHECKS = [
|
|
30305
30617
|
{
|
|
@@ -30447,10 +30759,10 @@ var BING_AUTH_CHECKS = [
|
|
|
30447
30759
|
];
|
|
30448
30760
|
|
|
30449
30761
|
// ../api-routes/src/doctor/checks/content.ts
|
|
30450
|
-
import { eq as
|
|
30762
|
+
import { eq as eq28 } from "drizzle-orm";
|
|
30451
30763
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
30452
30764
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
30453
|
-
function
|
|
30765
|
+
function skippedNoProject2() {
|
|
30454
30766
|
return {
|
|
30455
30767
|
status: CheckStatuses.skipped,
|
|
30456
30768
|
code: "content.winnability.no-project",
|
|
@@ -30460,7 +30772,7 @@ function skippedNoProject() {
|
|
|
30460
30772
|
}
|
|
30461
30773
|
function loadProject(ctx) {
|
|
30462
30774
|
if (!ctx.project) return null;
|
|
30463
|
-
return ctx.db.select().from(projects).where(
|
|
30775
|
+
return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
|
|
30464
30776
|
}
|
|
30465
30777
|
function percent(value) {
|
|
30466
30778
|
return Math.round(value * 100);
|
|
@@ -30471,7 +30783,7 @@ var winnabilityCoverageCheck = {
|
|
|
30471
30783
|
scope: CheckScopes.project,
|
|
30472
30784
|
title: "Content winnability classification coverage",
|
|
30473
30785
|
run: (ctx) => {
|
|
30474
|
-
if (!ctx.project) return
|
|
30786
|
+
if (!ctx.project) return skippedNoProject2();
|
|
30475
30787
|
const project = loadProject(ctx);
|
|
30476
30788
|
if (!project) {
|
|
30477
30789
|
return {
|
|
@@ -30552,7 +30864,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30552
30864
|
);
|
|
30553
30865
|
|
|
30554
30866
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
30555
|
-
import { eq as
|
|
30867
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30556
30868
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30557
30869
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30558
30870
|
var adsConnectionCheck = {
|
|
@@ -30569,7 +30881,7 @@ var adsConnectionCheck = {
|
|
|
30569
30881
|
remediation: null
|
|
30570
30882
|
};
|
|
30571
30883
|
}
|
|
30572
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
30884
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30573
30885
|
if (!row) {
|
|
30574
30886
|
return {
|
|
30575
30887
|
status: CheckStatuses.skipped,
|
|
@@ -30619,7 +30931,7 @@ var adsRecentSyncCheck = {
|
|
|
30619
30931
|
remediation: null
|
|
30620
30932
|
};
|
|
30621
30933
|
}
|
|
30622
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
30934
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30623
30935
|
if (!row) {
|
|
30624
30936
|
return {
|
|
30625
30937
|
status: CheckStatuses.skipped,
|
|
@@ -30810,10 +31122,10 @@ var ga4ConnectionCheck = {
|
|
|
30810
31122
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
30811
31123
|
|
|
30812
31124
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
30813
|
-
import { and as
|
|
31125
|
+
import { and as and22, eq as eq30 } from "drizzle-orm";
|
|
30814
31126
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
30815
31127
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
30816
|
-
function
|
|
31128
|
+
function skippedNoProject3() {
|
|
30817
31129
|
return {
|
|
30818
31130
|
status: CheckStatuses.skipped,
|
|
30819
31131
|
code: "gbp.auth.no-project",
|
|
@@ -30830,7 +31142,7 @@ function storeUnavailable() {
|
|
|
30830
31142
|
};
|
|
30831
31143
|
}
|
|
30832
31144
|
async function resolveGbpToken(ctx) {
|
|
30833
|
-
if (!ctx.project) return { ok: false, output:
|
|
31145
|
+
if (!ctx.project) return { ok: false, output: skippedNoProject3() };
|
|
30834
31146
|
const store = ctx.googleConnectionStore;
|
|
30835
31147
|
if (!store) return { ok: false, output: storeUnavailable() };
|
|
30836
31148
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
@@ -30908,7 +31220,7 @@ var scopesCheck = {
|
|
|
30908
31220
|
scope: CheckScopes.project,
|
|
30909
31221
|
title: "GBP granted scopes",
|
|
30910
31222
|
run: async (ctx) => {
|
|
30911
|
-
if (!ctx.project) return
|
|
31223
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30912
31224
|
const store = ctx.googleConnectionStore;
|
|
30913
31225
|
if (!store) return storeUnavailable();
|
|
30914
31226
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -30945,7 +31257,7 @@ var accountAccessCheck = {
|
|
|
30945
31257
|
scope: CheckScopes.project,
|
|
30946
31258
|
title: "GBP account access",
|
|
30947
31259
|
run: async (ctx) => {
|
|
30948
|
-
if (!ctx.project) return
|
|
31260
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30949
31261
|
const store = ctx.googleConnectionStore;
|
|
30950
31262
|
if (!store) return storeUnavailable();
|
|
30951
31263
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -31042,8 +31354,8 @@ var recentSyncCheck = {
|
|
|
31042
31354
|
scope: CheckScopes.project,
|
|
31043
31355
|
title: "GBP recent sync",
|
|
31044
31356
|
run: (ctx) => {
|
|
31045
|
-
if (!ctx.project) return
|
|
31046
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31357
|
+
if (!ctx.project) return skippedNoProject3();
|
|
31358
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and22(eq30(gbpLocations.projectId, ctx.project.id), eq30(gbpLocations.selected, true))).all();
|
|
31047
31359
|
if (selected.length === 0) {
|
|
31048
31360
|
return {
|
|
31049
31361
|
status: CheckStatuses.skipped,
|
|
@@ -31103,7 +31415,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
31103
31415
|
);
|
|
31104
31416
|
|
|
31105
31417
|
// ../api-routes/src/doctor/checks/places.ts
|
|
31106
|
-
import { eq as
|
|
31418
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
31107
31419
|
var apiKeyCheck = {
|
|
31108
31420
|
id: "gbp.places.api-key",
|
|
31109
31421
|
category: CheckCategories.auth,
|
|
@@ -31148,7 +31460,7 @@ var apiKeyCheck = {
|
|
|
31148
31460
|
details: { tier: cfg.tier }
|
|
31149
31461
|
};
|
|
31150
31462
|
}
|
|
31151
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31463
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
|
|
31152
31464
|
const selected = rows.filter((r) => r.selected);
|
|
31153
31465
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
31154
31466
|
const details = {
|
|
@@ -31184,7 +31496,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
|
31184
31496
|
var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
|
|
31185
31497
|
async function resolveAccessToken(ctx) {
|
|
31186
31498
|
if (!ctx.project) {
|
|
31187
|
-
return { ok: false, output:
|
|
31499
|
+
return { ok: false, output: skippedNoProject4() };
|
|
31188
31500
|
}
|
|
31189
31501
|
const store = ctx.googleConnectionStore;
|
|
31190
31502
|
if (!store) {
|
|
@@ -31251,7 +31563,7 @@ async function resolveAccessToken(ctx) {
|
|
|
31251
31563
|
};
|
|
31252
31564
|
}
|
|
31253
31565
|
}
|
|
31254
|
-
function
|
|
31566
|
+
function skippedNoProject4() {
|
|
31255
31567
|
return {
|
|
31256
31568
|
status: CheckStatuses.skipped,
|
|
31257
31569
|
code: "google.auth.no-project",
|
|
@@ -31281,7 +31593,7 @@ var propertyAccessCheck = {
|
|
|
31281
31593
|
scope: CheckScopes.project,
|
|
31282
31594
|
title: "GSC property access",
|
|
31283
31595
|
run: async (ctx) => {
|
|
31284
|
-
if (!ctx.project) return
|
|
31596
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31285
31597
|
const store = ctx.googleConnectionStore;
|
|
31286
31598
|
if (!store) {
|
|
31287
31599
|
return {
|
|
@@ -31382,7 +31694,7 @@ var redirectUriCheck = {
|
|
|
31382
31694
|
scope: CheckScopes.project,
|
|
31383
31695
|
title: "OAuth redirect URI",
|
|
31384
31696
|
run: async (ctx) => {
|
|
31385
|
-
if (!ctx.project) return
|
|
31697
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31386
31698
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
31387
31699
|
if (!auth.clientId || !auth.clientSecret) {
|
|
31388
31700
|
return {
|
|
@@ -31436,7 +31748,7 @@ var scopesCheck2 = {
|
|
|
31436
31748
|
scope: CheckScopes.project,
|
|
31437
31749
|
title: "GSC granted scopes",
|
|
31438
31750
|
run: async (ctx) => {
|
|
31439
|
-
if (!ctx.project) return
|
|
31751
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31440
31752
|
const store = ctx.googleConnectionStore;
|
|
31441
31753
|
if (!store) {
|
|
31442
31754
|
return {
|
|
@@ -31599,10 +31911,10 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
31599
31911
|
];
|
|
31600
31912
|
|
|
31601
31913
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
31602
|
-
import { and as
|
|
31914
|
+
import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
31603
31915
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
31604
31916
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
31605
|
-
function
|
|
31917
|
+
function skippedNoProject5() {
|
|
31606
31918
|
return {
|
|
31607
31919
|
status: CheckStatuses.skipped,
|
|
31608
31920
|
code: "traffic.no-project",
|
|
@@ -31613,8 +31925,8 @@ function skippedNoProject4() {
|
|
|
31613
31925
|
function loadProbes(ctx) {
|
|
31614
31926
|
if (!ctx.project) return [];
|
|
31615
31927
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
31616
|
-
|
|
31617
|
-
|
|
31928
|
+
and23(
|
|
31929
|
+
eq32(trafficSources.projectId, ctx.project.id),
|
|
31618
31930
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
31619
31931
|
)
|
|
31620
31932
|
).all();
|
|
@@ -31636,7 +31948,7 @@ var sourceConnectedCheck = {
|
|
|
31636
31948
|
scope: CheckScopes.project,
|
|
31637
31949
|
title: "Traffic source connected",
|
|
31638
31950
|
run: (ctx) => {
|
|
31639
|
-
if (!ctx.project) return
|
|
31951
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31640
31952
|
const sources = loadProbes(ctx);
|
|
31641
31953
|
if (sources.length === 0) {
|
|
31642
31954
|
return {
|
|
@@ -31680,7 +31992,7 @@ var recentDataCheck = {
|
|
|
31680
31992
|
scope: CheckScopes.project,
|
|
31681
31993
|
title: "Traffic source recent data",
|
|
31682
31994
|
run: (ctx) => {
|
|
31683
|
-
if (!ctx.project) return
|
|
31995
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31684
31996
|
const sources = loadProbes(ctx);
|
|
31685
31997
|
if (sources.length === 0) {
|
|
31686
31998
|
return {
|
|
@@ -31694,16 +32006,16 @@ var recentDataCheck = {
|
|
|
31694
32006
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
31695
32007
|
const recentCrawlers = Number(
|
|
31696
32008
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31697
|
-
|
|
31698
|
-
|
|
32009
|
+
and23(
|
|
32010
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31699
32011
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
31700
32012
|
)
|
|
31701
32013
|
).get()?.total ?? 0
|
|
31702
32014
|
);
|
|
31703
32015
|
const recentReferrals = Number(
|
|
31704
32016
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31705
|
-
|
|
31706
|
-
|
|
32017
|
+
and23(
|
|
32018
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31707
32019
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
31708
32020
|
)
|
|
31709
32021
|
).get()?.total ?? 0
|
|
@@ -31718,16 +32030,16 @@ var recentDataCheck = {
|
|
|
31718
32030
|
}
|
|
31719
32031
|
const olderCrawlers = Number(
|
|
31720
32032
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31721
|
-
|
|
31722
|
-
|
|
32033
|
+
and23(
|
|
32034
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31723
32035
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
31724
32036
|
)
|
|
31725
32037
|
).get()?.total ?? 0
|
|
31726
32038
|
);
|
|
31727
32039
|
const olderReferrals = Number(
|
|
31728
32040
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31729
|
-
|
|
31730
|
-
|
|
32041
|
+
and23(
|
|
32042
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31731
32043
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
31732
32044
|
)
|
|
31733
32045
|
).get()?.total ?? 0
|
|
@@ -31842,7 +32154,7 @@ var credentialsCheck = {
|
|
|
31842
32154
|
scope: CheckScopes.project,
|
|
31843
32155
|
title: "Traffic source credentials",
|
|
31844
32156
|
run: async (ctx) => {
|
|
31845
|
-
if (!ctx.project) return
|
|
32157
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31846
32158
|
const sources = loadProbes(ctx);
|
|
31847
32159
|
if (sources.length === 0) {
|
|
31848
32160
|
return {
|
|
@@ -31871,7 +32183,7 @@ var scopesCheck3 = {
|
|
|
31871
32183
|
scope: CheckScopes.project,
|
|
31872
32184
|
title: "Traffic source scopes",
|
|
31873
32185
|
run: async (ctx) => {
|
|
31874
|
-
if (!ctx.project) return
|
|
32186
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31875
32187
|
const sources = loadProbes(ctx);
|
|
31876
32188
|
if (sources.length === 0) {
|
|
31877
32189
|
return {
|
|
@@ -31900,7 +32212,7 @@ var cacheBlindSpotCheck = {
|
|
|
31900
32212
|
scope: CheckScopes.project,
|
|
31901
32213
|
title: "WordPress traffic cache blind spot",
|
|
31902
32214
|
run: (ctx) => {
|
|
31903
|
-
if (!ctx.project) return
|
|
32215
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31904
32216
|
const wpSources = loadProbes(ctx).filter(
|
|
31905
32217
|
(s) => s.sourceType === TrafficSourceTypes.wordpress
|
|
31906
32218
|
);
|
|
@@ -32015,6 +32327,7 @@ var ALL_CHECKS = [
|
|
|
32015
32327
|
...ADS_CHECKS,
|
|
32016
32328
|
...PROVIDERS_CHECKS,
|
|
32017
32329
|
...TRAFFIC_SOURCE_CHECKS,
|
|
32330
|
+
...BACKLINKS_CHECKS,
|
|
32018
32331
|
...CONTENT_CHECKS,
|
|
32019
32332
|
...AGENT_CHECKS
|
|
32020
32333
|
];
|
|
@@ -32140,7 +32453,7 @@ async function doctorRoutes(app, opts) {
|
|
|
32140
32453
|
|
|
32141
32454
|
// ../api-routes/src/discovery/routes.ts
|
|
32142
32455
|
import crypto26 from "crypto";
|
|
32143
|
-
import { and as
|
|
32456
|
+
import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
|
|
32144
32457
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
32145
32458
|
async function discoveryRoutes(app, opts) {
|
|
32146
32459
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -32172,9 +32485,9 @@ async function discoveryRoutes(app, opts) {
|
|
|
32172
32485
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32173
32486
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
32174
32487
|
const decision = app.db.transaction((tx) => {
|
|
32175
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
32176
|
-
|
|
32177
|
-
|
|
32488
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and24(
|
|
32489
|
+
eq33(discoverySessions.projectId, project.id),
|
|
32490
|
+
eq33(discoverySessions.icpDescription, icpDescription),
|
|
32178
32491
|
inArray11(discoverySessions.status, [
|
|
32179
32492
|
DiscoverySessionStatuses.queued,
|
|
32180
32493
|
DiscoverySessionStatuses.seeding,
|
|
@@ -32244,7 +32557,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32244
32557
|
const project = resolveProject(app.db, request.params.name);
|
|
32245
32558
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
32246
32559
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
32247
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32560
|
+
const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
|
|
32248
32561
|
return reply.send(rows.map(serializeSession));
|
|
32249
32562
|
}
|
|
32250
32563
|
);
|
|
@@ -32252,11 +32565,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
32252
32565
|
"/projects/:name/discover/sessions/:id",
|
|
32253
32566
|
async (request, reply) => {
|
|
32254
32567
|
const project = resolveProject(app.db, request.params.name);
|
|
32255
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32568
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32256
32569
|
if (!session || session.projectId !== project.id) {
|
|
32257
32570
|
throw notFound("Discovery session", request.params.id);
|
|
32258
32571
|
}
|
|
32259
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32572
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32260
32573
|
const detail = {
|
|
32261
32574
|
...serializeSession(session),
|
|
32262
32575
|
probes: probeRows.map(serializeProbe)
|
|
@@ -32268,12 +32581,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
32268
32581
|
"/projects/:name/discover/sessions/:id/promote",
|
|
32269
32582
|
async (request, reply) => {
|
|
32270
32583
|
const project = resolveProject(app.db, request.params.name);
|
|
32271
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32584
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32272
32585
|
if (!session || session.projectId !== project.id) {
|
|
32273
32586
|
throw notFound("Discovery session", request.params.id);
|
|
32274
32587
|
}
|
|
32275
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32276
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32588
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32589
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
32277
32590
|
const seenCompetitors = new Set(existingCompetitors);
|
|
32278
32591
|
const cited = /* @__PURE__ */ new Set();
|
|
32279
32592
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -32302,7 +32615,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32302
32615
|
);
|
|
32303
32616
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
32304
32617
|
const project = resolveProject(app.db, request.params.name);
|
|
32305
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32618
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32306
32619
|
if (!session || session.projectId !== project.id) {
|
|
32307
32620
|
throw notFound("Discovery session", request.params.id);
|
|
32308
32621
|
}
|
|
@@ -32325,7 +32638,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32325
32638
|
const bucketSet = new Set(buckets);
|
|
32326
32639
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
32327
32640
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
32328
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32641
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32329
32642
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
32330
32643
|
for (const probe of probeRows) {
|
|
32331
32644
|
if (!probe.bucket) continue;
|
|
@@ -32333,7 +32646,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32333
32646
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
32334
32647
|
}
|
|
32335
32648
|
const existingQueries = new Set(
|
|
32336
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
32649
|
+
app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
32337
32650
|
);
|
|
32338
32651
|
const promotedQueries = [];
|
|
32339
32652
|
const skippedQueries = [];
|
|
@@ -32349,7 +32662,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32349
32662
|
const skippedCompetitors = [];
|
|
32350
32663
|
if (includeCompetitors) {
|
|
32351
32664
|
const existingCompetitors = new Set(
|
|
32352
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32665
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
32353
32666
|
);
|
|
32354
32667
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
32355
32668
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -32453,7 +32766,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
32453
32766
|
|
|
32454
32767
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
32455
32768
|
import crypto27 from "crypto";
|
|
32456
|
-
import { eq as
|
|
32769
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
32457
32770
|
var DEFAULT_MAX_PROBES = 100;
|
|
32458
32771
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
32459
32772
|
function classifyProbeBucket(input) {
|
|
@@ -32507,7 +32820,7 @@ async function executeDiscovery(opts) {
|
|
|
32507
32820
|
status: DiscoverySessionStatuses.seeding,
|
|
32508
32821
|
dedupThreshold,
|
|
32509
32822
|
startedAt
|
|
32510
|
-
}).where(
|
|
32823
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32511
32824
|
const seedResult = await opts.deps.seed({
|
|
32512
32825
|
project: opts.project,
|
|
32513
32826
|
icpDescription: opts.icpDescription,
|
|
@@ -32533,7 +32846,7 @@ async function executeDiscovery(opts) {
|
|
|
32533
32846
|
seedCountRaw,
|
|
32534
32847
|
seedCount,
|
|
32535
32848
|
warning
|
|
32536
|
-
}).where(
|
|
32849
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32537
32850
|
const probeRows = [];
|
|
32538
32851
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
32539
32852
|
for (const query of probedCanonicals) {
|
|
@@ -32573,7 +32886,7 @@ async function executeDiscovery(opts) {
|
|
|
32573
32886
|
wastedCount: buckets["wasted-surface"],
|
|
32574
32887
|
competitorMap,
|
|
32575
32888
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32576
|
-
}).where(
|
|
32889
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32577
32890
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
32578
32891
|
return {
|
|
32579
32892
|
buckets,
|
|
@@ -32613,7 +32926,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
32613
32926
|
status: DiscoverySessionStatuses.failed,
|
|
32614
32927
|
error,
|
|
32615
32928
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32616
|
-
}).where(
|
|
32929
|
+
}).where(eq34(discoverySessions.id, sessionId)).run();
|
|
32617
32930
|
}
|
|
32618
32931
|
function dedupeStrings(input) {
|
|
32619
32932
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -32631,7 +32944,7 @@ function dedupeStrings(input) {
|
|
|
32631
32944
|
|
|
32632
32945
|
// ../api-routes/src/technical-aeo.ts
|
|
32633
32946
|
import crypto28 from "crypto";
|
|
32634
|
-
import { and as
|
|
32947
|
+
import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
|
|
32635
32948
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
32636
32949
|
function emptyScore(projectName) {
|
|
32637
32950
|
return {
|
|
@@ -32663,9 +32976,9 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
32663
32976
|
async function technicalAeoRoutes(app, opts) {
|
|
32664
32977
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
32665
32978
|
const project = resolveProject(app.db, request.params.name);
|
|
32666
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
32667
|
-
|
|
32668
|
-
|
|
32979
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
32980
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
32981
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32669
32982
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32670
32983
|
notProbeRun()
|
|
32671
32984
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
@@ -32698,9 +33011,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32698
33011
|
});
|
|
32699
33012
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
32700
33013
|
const project = resolveProject(app.db, request.params.name);
|
|
32701
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
32702
|
-
|
|
32703
|
-
|
|
33014
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33015
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33016
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32704
33017
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32705
33018
|
notProbeRun()
|
|
32706
33019
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
@@ -32708,9 +33021,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32708
33021
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
32709
33022
|
}
|
|
32710
33023
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
32711
|
-
const conds = [
|
|
32712
|
-
if (statusFilter) conds.push(
|
|
32713
|
-
const where =
|
|
33024
|
+
const conds = [eq35(siteAuditPages.runId, latest.runId)];
|
|
33025
|
+
if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
|
|
33026
|
+
const where = and25(...conds);
|
|
32714
33027
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
32715
33028
|
const total = totalRow?.value ?? 0;
|
|
32716
33029
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
@@ -32734,9 +33047,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32734
33047
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
32735
33048
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
32736
33049
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
32737
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
32738
|
-
|
|
32739
|
-
|
|
33050
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33051
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33052
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32740
33053
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32741
33054
|
notProbeRun()
|
|
32742
33055
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -32748,9 +33061,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32748
33061
|
if (!parsed.success) {
|
|
32749
33062
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
32750
33063
|
}
|
|
32751
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
32752
|
-
|
|
32753
|
-
|
|
33064
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and25(
|
|
33065
|
+
eq35(runs.projectId, project.id),
|
|
33066
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32754
33067
|
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
32755
33068
|
)).get();
|
|
32756
33069
|
if (existing) {
|
|
@@ -32937,6 +33250,8 @@ async function apiRoutes(app, opts) {
|
|
|
32937
33250
|
onInstallBacklinks: opts.onInstallBacklinks,
|
|
32938
33251
|
onReleaseSyncRequested: opts.onReleaseSyncRequested,
|
|
32939
33252
|
onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
|
|
33253
|
+
onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
|
|
33254
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
32940
33255
|
onBacklinksPruneCache: opts.onBacklinksPruneCache,
|
|
32941
33256
|
listCachedReleases: opts.listCachedReleases,
|
|
32942
33257
|
discoverLatestRelease: opts.discoverLatestRelease
|
|
@@ -33335,9 +33650,9 @@ var IntelligenceService = class {
|
|
|
33335
33650
|
*/
|
|
33336
33651
|
analyzeAndPersist(runId, projectId) {
|
|
33337
33652
|
const recentRuns = this.db.select().from(runs).where(
|
|
33338
|
-
|
|
33339
|
-
|
|
33340
|
-
or5(
|
|
33653
|
+
and26(
|
|
33654
|
+
eq36(runs.projectId, projectId),
|
|
33655
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33341
33656
|
// Defensive: RunCoordinator already skips probes before this is
|
|
33342
33657
|
// called, but if a future call site invokes analyzeAndPersist
|
|
33343
33658
|
// directly for a probe, probes still must not pollute the
|
|
@@ -33419,7 +33734,7 @@ var IntelligenceService = class {
|
|
|
33419
33734
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
33420
33735
|
*/
|
|
33421
33736
|
analyzeAndPersistGbp(runId, projectId) {
|
|
33422
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
33737
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
|
|
33423
33738
|
if (!runRow) {
|
|
33424
33739
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
33425
33740
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -33427,9 +33742,9 @@ var IntelligenceService = class {
|
|
|
33427
33742
|
}
|
|
33428
33743
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
33429
33744
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
33430
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
33431
|
-
|
|
33432
|
-
|
|
33745
|
+
const selected = this.db.select().from(gbpLocations).where(and26(
|
|
33746
|
+
eq36(gbpLocations.projectId, projectId),
|
|
33747
|
+
eq36(gbpLocations.selected, true),
|
|
33433
33748
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
33434
33749
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
33435
33750
|
)).all();
|
|
@@ -33464,10 +33779,10 @@ var IntelligenceService = class {
|
|
|
33464
33779
|
}
|
|
33465
33780
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
33466
33781
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
33467
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
33468
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
33469
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
33470
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
33782
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and26(eq36(gbpDailyMetrics.projectId, projectId), eq36(gbpDailyMetrics.locationName, locationName))).all();
|
|
33783
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and26(eq36(gbpPlaceActions.projectId, projectId), eq36(gbpPlaceActions.locationName, locationName))).all();
|
|
33784
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and26(eq36(gbpLodgingSnapshots.projectId, projectId), eq36(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
33785
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and26(eq36(gbpPlaceDetails.projectId, projectId), eq36(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
33471
33786
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
33472
33787
|
const summary = buildGbpSummary({
|
|
33473
33788
|
locationName,
|
|
@@ -33499,7 +33814,7 @@ var IntelligenceService = class {
|
|
|
33499
33814
|
/** Build the month-over-month keyword series for a location from the
|
|
33500
33815
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
33501
33816
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
33502
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
33817
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and26(eq36(gbpKeywordMonthly.projectId, projectId), eq36(gbpKeywordMonthly.locationName, locationName))).all();
|
|
33503
33818
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
33504
33819
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
33505
33820
|
const recentMonth = months[0] ?? null;
|
|
@@ -33530,7 +33845,7 @@ var IntelligenceService = class {
|
|
|
33530
33845
|
*/
|
|
33531
33846
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
33532
33847
|
const covered = new Set(coveredLocationNames);
|
|
33533
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
33848
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and26(eq36(insights.projectId, projectId), eq36(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
33534
33849
|
const staleIds = [];
|
|
33535
33850
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
33536
33851
|
for (const row of existing) {
|
|
@@ -33541,7 +33856,7 @@ var IntelligenceService = class {
|
|
|
33541
33856
|
}
|
|
33542
33857
|
this.db.transaction((tx) => {
|
|
33543
33858
|
for (const id of staleIds) {
|
|
33544
|
-
tx.delete(insights).where(
|
|
33859
|
+
tx.delete(insights).where(eq36(insights.id, id)).run();
|
|
33545
33860
|
}
|
|
33546
33861
|
for (const insight of gbpInsights) {
|
|
33547
33862
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -33619,7 +33934,7 @@ var IntelligenceService = class {
|
|
|
33619
33934
|
* create per run + aggregate). DB is left untouched.
|
|
33620
33935
|
*/
|
|
33621
33936
|
backfill(projectName, opts, onProgress) {
|
|
33622
|
-
const project = this.db.select().from(projects).where(
|
|
33937
|
+
const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
|
|
33623
33938
|
if (!project) {
|
|
33624
33939
|
throw new Error(`Project "${projectName}" not found`);
|
|
33625
33940
|
}
|
|
@@ -33632,9 +33947,9 @@ var IntelligenceService = class {
|
|
|
33632
33947
|
sinceTimestamp = parsed;
|
|
33633
33948
|
}
|
|
33634
33949
|
const allRuns = this.db.select().from(runs).where(
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
or5(
|
|
33950
|
+
and26(
|
|
33951
|
+
eq36(runs.projectId, project.id),
|
|
33952
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33638
33953
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
33639
33954
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33640
33955
|
)
|
|
@@ -33713,7 +34028,7 @@ var IntelligenceService = class {
|
|
|
33713
34028
|
return { processed, skipped, totalInsights };
|
|
33714
34029
|
}
|
|
33715
34030
|
loadTrackedCompetitors(projectId) {
|
|
33716
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34031
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
33717
34032
|
}
|
|
33718
34033
|
/**
|
|
33719
34034
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -33734,15 +34049,15 @@ var IntelligenceService = class {
|
|
|
33734
34049
|
}
|
|
33735
34050
|
persistResult(result, runId, projectId) {
|
|
33736
34051
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
33737
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
34052
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq36(insights.runId, runId)).all();
|
|
33738
34053
|
for (const row of existingInsights) {
|
|
33739
34054
|
if (row.dismissed) {
|
|
33740
34055
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
33741
34056
|
}
|
|
33742
34057
|
}
|
|
33743
34058
|
this.db.transaction((tx) => {
|
|
33744
|
-
tx.delete(insights).where(
|
|
33745
|
-
tx.delete(healthSnapshots).where(
|
|
34059
|
+
tx.delete(insights).where(eq36(insights.runId, runId)).run();
|
|
34060
|
+
tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
|
|
33746
34061
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33747
34062
|
for (const insight of result.insights) {
|
|
33748
34063
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -33793,24 +34108,24 @@ var IntelligenceService = class {
|
|
|
33793
34108
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
33794
34109
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
33795
34110
|
if (regressions.length === 0) return rawInsights;
|
|
33796
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
34111
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
|
|
33797
34112
|
const gscConnected = gscRows.length > 0;
|
|
33798
34113
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
33799
34114
|
for (const row of gscRows) {
|
|
33800
34115
|
const key = row.query.toLowerCase();
|
|
33801
34116
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
33802
34117
|
}
|
|
33803
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
34118
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33804
34119
|
const locationCount = Math.max(
|
|
33805
34120
|
1,
|
|
33806
34121
|
(projectRow?.locations ?? []).length
|
|
33807
34122
|
);
|
|
33808
34123
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
33809
34124
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
33810
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
33813
|
-
or5(
|
|
34125
|
+
and26(
|
|
34126
|
+
eq36(runs.projectId, projectId),
|
|
34127
|
+
eq36(runs.kind, RunKinds["answer-visibility"]),
|
|
34128
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33814
34129
|
// Defensive — see top of file.
|
|
33815
34130
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33816
34131
|
)
|
|
@@ -33830,7 +34145,7 @@ var IntelligenceService = class {
|
|
|
33830
34145
|
const haveHistory = recentRunIds.length > 0;
|
|
33831
34146
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
33832
34147
|
if (haveHistory) {
|
|
33833
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
34148
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and26(eq36(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
|
|
33834
34149
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
33835
34150
|
for (const row of priorRows) {
|
|
33836
34151
|
if (!row.runId) continue;
|
|
@@ -33859,7 +34174,7 @@ var IntelligenceService = class {
|
|
|
33859
34174
|
});
|
|
33860
34175
|
}
|
|
33861
34176
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
33862
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
34177
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33863
34178
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
33864
34179
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
33865
34180
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -33875,7 +34190,7 @@ var IntelligenceService = class {
|
|
|
33875
34190
|
citedDomains: querySnapshots.citedDomains,
|
|
33876
34191
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
33877
34192
|
snapshotLocation: querySnapshots.location
|
|
33878
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
34193
|
+
}).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
|
|
33879
34194
|
const snapshots = [];
|
|
33880
34195
|
let orphanCount = 0;
|
|
33881
34196
|
for (const r of rows) {
|
|
@@ -33987,6 +34302,8 @@ export {
|
|
|
33987
34302
|
hashLodging,
|
|
33988
34303
|
getUrlInfo,
|
|
33989
34304
|
getCrawlIssues,
|
|
34305
|
+
getLinkCounts,
|
|
34306
|
+
getUrlLinks,
|
|
33990
34307
|
PLUGIN_DIR,
|
|
33991
34308
|
DUCKDB_SPEC,
|
|
33992
34309
|
CC_CACHE_DIR,
|