@ainyc/canonry 4.80.0 → 4.82.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 +24 -12
- package/assets/assets/{BacklinksPage-dRc62jAY.js → BacklinksPage-CHclt-pq.js} +1 -1
- package/assets/assets/{ChartPrimitives-D2_IvTkk.js → ChartPrimitives-2Ub4vNWe.js} +1 -1
- package/assets/assets/ProjectPage-UPmHfuxR.js +6 -0
- package/assets/assets/{RunRow-C0MA3yuQ.js → RunRow-rUL1UeA3.js} +1 -1
- package/assets/assets/{RunsPage-4uxTYgGy.js → RunsPage-BQpHfUJf.js} +1 -1
- package/assets/assets/{SettingsPage-3-SLhcJ7.js → SettingsPage-DjTJlr_1.js} +1 -1
- package/assets/assets/{TrafficPage-DZ50qwme.js → TrafficPage-D7rv3BrH.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-BysyuH2H.js +1 -0
- package/assets/assets/{arrow-left-BaZIkAXX.js → arrow-left-CR_FGlkE.js} +1 -1
- package/assets/assets/{extract-error-message-cpvfuFqW.js → extract-error-message-BKkAbWNp.js} +1 -1
- package/assets/assets/{index-EnY_OBRd.js → index-DzzTt20n.js} +87 -87
- package/assets/assets/{trash-2-JpcztiS5.js → trash-2-uSttujvh.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-CXIGHPBE.js → chunk-IEUTAQUF.js} +471 -124
- package/dist/{chunk-2QBSRHSN.js → chunk-JLAD6CYH.js} +88 -8
- package/dist/{chunk-AVN6Q6LM.js → chunk-KPSFRSS7.js} +96 -3
- package/dist/{chunk-LCABGFYN.js → chunk-NSZ3D3MM.js} +404 -242
- package/dist/cli.js +145 -18
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-ZWW3I3NL.js → intelligence-service-2UUJ3YGI.js} +2 -2
- package/dist/mcp.js +23 -4
- 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,
|
|
@@ -153,6 +156,7 @@ import {
|
|
|
153
156
|
hasLocationLabel,
|
|
154
157
|
indexingRequestResponseDtoSchema,
|
|
155
158
|
internalError,
|
|
159
|
+
isReadOnlyKey,
|
|
156
160
|
keywordDtoSchema,
|
|
157
161
|
keywordGenerateRequestSchema,
|
|
158
162
|
latestProjectRunDtoSchema,
|
|
@@ -242,10 +246,10 @@ import {
|
|
|
242
246
|
wordpressSchemaDeployResultDtoSchema,
|
|
243
247
|
wordpressSchemaStatusResultDtoSchema,
|
|
244
248
|
wordpressStatusDtoSchema
|
|
245
|
-
} from "./chunk-
|
|
249
|
+
} from "./chunk-KPSFRSS7.js";
|
|
246
250
|
|
|
247
251
|
// src/intelligence-service.ts
|
|
248
|
-
import { eq as
|
|
252
|
+
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
253
|
|
|
250
254
|
// ../db/src/client.ts
|
|
251
255
|
import { mkdirSync } from "fs";
|
|
@@ -850,7 +854,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
|
|
|
850
854
|
var backlinkDomains = sqliteTable("backlink_domains", {
|
|
851
855
|
id: text("id").primaryKey(),
|
|
852
856
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
853
|
-
|
|
857
|
+
// Nullable: Bing Webmaster backlink rows have no Common Crawl release sync.
|
|
858
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
859
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
854
860
|
release: text("release").notNull(),
|
|
855
861
|
targetDomain: text("target_domain").notNull(),
|
|
856
862
|
linkingDomain: text("linking_domain").notNull(),
|
|
@@ -861,12 +867,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
|
|
|
861
867
|
index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
|
|
862
868
|
index("idx_backlink_domains_project_release").on(table.projectId, table.release),
|
|
863
869
|
index("idx_backlink_domains_hosts").on(table.numHosts),
|
|
864
|
-
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.release, table.linkingDomain)
|
|
870
|
+
uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.source, table.release, table.linkingDomain)
|
|
865
871
|
]);
|
|
866
872
|
var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
867
873
|
id: text("id").primaryKey(),
|
|
868
874
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
869
|
-
|
|
875
|
+
// Nullable: Bing Webmaster summaries have no Common Crawl release sync.
|
|
876
|
+
releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
|
|
877
|
+
source: text("source").$type().notNull().default("commoncrawl"),
|
|
870
878
|
release: text("release").notNull(),
|
|
871
879
|
targetDomain: text("target_domain").notNull(),
|
|
872
880
|
totalLinkingDomains: integer("total_linking_domains").notNull(),
|
|
@@ -875,7 +883,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
|
|
|
875
883
|
queriedAt: text("queried_at").notNull(),
|
|
876
884
|
createdAt: text("created_at").notNull()
|
|
877
885
|
}, (table) => [
|
|
878
|
-
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.release),
|
|
886
|
+
uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.source, table.release),
|
|
879
887
|
index("idx_backlink_summaries_project").on(table.projectId)
|
|
880
888
|
]);
|
|
881
889
|
var agentMemory = sqliteTable("agent_memory", {
|
|
@@ -3051,8 +3059,86 @@ var MIGRATION_VERSIONS = [
|
|
|
3051
3059
|
`CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
|
|
3052
3060
|
`CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
|
|
3053
3061
|
]
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
// Bing Webmaster inbound links land in the SAME backlink store as Common
|
|
3065
|
+
// Crawl, tagged by a `source` discriminator (commoncrawl | bing-webmaster).
|
|
3066
|
+
// Bing rows have no `cc_release_syncs` row, so `release_sync_id` becomes
|
|
3067
|
+
// nullable and the per-window UNIQUE gains `source`. SQLite can't drop a
|
|
3068
|
+
// NOT NULL or rewrite a UNIQUE in place — canonical table rebuild (the
|
|
3069
|
+
// v58/v60 pattern). Guarded on the `source` column's absence so a replay
|
|
3070
|
+
// over the already-migrated schema is a no-op (the hardcoded
|
|
3071
|
+
// `source='commoncrawl'` backfill must never clobber real bing rows).
|
|
3072
|
+
version: 78,
|
|
3073
|
+
name: "backlinks-source-discriminator",
|
|
3074
|
+
statements: [],
|
|
3075
|
+
run: (tx) => {
|
|
3076
|
+
addBacklinkSourceDiscriminator(tx);
|
|
3077
|
+
}
|
|
3054
3078
|
}
|
|
3055
3079
|
];
|
|
3080
|
+
function rebuildBacklinkTableWithSource(tx, table) {
|
|
3081
|
+
if (!tableExists(tx, table)) return;
|
|
3082
|
+
if (columnExists(tx, table, "source")) return;
|
|
3083
|
+
if (table === "backlink_domains") {
|
|
3084
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
|
|
3085
|
+
tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
|
|
3086
|
+
id TEXT PRIMARY KEY,
|
|
3087
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3088
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3089
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3090
|
+
release TEXT NOT NULL,
|
|
3091
|
+
target_domain TEXT NOT NULL,
|
|
3092
|
+
linking_domain TEXT NOT NULL,
|
|
3093
|
+
num_hosts INTEGER NOT NULL,
|
|
3094
|
+
created_at TEXT NOT NULL
|
|
3095
|
+
)`));
|
|
3096
|
+
tx.run(sql.raw(`INSERT INTO backlink_domains_v78
|
|
3097
|
+
(id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
|
|
3098
|
+
SELECT bd.id, bd.project_id,
|
|
3099
|
+
CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
|
|
3100
|
+
'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
|
|
3101
|
+
FROM backlink_domains bd
|
|
3102
|
+
WHERE bd.project_id IN (SELECT id FROM projects)`));
|
|
3103
|
+
tx.run(sql.raw(`DROP TABLE backlink_domains`));
|
|
3104
|
+
tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
|
|
3105
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
|
|
3106
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
|
|
3107
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
|
|
3108
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
|
|
3109
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
|
|
3113
|
+
tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
|
|
3114
|
+
id TEXT PRIMARY KEY,
|
|
3115
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3116
|
+
release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
|
|
3117
|
+
source TEXT NOT NULL DEFAULT 'commoncrawl',
|
|
3118
|
+
release TEXT NOT NULL,
|
|
3119
|
+
target_domain TEXT NOT NULL,
|
|
3120
|
+
total_linking_domains INTEGER NOT NULL,
|
|
3121
|
+
total_hosts INTEGER NOT NULL,
|
|
3122
|
+
top_10_hosts_share TEXT NOT NULL,
|
|
3123
|
+
queried_at TEXT NOT NULL,
|
|
3124
|
+
created_at TEXT NOT NULL
|
|
3125
|
+
)`));
|
|
3126
|
+
tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
|
|
3127
|
+
(id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
|
|
3128
|
+
SELECT bs.id, bs.project_id,
|
|
3129
|
+
CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
|
|
3130
|
+
'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
|
|
3131
|
+
FROM backlink_summaries bs
|
|
3132
|
+
WHERE bs.project_id IN (SELECT id FROM projects)`));
|
|
3133
|
+
tx.run(sql.raw(`DROP TABLE backlink_summaries`));
|
|
3134
|
+
tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
|
|
3135
|
+
tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
|
|
3136
|
+
tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
|
|
3137
|
+
}
|
|
3138
|
+
function addBacklinkSourceDiscriminator(tx) {
|
|
3139
|
+
rebuildBacklinkTableWithSource(tx, "backlink_domains");
|
|
3140
|
+
rebuildBacklinkTableWithSource(tx, "backlink_summaries");
|
|
3141
|
+
}
|
|
3056
3142
|
function isDuplicateColumnError(err) {
|
|
3057
3143
|
if (!(err instanceof Error)) return false;
|
|
3058
3144
|
if (err.message.includes("duplicate column name")) return true;
|
|
@@ -5205,6 +5291,7 @@ import fs8 from "fs";
|
|
|
5205
5291
|
// ../api-routes/src/auth.ts
|
|
5206
5292
|
import crypto2 from "crypto";
|
|
5207
5293
|
import { eq } from "drizzle-orm";
|
|
5294
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
5208
5295
|
function requireScope(request, scope) {
|
|
5209
5296
|
const key = request.apiKey;
|
|
5210
5297
|
if (!key) return;
|
|
@@ -5272,6 +5359,9 @@ async function authPlugin(app, opts = {}) {
|
|
|
5272
5359
|
app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
|
|
5273
5360
|
const scopes = Array.isArray(key.scopes) ? key.scopes : [];
|
|
5274
5361
|
request.apiKey = { id: key.id, name: key.name, scopes };
|
|
5362
|
+
if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
|
|
5363
|
+
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
5364
|
+
}
|
|
5275
5365
|
});
|
|
5276
5366
|
}
|
|
5277
5367
|
|
|
@@ -13206,6 +13296,7 @@ var SCHEMA_TABLE = {
|
|
|
13206
13296
|
AuditLogEntry: auditLogEntrySchema,
|
|
13207
13297
|
BacklinkHistoryEntry: backlinkHistoryEntrySchema,
|
|
13208
13298
|
BacklinkListResponse: backlinkListResponseSchema,
|
|
13299
|
+
BacklinkSourcesResponse: backlinkSourcesResponseSchema,
|
|
13209
13300
|
BacklinkSummaryDto: backlinkSummaryDtoSchema,
|
|
13210
13301
|
BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
|
|
13211
13302
|
BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
|
|
@@ -14345,6 +14436,17 @@ var routeCatalog = [
|
|
|
14345
14436
|
200: jsonResponse("Keys returned.", "ApiKeyListDto")
|
|
14346
14437
|
}
|
|
14347
14438
|
},
|
|
14439
|
+
{
|
|
14440
|
+
method: "get",
|
|
14441
|
+
path: "/api/v1/keys/self",
|
|
14442
|
+
summary: "Introspect the current API key",
|
|
14443
|
+
description: "Returns SAFE metadata for the key that authenticated this request, including the derived `readOnly` flag. Lets a caller (or the MCP adapter at startup) discover whether its configured key is read-only without listing every key on the instance. Ungated read \u2014 a read-only key can call it.",
|
|
14444
|
+
tags: ["keys"],
|
|
14445
|
+
responses: {
|
|
14446
|
+
200: jsonResponse("Current key returned.", "ApiKeyDto"),
|
|
14447
|
+
404: errorResponse("No key on the request (auth skipped).")
|
|
14448
|
+
}
|
|
14449
|
+
},
|
|
14348
14450
|
{
|
|
14349
14451
|
method: "post",
|
|
14350
14452
|
path: "/api/v1/keys",
|
|
@@ -16666,7 +16768,8 @@ var routeCatalog = [
|
|
|
16666
16768
|
tags: ["backlinks"],
|
|
16667
16769
|
parameters: [
|
|
16668
16770
|
nameParameter,
|
|
16669
|
-
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema }
|
|
16771
|
+
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16772
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16670
16773
|
],
|
|
16671
16774
|
responses: {
|
|
16672
16775
|
200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
|
|
@@ -16684,7 +16787,8 @@ var routeCatalog = [
|
|
|
16684
16787
|
nameParameter,
|
|
16685
16788
|
{ name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
|
|
16686
16789
|
{ name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
|
|
16687
|
-
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema }
|
|
16790
|
+
{ name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema },
|
|
16791
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16688
16792
|
],
|
|
16689
16793
|
responses: {
|
|
16690
16794
|
200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
|
|
@@ -16696,12 +16800,44 @@ var routeCatalog = [
|
|
|
16696
16800
|
path: "/api/v1/projects/{name}/backlinks/history",
|
|
16697
16801
|
summary: "Get per-release backlink summaries for a project",
|
|
16698
16802
|
tags: ["backlinks"],
|
|
16699
|
-
parameters: [
|
|
16803
|
+
parameters: [
|
|
16804
|
+
nameParameter,
|
|
16805
|
+
{ name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
|
|
16806
|
+
],
|
|
16700
16807
|
responses: {
|
|
16701
16808
|
200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
|
|
16702
16809
|
404: errorResponse("Project not found.")
|
|
16703
16810
|
}
|
|
16704
16811
|
},
|
|
16812
|
+
{
|
|
16813
|
+
method: "get",
|
|
16814
|
+
path: "/api/v1/projects/{name}/backlinks/sources",
|
|
16815
|
+
summary: "Report per-source backlink availability for a project",
|
|
16816
|
+
description: "Returns connection + data availability for every backlink source (commoncrawl, bing-webmaster) so callers can degrade gracefully across CC-only / Bing-only / both / neither.",
|
|
16817
|
+
tags: ["backlinks"],
|
|
16818
|
+
parameters: [
|
|
16819
|
+
nameParameter,
|
|
16820
|
+
{ name: "excludeCrawlers", in: "query", description: 'When "1"/"true", count linking domains excluding crawler/proxy hosts (matches the dashboard). Default off.', schema: stringSchema }
|
|
16821
|
+
],
|
|
16822
|
+
responses: {
|
|
16823
|
+
200: jsonResponse("Per-source availability returned.", "BacklinkSourcesResponse"),
|
|
16824
|
+
404: errorResponse("Project not found.")
|
|
16825
|
+
}
|
|
16826
|
+
},
|
|
16827
|
+
{
|
|
16828
|
+
method: "post",
|
|
16829
|
+
path: "/api/v1/projects/{name}/backlinks/bing-sync",
|
|
16830
|
+
summary: "Sync a project's inbound links from Bing Webmaster Tools",
|
|
16831
|
+
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.',
|
|
16832
|
+
tags: ["backlinks"],
|
|
16833
|
+
parameters: [nameParameter],
|
|
16834
|
+
responses: {
|
|
16835
|
+
201: jsonResponse("Bing sync run queued.", "RunDto"),
|
|
16836
|
+
400: errorResponse("No Bing Webmaster connection for this project."),
|
|
16837
|
+
404: errorResponse("Project not found."),
|
|
16838
|
+
422: errorResponse("Bing backlinks sync is not available on this deployment.")
|
|
16839
|
+
}
|
|
16840
|
+
},
|
|
16705
16841
|
{
|
|
16706
16842
|
method: "post",
|
|
16707
16843
|
path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
|
|
@@ -17466,11 +17602,13 @@ import crypto12 from "crypto";
|
|
|
17466
17602
|
import { desc as desc9, eq as eq17 } from "drizzle-orm";
|
|
17467
17603
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
17468
17604
|
function toApiKeyDto(row) {
|
|
17605
|
+
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
17469
17606
|
return {
|
|
17470
17607
|
id: row.id,
|
|
17471
17608
|
name: row.name,
|
|
17472
17609
|
keyPrefix: row.keyPrefix,
|
|
17473
|
-
scopes
|
|
17610
|
+
scopes,
|
|
17611
|
+
readOnly: isReadOnlyKey(scopes),
|
|
17474
17612
|
createdAt: row.createdAt,
|
|
17475
17613
|
lastUsedAt: row.lastUsedAt ?? null,
|
|
17476
17614
|
revokedAt: row.revokedAt ?? null
|
|
@@ -17481,6 +17619,17 @@ async function keysRoutes(app) {
|
|
|
17481
17619
|
const rows = app.db.select().from(apiKeys).orderBy(desc9(apiKeys.createdAt)).all();
|
|
17482
17620
|
return { keys: rows.map(toApiKeyDto) };
|
|
17483
17621
|
});
|
|
17622
|
+
app.get("/keys/self", async (request) => {
|
|
17623
|
+
const id = request.apiKey?.id;
|
|
17624
|
+
if (!id) {
|
|
17625
|
+
throw notFound("API key", "self");
|
|
17626
|
+
}
|
|
17627
|
+
const row = app.db.select().from(apiKeys).where(eq17(apiKeys.id, id)).get();
|
|
17628
|
+
if (!row) {
|
|
17629
|
+
throw notFound("API key", id);
|
|
17630
|
+
}
|
|
17631
|
+
return toApiKeyDto(row);
|
|
17632
|
+
});
|
|
17484
17633
|
app.post("/keys", async (request) => {
|
|
17485
17634
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
17486
17635
|
const parsed = createApiKeyRequestSchema.safeParse(request.body);
|
|
@@ -17516,6 +17665,7 @@ async function keysRoutes(app) {
|
|
|
17516
17665
|
name,
|
|
17517
17666
|
keyPrefix,
|
|
17518
17667
|
scopes: effectiveScopes,
|
|
17668
|
+
readOnly: isReadOnlyKey(effectiveScopes),
|
|
17519
17669
|
createdAt: now,
|
|
17520
17670
|
lastUsedAt: null,
|
|
17521
17671
|
revokedAt: null,
|
|
@@ -20901,6 +21051,7 @@ var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
|
20901
21051
|
var BING_SUBMIT_URL_BATCH_LIMIT = 500;
|
|
20902
21052
|
var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
|
|
20903
21053
|
var BING_REQUEST_TIMEOUT_MS = 3e4;
|
|
21054
|
+
var BING_LINKS_MAX_PAGES = 20;
|
|
20904
21055
|
|
|
20905
21056
|
// ../integration-bing/src/types.ts
|
|
20906
21057
|
var BingApiError = class extends Error {
|
|
@@ -21064,6 +21215,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
|
|
|
21064
21215
|
const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
|
|
21065
21216
|
return data ?? [];
|
|
21066
21217
|
}
|
|
21218
|
+
async function getLinkCounts(apiKey, siteUrl, opts = {}) {
|
|
21219
|
+
validateApiKey(apiKey);
|
|
21220
|
+
validateSiteUrl2(siteUrl);
|
|
21221
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21222
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21223
|
+
const out = [];
|
|
21224
|
+
let page = 0;
|
|
21225
|
+
let totalPages = 1;
|
|
21226
|
+
while (page < totalPages && page < maxPages) {
|
|
21227
|
+
const data = await bingFetch(apiKey, `GetLinkCounts?siteUrl=${encodedSite}&page=${page}`);
|
|
21228
|
+
for (const link of data?.Links ?? []) {
|
|
21229
|
+
if (link && typeof link.Url === "string") {
|
|
21230
|
+
out.push({ Url: link.Url, Count: Number(link.Count ?? 0) });
|
|
21231
|
+
}
|
|
21232
|
+
}
|
|
21233
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21234
|
+
page++;
|
|
21235
|
+
}
|
|
21236
|
+
return out;
|
|
21237
|
+
}
|
|
21238
|
+
async function getUrlLinks(apiKey, siteUrl, link, opts = {}) {
|
|
21239
|
+
validateApiKey(apiKey);
|
|
21240
|
+
validateSiteUrl2(siteUrl);
|
|
21241
|
+
validateUrl2(link);
|
|
21242
|
+
const encodedSite = encodeURIComponent(siteUrl);
|
|
21243
|
+
const encodedLink = encodeURIComponent(link);
|
|
21244
|
+
const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
|
|
21245
|
+
const out = [];
|
|
21246
|
+
let page = 0;
|
|
21247
|
+
let totalPages = 1;
|
|
21248
|
+
while (page < totalPages && page < maxPages) {
|
|
21249
|
+
const data = await bingFetch(
|
|
21250
|
+
apiKey,
|
|
21251
|
+
`GetUrlLinks?siteUrl=${encodedSite}&link=${encodedLink}&page=${page}`
|
|
21252
|
+
);
|
|
21253
|
+
for (const detail of data?.Details ?? []) {
|
|
21254
|
+
if (detail && typeof detail.Url === "string") {
|
|
21255
|
+
out.push({ Url: detail.Url, AnchorText: detail.AnchorText });
|
|
21256
|
+
}
|
|
21257
|
+
}
|
|
21258
|
+
totalPages = Number(data?.TotalPages ?? 1) || 1;
|
|
21259
|
+
page++;
|
|
21260
|
+
}
|
|
21261
|
+
return out;
|
|
21262
|
+
}
|
|
21067
21263
|
|
|
21068
21264
|
// ../api-routes/src/bing.ts
|
|
21069
21265
|
function parseBingDate(value) {
|
|
@@ -24662,9 +24858,18 @@ function mapSummaryRow(row) {
|
|
|
24662
24858
|
totalLinkingDomains: row.totalLinkingDomains,
|
|
24663
24859
|
totalHosts: row.totalHosts,
|
|
24664
24860
|
top10HostsShare: row.top10HostsShare,
|
|
24665
|
-
queriedAt: row.queriedAt
|
|
24861
|
+
queriedAt: row.queriedAt,
|
|
24862
|
+
source: row.source
|
|
24666
24863
|
};
|
|
24667
24864
|
}
|
|
24865
|
+
function parseSourceParam(value) {
|
|
24866
|
+
if (value === void 0 || value === "") return BacklinkSources.commoncrawl;
|
|
24867
|
+
const parsed = backlinkSourceSchema.safeParse(value);
|
|
24868
|
+
if (!parsed.success) {
|
|
24869
|
+
throw validationError(`Invalid source "${value}". Expected one of: ${Object.values(BacklinkSources).join(", ")}.`);
|
|
24870
|
+
}
|
|
24871
|
+
return parsed.data;
|
|
24872
|
+
}
|
|
24668
24873
|
function mapRunRow(row) {
|
|
24669
24874
|
return {
|
|
24670
24875
|
id: row.id,
|
|
@@ -24679,9 +24884,13 @@ function mapRunRow(row) {
|
|
|
24679
24884
|
createdAt: row.createdAt
|
|
24680
24885
|
};
|
|
24681
24886
|
}
|
|
24682
|
-
function latestSummaryForProject(db, projectId, release) {
|
|
24683
|
-
const
|
|
24684
|
-
|
|
24887
|
+
function latestSummaryForProject(db, projectId, source, release) {
|
|
24888
|
+
const conditions = [
|
|
24889
|
+
eq25(backlinkSummaries.projectId, projectId),
|
|
24890
|
+
eq25(backlinkSummaries.source, source)
|
|
24891
|
+
];
|
|
24892
|
+
if (release) conditions.push(eq25(backlinkSummaries.release, release));
|
|
24893
|
+
return db.select().from(backlinkSummaries).where(and19(...conditions)).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24685
24894
|
}
|
|
24686
24895
|
function parseExcludeCrawlers(value) {
|
|
24687
24896
|
if (!value) return false;
|
|
@@ -24691,6 +24900,7 @@ function parseExcludeCrawlers(value) {
|
|
|
24691
24900
|
function computeFilteredSummary(db, base) {
|
|
24692
24901
|
const baseDomainCondition = and19(
|
|
24693
24902
|
eq25(backlinkDomains.projectId, base.projectId),
|
|
24903
|
+
eq25(backlinkDomains.source, base.source),
|
|
24694
24904
|
eq25(backlinkDomains.release, base.release)
|
|
24695
24905
|
);
|
|
24696
24906
|
const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
@@ -24717,10 +24927,48 @@ function computeFilteredSummary(db, base) {
|
|
|
24717
24927
|
totalHosts,
|
|
24718
24928
|
top10HostsShare: top10Share.toFixed(6),
|
|
24719
24929
|
queriedAt: base.queriedAt,
|
|
24930
|
+
source: base.source,
|
|
24720
24931
|
excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
|
|
24721
24932
|
excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
|
|
24722
24933
|
};
|
|
24723
24934
|
}
|
|
24935
|
+
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
24936
|
+
const summary = db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.source, source))).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24937
|
+
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
24938
|
+
if (summary && excludeCrawlers) {
|
|
24939
|
+
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and19(
|
|
24940
|
+
eq25(backlinkDomains.projectId, projectId),
|
|
24941
|
+
eq25(backlinkDomains.source, source),
|
|
24942
|
+
eq25(backlinkDomains.release, summary.release),
|
|
24943
|
+
backlinkCrawlerExclusionClause()
|
|
24944
|
+
)).get();
|
|
24945
|
+
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
24946
|
+
}
|
|
24947
|
+
return {
|
|
24948
|
+
source,
|
|
24949
|
+
connected,
|
|
24950
|
+
hasData: !!summary,
|
|
24951
|
+
latestRelease: summary?.release ?? null,
|
|
24952
|
+
totalLinkingDomains,
|
|
24953
|
+
lastSyncedAt: summary?.queriedAt ?? null
|
|
24954
|
+
};
|
|
24955
|
+
}
|
|
24956
|
+
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
24957
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
24958
|
+
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
24959
|
+
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
24960
|
+
const sources = [
|
|
24961
|
+
buildSourceAvailability(db, project.id, BacklinkSources.commoncrawl, ccConnected, excludeCrawlers),
|
|
24962
|
+
buildSourceAvailability(db, project.id, BacklinkSources["bing-webmaster"], bingConnected, excludeCrawlers)
|
|
24963
|
+
];
|
|
24964
|
+
return {
|
|
24965
|
+
projectId: project.id,
|
|
24966
|
+
targetDomain: project.canonicalDomain,
|
|
24967
|
+
sources,
|
|
24968
|
+
anyConnected: sources.some((s) => s.connected),
|
|
24969
|
+
anyData: sources.some((s) => s.hasData)
|
|
24970
|
+
};
|
|
24971
|
+
}
|
|
24724
24972
|
async function backlinksRoutes(app, opts) {
|
|
24725
24973
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
24726
24974
|
if (!opts.getBacklinksStatus) {
|
|
@@ -24852,7 +25100,8 @@ async function backlinksRoutes(app, opts) {
|
|
|
24852
25100
|
"/projects/:name/backlinks/summary",
|
|
24853
25101
|
async (request, reply) => {
|
|
24854
25102
|
const project = resolveProject(app.db, request.params.name);
|
|
24855
|
-
const
|
|
25103
|
+
const source = parseSourceParam(request.query.source);
|
|
25104
|
+
const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24856
25105
|
if (!row) return reply.send(null);
|
|
24857
25106
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24858
25107
|
return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
|
|
@@ -24860,10 +25109,11 @@ async function backlinksRoutes(app, opts) {
|
|
|
24860
25109
|
);
|
|
24861
25110
|
app.get("/projects/:name/backlinks/domains", async (request, reply) => {
|
|
24862
25111
|
const project = resolveProject(app.db, request.params.name);
|
|
24863
|
-
const
|
|
25112
|
+
const source = parseSourceParam(request.query.source);
|
|
25113
|
+
const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
|
|
24864
25114
|
const targetRelease = request.query.release ?? summaryRow?.release;
|
|
24865
25115
|
if (!targetRelease) {
|
|
24866
|
-
const response2 = { summary: null, total: 0, rows: [] };
|
|
25116
|
+
const response2 = { source, summary: null, total: 0, rows: [] };
|
|
24867
25117
|
return reply.send(response2);
|
|
24868
25118
|
}
|
|
24869
25119
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
@@ -24871,19 +25121,22 @@ async function backlinksRoutes(app, opts) {
|
|
|
24871
25121
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
24872
25122
|
const baseDomainCondition = and19(
|
|
24873
25123
|
eq25(backlinkDomains.projectId, project.id),
|
|
25124
|
+
eq25(backlinkDomains.source, source),
|
|
24874
25125
|
eq25(backlinkDomains.release, targetRelease)
|
|
24875
25126
|
);
|
|
24876
25127
|
const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
24877
25128
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
24878
25129
|
const rows = app.db.select({
|
|
24879
25130
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
24880
|
-
numHosts: backlinkDomains.numHosts
|
|
25131
|
+
numHosts: backlinkDomains.numHosts,
|
|
25132
|
+
source: backlinkDomains.source
|
|
24881
25133
|
}).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
24882
25134
|
let summary = null;
|
|
24883
25135
|
if (summaryRow) {
|
|
24884
25136
|
summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
|
|
24885
25137
|
}
|
|
24886
25138
|
const response = {
|
|
25139
|
+
source,
|
|
24887
25140
|
summary,
|
|
24888
25141
|
total: Number(totalRow?.count ?? 0),
|
|
24889
25142
|
rows
|
|
@@ -24894,17 +25147,58 @@ async function backlinksRoutes(app, opts) {
|
|
|
24894
25147
|
"/projects/:name/backlinks/history",
|
|
24895
25148
|
async (request, reply) => {
|
|
24896
25149
|
const project = resolveProject(app.db, request.params.name);
|
|
24897
|
-
const
|
|
25150
|
+
const source = parseSourceParam(request.query.source);
|
|
25151
|
+
const rows = app.db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, project.id), eq25(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
24898
25152
|
const response = rows.map((r) => ({
|
|
24899
25153
|
release: r.release,
|
|
24900
25154
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
24901
25155
|
totalHosts: r.totalHosts,
|
|
24902
25156
|
top10HostsShare: r.top10HostsShare,
|
|
24903
|
-
queriedAt: r.queriedAt
|
|
25157
|
+
queriedAt: r.queriedAt,
|
|
25158
|
+
source: r.source
|
|
24904
25159
|
}));
|
|
24905
25160
|
return reply.send(response);
|
|
24906
25161
|
}
|
|
24907
25162
|
);
|
|
25163
|
+
app.get(
|
|
25164
|
+
"/projects/:name/backlinks/sources",
|
|
25165
|
+
async (request, reply) => {
|
|
25166
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25167
|
+
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25168
|
+
const response = computeSourceAvailability(app.db, project, opts.bingConnectionStore, excludeCrawlers);
|
|
25169
|
+
return reply.send(response);
|
|
25170
|
+
}
|
|
25171
|
+
);
|
|
25172
|
+
app.post(
|
|
25173
|
+
"/projects/:name/backlinks/bing-sync",
|
|
25174
|
+
async (request, reply) => {
|
|
25175
|
+
const project = resolveProject(app.db, request.params.name);
|
|
25176
|
+
if (!opts.onBingBacklinkSyncRequested) {
|
|
25177
|
+
throw missingDependency(
|
|
25178
|
+
"Bing backlinks sync is only available from a local canonry install with Bing Webmaster connected."
|
|
25179
|
+
);
|
|
25180
|
+
}
|
|
25181
|
+
const conn = opts.bingConnectionStore?.getConnection(project.canonicalDomain);
|
|
25182
|
+
if (!conn) {
|
|
25183
|
+
throw validationError(
|
|
25184
|
+
`No Bing Webmaster connection for "${project.name}". Run \`canonry bing connect ${project.name} --api-key <key>\` first.`
|
|
25185
|
+
);
|
|
25186
|
+
}
|
|
25187
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25188
|
+
const runId = crypto22.randomUUID();
|
|
25189
|
+
app.db.insert(runs).values({
|
|
25190
|
+
id: runId,
|
|
25191
|
+
projectId: project.id,
|
|
25192
|
+
kind: RunKinds["backlink-extract"],
|
|
25193
|
+
status: RunStatuses.queued,
|
|
25194
|
+
trigger: RunTriggers.manual,
|
|
25195
|
+
createdAt: now
|
|
25196
|
+
}).run();
|
|
25197
|
+
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25198
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
25199
|
+
return reply.status(201).send(mapRunRow(run));
|
|
25200
|
+
}
|
|
25201
|
+
);
|
|
24908
25202
|
}
|
|
24909
25203
|
|
|
24910
25204
|
// ../api-routes/src/traffic.ts
|
|
@@ -30300,6 +30594,54 @@ function readInstalledManifest(skillDir) {
|
|
|
30300
30594
|
}
|
|
30301
30595
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
30302
30596
|
|
|
30597
|
+
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
30598
|
+
import { and as and21, eq as eq27 } from "drizzle-orm";
|
|
30599
|
+
function skippedNoProject() {
|
|
30600
|
+
return {
|
|
30601
|
+
status: CheckStatuses.skipped,
|
|
30602
|
+
code: "backlinks.source.no-project",
|
|
30603
|
+
summary: "Project context required."
|
|
30604
|
+
};
|
|
30605
|
+
}
|
|
30606
|
+
var BACKLINKS_CHECKS = [
|
|
30607
|
+
{
|
|
30608
|
+
id: "backlinks.source.connected",
|
|
30609
|
+
category: CheckCategories.integrations,
|
|
30610
|
+
scope: CheckScopes.project,
|
|
30611
|
+
title: "Backlinks source connected",
|
|
30612
|
+
run: (ctx) => {
|
|
30613
|
+
if (!ctx.project) return skippedNoProject();
|
|
30614
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq27(projects.id, ctx.project.id)).get();
|
|
30615
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
30616
|
+
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
30617
|
+
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
30618
|
+
const connected = [];
|
|
30619
|
+
if (ccConnected) connected.push(BacklinkSources.commoncrawl);
|
|
30620
|
+
if (bingConnected) connected.push(BacklinkSources["bing-webmaster"]);
|
|
30621
|
+
if (connected.length === 0) {
|
|
30622
|
+
return {
|
|
30623
|
+
status: CheckStatuses.warn,
|
|
30624
|
+
code: "backlinks.source.none",
|
|
30625
|
+
summary: `No backlink source is set up for ${ctx.project.name}.`,
|
|
30626
|
+
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}\`).`,
|
|
30627
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
30628
|
+
};
|
|
30629
|
+
}
|
|
30630
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and21(
|
|
30631
|
+
eq27(backlinkSummaries.projectId, ctx.project.id),
|
|
30632
|
+
eq27(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
30633
|
+
)).limit(1).get() : false;
|
|
30634
|
+
return {
|
|
30635
|
+
status: CheckStatuses.ok,
|
|
30636
|
+
code: "backlinks.source.connected",
|
|
30637
|
+
summary: `${connected.length} backlink source${connected.length === 1 ? "" : "s"} set up: ${connected.join(", ")}.`,
|
|
30638
|
+
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,
|
|
30639
|
+
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected, connected, commoncrawlHasData: ccHasData }
|
|
30640
|
+
};
|
|
30641
|
+
}
|
|
30642
|
+
}
|
|
30643
|
+
];
|
|
30644
|
+
|
|
30303
30645
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
30304
30646
|
var BING_AUTH_CHECKS = [
|
|
30305
30647
|
{
|
|
@@ -30447,10 +30789,10 @@ var BING_AUTH_CHECKS = [
|
|
|
30447
30789
|
];
|
|
30448
30790
|
|
|
30449
30791
|
// ../api-routes/src/doctor/checks/content.ts
|
|
30450
|
-
import { eq as
|
|
30792
|
+
import { eq as eq28 } from "drizzle-orm";
|
|
30451
30793
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
30452
30794
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
30453
|
-
function
|
|
30795
|
+
function skippedNoProject2() {
|
|
30454
30796
|
return {
|
|
30455
30797
|
status: CheckStatuses.skipped,
|
|
30456
30798
|
code: "content.winnability.no-project",
|
|
@@ -30460,7 +30802,7 @@ function skippedNoProject() {
|
|
|
30460
30802
|
}
|
|
30461
30803
|
function loadProject(ctx) {
|
|
30462
30804
|
if (!ctx.project) return null;
|
|
30463
|
-
return ctx.db.select().from(projects).where(
|
|
30805
|
+
return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
|
|
30464
30806
|
}
|
|
30465
30807
|
function percent(value) {
|
|
30466
30808
|
return Math.round(value * 100);
|
|
@@ -30471,7 +30813,7 @@ var winnabilityCoverageCheck = {
|
|
|
30471
30813
|
scope: CheckScopes.project,
|
|
30472
30814
|
title: "Content winnability classification coverage",
|
|
30473
30815
|
run: (ctx) => {
|
|
30474
|
-
if (!ctx.project) return
|
|
30816
|
+
if (!ctx.project) return skippedNoProject2();
|
|
30475
30817
|
const project = loadProject(ctx);
|
|
30476
30818
|
if (!project) {
|
|
30477
30819
|
return {
|
|
@@ -30552,7 +30894,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30552
30894
|
);
|
|
30553
30895
|
|
|
30554
30896
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
30555
|
-
import { eq as
|
|
30897
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30556
30898
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30557
30899
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30558
30900
|
var adsConnectionCheck = {
|
|
@@ -30569,7 +30911,7 @@ var adsConnectionCheck = {
|
|
|
30569
30911
|
remediation: null
|
|
30570
30912
|
};
|
|
30571
30913
|
}
|
|
30572
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
30914
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30573
30915
|
if (!row) {
|
|
30574
30916
|
return {
|
|
30575
30917
|
status: CheckStatuses.skipped,
|
|
@@ -30619,7 +30961,7 @@ var adsRecentSyncCheck = {
|
|
|
30619
30961
|
remediation: null
|
|
30620
30962
|
};
|
|
30621
30963
|
}
|
|
30622
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
30964
|
+
const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
|
|
30623
30965
|
if (!row) {
|
|
30624
30966
|
return {
|
|
30625
30967
|
status: CheckStatuses.skipped,
|
|
@@ -30810,10 +31152,10 @@ var ga4ConnectionCheck = {
|
|
|
30810
31152
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
30811
31153
|
|
|
30812
31154
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
30813
|
-
import { and as
|
|
31155
|
+
import { and as and22, eq as eq30 } from "drizzle-orm";
|
|
30814
31156
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
30815
31157
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
30816
|
-
function
|
|
31158
|
+
function skippedNoProject3() {
|
|
30817
31159
|
return {
|
|
30818
31160
|
status: CheckStatuses.skipped,
|
|
30819
31161
|
code: "gbp.auth.no-project",
|
|
@@ -30830,7 +31172,7 @@ function storeUnavailable() {
|
|
|
30830
31172
|
};
|
|
30831
31173
|
}
|
|
30832
31174
|
async function resolveGbpToken(ctx) {
|
|
30833
|
-
if (!ctx.project) return { ok: false, output:
|
|
31175
|
+
if (!ctx.project) return { ok: false, output: skippedNoProject3() };
|
|
30834
31176
|
const store = ctx.googleConnectionStore;
|
|
30835
31177
|
if (!store) return { ok: false, output: storeUnavailable() };
|
|
30836
31178
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
@@ -30908,7 +31250,7 @@ var scopesCheck = {
|
|
|
30908
31250
|
scope: CheckScopes.project,
|
|
30909
31251
|
title: "GBP granted scopes",
|
|
30910
31252
|
run: async (ctx) => {
|
|
30911
|
-
if (!ctx.project) return
|
|
31253
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30912
31254
|
const store = ctx.googleConnectionStore;
|
|
30913
31255
|
if (!store) return storeUnavailable();
|
|
30914
31256
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -30945,7 +31287,7 @@ var accountAccessCheck = {
|
|
|
30945
31287
|
scope: CheckScopes.project,
|
|
30946
31288
|
title: "GBP account access",
|
|
30947
31289
|
run: async (ctx) => {
|
|
30948
|
-
if (!ctx.project) return
|
|
31290
|
+
if (!ctx.project) return skippedNoProject3();
|
|
30949
31291
|
const store = ctx.googleConnectionStore;
|
|
30950
31292
|
if (!store) return storeUnavailable();
|
|
30951
31293
|
const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
|
|
@@ -31042,8 +31384,8 @@ var recentSyncCheck = {
|
|
|
31042
31384
|
scope: CheckScopes.project,
|
|
31043
31385
|
title: "GBP recent sync",
|
|
31044
31386
|
run: (ctx) => {
|
|
31045
|
-
if (!ctx.project) return
|
|
31046
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31387
|
+
if (!ctx.project) return skippedNoProject3();
|
|
31388
|
+
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
31389
|
if (selected.length === 0) {
|
|
31048
31390
|
return {
|
|
31049
31391
|
status: CheckStatuses.skipped,
|
|
@@ -31103,7 +31445,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
31103
31445
|
);
|
|
31104
31446
|
|
|
31105
31447
|
// ../api-routes/src/doctor/checks/places.ts
|
|
31106
|
-
import { eq as
|
|
31448
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
31107
31449
|
var apiKeyCheck = {
|
|
31108
31450
|
id: "gbp.places.api-key",
|
|
31109
31451
|
category: CheckCategories.auth,
|
|
@@ -31148,7 +31490,7 @@ var apiKeyCheck = {
|
|
|
31148
31490
|
details: { tier: cfg.tier }
|
|
31149
31491
|
};
|
|
31150
31492
|
}
|
|
31151
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31493
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
|
|
31152
31494
|
const selected = rows.filter((r) => r.selected);
|
|
31153
31495
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
31154
31496
|
const details = {
|
|
@@ -31184,7 +31526,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
|
|
|
31184
31526
|
var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
|
|
31185
31527
|
async function resolveAccessToken(ctx) {
|
|
31186
31528
|
if (!ctx.project) {
|
|
31187
|
-
return { ok: false, output:
|
|
31529
|
+
return { ok: false, output: skippedNoProject4() };
|
|
31188
31530
|
}
|
|
31189
31531
|
const store = ctx.googleConnectionStore;
|
|
31190
31532
|
if (!store) {
|
|
@@ -31251,7 +31593,7 @@ async function resolveAccessToken(ctx) {
|
|
|
31251
31593
|
};
|
|
31252
31594
|
}
|
|
31253
31595
|
}
|
|
31254
|
-
function
|
|
31596
|
+
function skippedNoProject4() {
|
|
31255
31597
|
return {
|
|
31256
31598
|
status: CheckStatuses.skipped,
|
|
31257
31599
|
code: "google.auth.no-project",
|
|
@@ -31281,7 +31623,7 @@ var propertyAccessCheck = {
|
|
|
31281
31623
|
scope: CheckScopes.project,
|
|
31282
31624
|
title: "GSC property access",
|
|
31283
31625
|
run: async (ctx) => {
|
|
31284
|
-
if (!ctx.project) return
|
|
31626
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31285
31627
|
const store = ctx.googleConnectionStore;
|
|
31286
31628
|
if (!store) {
|
|
31287
31629
|
return {
|
|
@@ -31382,7 +31724,7 @@ var redirectUriCheck = {
|
|
|
31382
31724
|
scope: CheckScopes.project,
|
|
31383
31725
|
title: "OAuth redirect URI",
|
|
31384
31726
|
run: async (ctx) => {
|
|
31385
|
-
if (!ctx.project) return
|
|
31727
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31386
31728
|
const auth = ctx.getGoogleAuthConfig?.() ?? {};
|
|
31387
31729
|
if (!auth.clientId || !auth.clientSecret) {
|
|
31388
31730
|
return {
|
|
@@ -31436,7 +31778,7 @@ var scopesCheck2 = {
|
|
|
31436
31778
|
scope: CheckScopes.project,
|
|
31437
31779
|
title: "GSC granted scopes",
|
|
31438
31780
|
run: async (ctx) => {
|
|
31439
|
-
if (!ctx.project) return
|
|
31781
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31440
31782
|
const store = ctx.googleConnectionStore;
|
|
31441
31783
|
if (!store) {
|
|
31442
31784
|
return {
|
|
@@ -31599,10 +31941,10 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
31599
31941
|
];
|
|
31600
31942
|
|
|
31601
31943
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
31602
|
-
import { and as
|
|
31944
|
+
import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
31603
31945
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
31604
31946
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
31605
|
-
function
|
|
31947
|
+
function skippedNoProject5() {
|
|
31606
31948
|
return {
|
|
31607
31949
|
status: CheckStatuses.skipped,
|
|
31608
31950
|
code: "traffic.no-project",
|
|
@@ -31613,8 +31955,8 @@ function skippedNoProject4() {
|
|
|
31613
31955
|
function loadProbes(ctx) {
|
|
31614
31956
|
if (!ctx.project) return [];
|
|
31615
31957
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
31616
|
-
|
|
31617
|
-
|
|
31958
|
+
and23(
|
|
31959
|
+
eq32(trafficSources.projectId, ctx.project.id),
|
|
31618
31960
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
31619
31961
|
)
|
|
31620
31962
|
).all();
|
|
@@ -31636,7 +31978,7 @@ var sourceConnectedCheck = {
|
|
|
31636
31978
|
scope: CheckScopes.project,
|
|
31637
31979
|
title: "Traffic source connected",
|
|
31638
31980
|
run: (ctx) => {
|
|
31639
|
-
if (!ctx.project) return
|
|
31981
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31640
31982
|
const sources = loadProbes(ctx);
|
|
31641
31983
|
if (sources.length === 0) {
|
|
31642
31984
|
return {
|
|
@@ -31680,7 +32022,7 @@ var recentDataCheck = {
|
|
|
31680
32022
|
scope: CheckScopes.project,
|
|
31681
32023
|
title: "Traffic source recent data",
|
|
31682
32024
|
run: (ctx) => {
|
|
31683
|
-
if (!ctx.project) return
|
|
32025
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31684
32026
|
const sources = loadProbes(ctx);
|
|
31685
32027
|
if (sources.length === 0) {
|
|
31686
32028
|
return {
|
|
@@ -31694,16 +32036,16 @@ var recentDataCheck = {
|
|
|
31694
32036
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
31695
32037
|
const recentCrawlers = Number(
|
|
31696
32038
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31697
|
-
|
|
31698
|
-
|
|
32039
|
+
and23(
|
|
32040
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31699
32041
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
31700
32042
|
)
|
|
31701
32043
|
).get()?.total ?? 0
|
|
31702
32044
|
);
|
|
31703
32045
|
const recentReferrals = Number(
|
|
31704
32046
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31705
|
-
|
|
31706
|
-
|
|
32047
|
+
and23(
|
|
32048
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31707
32049
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
31708
32050
|
)
|
|
31709
32051
|
).get()?.total ?? 0
|
|
@@ -31718,16 +32060,16 @@ var recentDataCheck = {
|
|
|
31718
32060
|
}
|
|
31719
32061
|
const olderCrawlers = Number(
|
|
31720
32062
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
31721
|
-
|
|
31722
|
-
|
|
32063
|
+
and23(
|
|
32064
|
+
eq32(crawlerEventsHourly.projectId, ctx.project.id),
|
|
31723
32065
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
31724
32066
|
)
|
|
31725
32067
|
).get()?.total ?? 0
|
|
31726
32068
|
);
|
|
31727
32069
|
const olderReferrals = Number(
|
|
31728
32070
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31729
|
-
|
|
31730
|
-
|
|
32071
|
+
and23(
|
|
32072
|
+
eq32(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
31731
32073
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
31732
32074
|
)
|
|
31733
32075
|
).get()?.total ?? 0
|
|
@@ -31842,7 +32184,7 @@ var credentialsCheck = {
|
|
|
31842
32184
|
scope: CheckScopes.project,
|
|
31843
32185
|
title: "Traffic source credentials",
|
|
31844
32186
|
run: async (ctx) => {
|
|
31845
|
-
if (!ctx.project) return
|
|
32187
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31846
32188
|
const sources = loadProbes(ctx);
|
|
31847
32189
|
if (sources.length === 0) {
|
|
31848
32190
|
return {
|
|
@@ -31871,7 +32213,7 @@ var scopesCheck3 = {
|
|
|
31871
32213
|
scope: CheckScopes.project,
|
|
31872
32214
|
title: "Traffic source scopes",
|
|
31873
32215
|
run: async (ctx) => {
|
|
31874
|
-
if (!ctx.project) return
|
|
32216
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31875
32217
|
const sources = loadProbes(ctx);
|
|
31876
32218
|
if (sources.length === 0) {
|
|
31877
32219
|
return {
|
|
@@ -31900,7 +32242,7 @@ var cacheBlindSpotCheck = {
|
|
|
31900
32242
|
scope: CheckScopes.project,
|
|
31901
32243
|
title: "WordPress traffic cache blind spot",
|
|
31902
32244
|
run: (ctx) => {
|
|
31903
|
-
if (!ctx.project) return
|
|
32245
|
+
if (!ctx.project) return skippedNoProject5();
|
|
31904
32246
|
const wpSources = loadProbes(ctx).filter(
|
|
31905
32247
|
(s) => s.sourceType === TrafficSourceTypes.wordpress
|
|
31906
32248
|
);
|
|
@@ -32015,6 +32357,7 @@ var ALL_CHECKS = [
|
|
|
32015
32357
|
...ADS_CHECKS,
|
|
32016
32358
|
...PROVIDERS_CHECKS,
|
|
32017
32359
|
...TRAFFIC_SOURCE_CHECKS,
|
|
32360
|
+
...BACKLINKS_CHECKS,
|
|
32018
32361
|
...CONTENT_CHECKS,
|
|
32019
32362
|
...AGENT_CHECKS
|
|
32020
32363
|
];
|
|
@@ -32140,7 +32483,7 @@ async function doctorRoutes(app, opts) {
|
|
|
32140
32483
|
|
|
32141
32484
|
// ../api-routes/src/discovery/routes.ts
|
|
32142
32485
|
import crypto26 from "crypto";
|
|
32143
|
-
import { and as
|
|
32486
|
+
import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
|
|
32144
32487
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
32145
32488
|
async function discoveryRoutes(app, opts) {
|
|
32146
32489
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -32172,9 +32515,9 @@ async function discoveryRoutes(app, opts) {
|
|
|
32172
32515
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32173
32516
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
32174
32517
|
const decision = app.db.transaction((tx) => {
|
|
32175
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
32176
|
-
|
|
32177
|
-
|
|
32518
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and24(
|
|
32519
|
+
eq33(discoverySessions.projectId, project.id),
|
|
32520
|
+
eq33(discoverySessions.icpDescription, icpDescription),
|
|
32178
32521
|
inArray11(discoverySessions.status, [
|
|
32179
32522
|
DiscoverySessionStatuses.queued,
|
|
32180
32523
|
DiscoverySessionStatuses.seeding,
|
|
@@ -32244,7 +32587,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32244
32587
|
const project = resolveProject(app.db, request.params.name);
|
|
32245
32588
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
32246
32589
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
32247
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32590
|
+
const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
|
|
32248
32591
|
return reply.send(rows.map(serializeSession));
|
|
32249
32592
|
}
|
|
32250
32593
|
);
|
|
@@ -32252,11 +32595,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
32252
32595
|
"/projects/:name/discover/sessions/:id",
|
|
32253
32596
|
async (request, reply) => {
|
|
32254
32597
|
const project = resolveProject(app.db, request.params.name);
|
|
32255
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32598
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32256
32599
|
if (!session || session.projectId !== project.id) {
|
|
32257
32600
|
throw notFound("Discovery session", request.params.id);
|
|
32258
32601
|
}
|
|
32259
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32602
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32260
32603
|
const detail = {
|
|
32261
32604
|
...serializeSession(session),
|
|
32262
32605
|
probes: probeRows.map(serializeProbe)
|
|
@@ -32268,12 +32611,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
32268
32611
|
"/projects/:name/discover/sessions/:id/promote",
|
|
32269
32612
|
async (request, reply) => {
|
|
32270
32613
|
const project = resolveProject(app.db, request.params.name);
|
|
32271
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32614
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32272
32615
|
if (!session || session.projectId !== project.id) {
|
|
32273
32616
|
throw notFound("Discovery session", request.params.id);
|
|
32274
32617
|
}
|
|
32275
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32276
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32618
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32619
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
32277
32620
|
const seenCompetitors = new Set(existingCompetitors);
|
|
32278
32621
|
const cited = /* @__PURE__ */ new Set();
|
|
32279
32622
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -32302,7 +32645,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32302
32645
|
);
|
|
32303
32646
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
32304
32647
|
const project = resolveProject(app.db, request.params.name);
|
|
32305
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32648
|
+
const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
|
|
32306
32649
|
if (!session || session.projectId !== project.id) {
|
|
32307
32650
|
throw notFound("Discovery session", request.params.id);
|
|
32308
32651
|
}
|
|
@@ -32325,7 +32668,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32325
32668
|
const bucketSet = new Set(buckets);
|
|
32326
32669
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
32327
32670
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
32328
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32671
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
|
|
32329
32672
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
32330
32673
|
for (const probe of probeRows) {
|
|
32331
32674
|
if (!probe.bucket) continue;
|
|
@@ -32333,7 +32676,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32333
32676
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
32334
32677
|
}
|
|
32335
32678
|
const existingQueries = new Set(
|
|
32336
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
32679
|
+
app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
32337
32680
|
);
|
|
32338
32681
|
const promotedQueries = [];
|
|
32339
32682
|
const skippedQueries = [];
|
|
@@ -32349,7 +32692,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32349
32692
|
const skippedCompetitors = [];
|
|
32350
32693
|
if (includeCompetitors) {
|
|
32351
32694
|
const existingCompetitors = new Set(
|
|
32352
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32695
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
32353
32696
|
);
|
|
32354
32697
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
32355
32698
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -32453,7 +32796,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
32453
32796
|
|
|
32454
32797
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
32455
32798
|
import crypto27 from "crypto";
|
|
32456
|
-
import { eq as
|
|
32799
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
32457
32800
|
var DEFAULT_MAX_PROBES = 100;
|
|
32458
32801
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
32459
32802
|
function classifyProbeBucket(input) {
|
|
@@ -32507,7 +32850,7 @@ async function executeDiscovery(opts) {
|
|
|
32507
32850
|
status: DiscoverySessionStatuses.seeding,
|
|
32508
32851
|
dedupThreshold,
|
|
32509
32852
|
startedAt
|
|
32510
|
-
}).where(
|
|
32853
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32511
32854
|
const seedResult = await opts.deps.seed({
|
|
32512
32855
|
project: opts.project,
|
|
32513
32856
|
icpDescription: opts.icpDescription,
|
|
@@ -32533,7 +32876,7 @@ async function executeDiscovery(opts) {
|
|
|
32533
32876
|
seedCountRaw,
|
|
32534
32877
|
seedCount,
|
|
32535
32878
|
warning
|
|
32536
|
-
}).where(
|
|
32879
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32537
32880
|
const probeRows = [];
|
|
32538
32881
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
32539
32882
|
for (const query of probedCanonicals) {
|
|
@@ -32573,7 +32916,7 @@ async function executeDiscovery(opts) {
|
|
|
32573
32916
|
wastedCount: buckets["wasted-surface"],
|
|
32574
32917
|
competitorMap,
|
|
32575
32918
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32576
|
-
}).where(
|
|
32919
|
+
}).where(eq34(discoverySessions.id, opts.sessionId)).run();
|
|
32577
32920
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
32578
32921
|
return {
|
|
32579
32922
|
buckets,
|
|
@@ -32613,7 +32956,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
32613
32956
|
status: DiscoverySessionStatuses.failed,
|
|
32614
32957
|
error,
|
|
32615
32958
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32616
|
-
}).where(
|
|
32959
|
+
}).where(eq34(discoverySessions.id, sessionId)).run();
|
|
32617
32960
|
}
|
|
32618
32961
|
function dedupeStrings(input) {
|
|
32619
32962
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -32631,7 +32974,7 @@ function dedupeStrings(input) {
|
|
|
32631
32974
|
|
|
32632
32975
|
// ../api-routes/src/technical-aeo.ts
|
|
32633
32976
|
import crypto28 from "crypto";
|
|
32634
|
-
import { and as
|
|
32977
|
+
import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
|
|
32635
32978
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
32636
32979
|
function emptyScore(projectName) {
|
|
32637
32980
|
return {
|
|
@@ -32663,9 +33006,9 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
32663
33006
|
async function technicalAeoRoutes(app, opts) {
|
|
32664
33007
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
32665
33008
|
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
|
-
|
|
33009
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33010
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33011
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32669
33012
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32670
33013
|
notProbeRun()
|
|
32671
33014
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
@@ -32698,9 +33041,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32698
33041
|
});
|
|
32699
33042
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
32700
33043
|
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
|
-
|
|
33044
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33045
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33046
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32704
33047
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32705
33048
|
notProbeRun()
|
|
32706
33049
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
@@ -32708,9 +33051,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32708
33051
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
32709
33052
|
}
|
|
32710
33053
|
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 =
|
|
33054
|
+
const conds = [eq35(siteAuditPages.runId, latest.runId)];
|
|
33055
|
+
if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
|
|
33056
|
+
const where = and25(...conds);
|
|
32714
33057
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
32715
33058
|
const total = totalRow?.value ?? 0;
|
|
32716
33059
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
@@ -32734,9 +33077,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32734
33077
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
32735
33078
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
32736
33079
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
32737
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
32738
|
-
|
|
32739
|
-
|
|
33080
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
|
|
33081
|
+
eq35(siteAuditSnapshots.projectId, project.id),
|
|
33082
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32740
33083
|
inArray12(runs.status, SURFACEABLE_STATUSES),
|
|
32741
33084
|
notProbeRun()
|
|
32742
33085
|
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -32748,9 +33091,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
32748
33091
|
if (!parsed.success) {
|
|
32749
33092
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
32750
33093
|
}
|
|
32751
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
32752
|
-
|
|
32753
|
-
|
|
33094
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and25(
|
|
33095
|
+
eq35(runs.projectId, project.id),
|
|
33096
|
+
eq35(runs.kind, RunKinds["site-audit"]),
|
|
32754
33097
|
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
32755
33098
|
)).get();
|
|
32756
33099
|
if (existing) {
|
|
@@ -32937,6 +33280,8 @@ async function apiRoutes(app, opts) {
|
|
|
32937
33280
|
onInstallBacklinks: opts.onInstallBacklinks,
|
|
32938
33281
|
onReleaseSyncRequested: opts.onReleaseSyncRequested,
|
|
32939
33282
|
onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
|
|
33283
|
+
onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
|
|
33284
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
32940
33285
|
onBacklinksPruneCache: opts.onBacklinksPruneCache,
|
|
32941
33286
|
listCachedReleases: opts.listCachedReleases,
|
|
32942
33287
|
discoverLatestRelease: opts.discoverLatestRelease
|
|
@@ -33335,9 +33680,9 @@ var IntelligenceService = class {
|
|
|
33335
33680
|
*/
|
|
33336
33681
|
analyzeAndPersist(runId, projectId) {
|
|
33337
33682
|
const recentRuns = this.db.select().from(runs).where(
|
|
33338
|
-
|
|
33339
|
-
|
|
33340
|
-
or5(
|
|
33683
|
+
and26(
|
|
33684
|
+
eq36(runs.projectId, projectId),
|
|
33685
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33341
33686
|
// Defensive: RunCoordinator already skips probes before this is
|
|
33342
33687
|
// called, but if a future call site invokes analyzeAndPersist
|
|
33343
33688
|
// directly for a probe, probes still must not pollute the
|
|
@@ -33419,7 +33764,7 @@ var IntelligenceService = class {
|
|
|
33419
33764
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
33420
33765
|
*/
|
|
33421
33766
|
analyzeAndPersistGbp(runId, projectId) {
|
|
33422
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
33767
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
|
|
33423
33768
|
if (!runRow) {
|
|
33424
33769
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
33425
33770
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -33427,9 +33772,9 @@ var IntelligenceService = class {
|
|
|
33427
33772
|
}
|
|
33428
33773
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
33429
33774
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
33430
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
33431
|
-
|
|
33432
|
-
|
|
33775
|
+
const selected = this.db.select().from(gbpLocations).where(and26(
|
|
33776
|
+
eq36(gbpLocations.projectId, projectId),
|
|
33777
|
+
eq36(gbpLocations.selected, true),
|
|
33433
33778
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
33434
33779
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
33435
33780
|
)).all();
|
|
@@ -33464,10 +33809,10 @@ var IntelligenceService = class {
|
|
|
33464
33809
|
}
|
|
33465
33810
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
33466
33811
|
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(
|
|
33812
|
+
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();
|
|
33813
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and26(eq36(gbpPlaceActions.projectId, projectId), eq36(gbpPlaceActions.locationName, locationName))).all();
|
|
33814
|
+
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();
|
|
33815
|
+
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
33816
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
33472
33817
|
const summary = buildGbpSummary({
|
|
33473
33818
|
locationName,
|
|
@@ -33499,7 +33844,7 @@ var IntelligenceService = class {
|
|
|
33499
33844
|
/** Build the month-over-month keyword series for a location from the
|
|
33500
33845
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
33501
33846
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
33502
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
33847
|
+
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
33848
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
33504
33849
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
33505
33850
|
const recentMonth = months[0] ?? null;
|
|
@@ -33530,7 +33875,7 @@ var IntelligenceService = class {
|
|
|
33530
33875
|
*/
|
|
33531
33876
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
33532
33877
|
const covered = new Set(coveredLocationNames);
|
|
33533
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
33878
|
+
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
33879
|
const staleIds = [];
|
|
33535
33880
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
33536
33881
|
for (const row of existing) {
|
|
@@ -33541,7 +33886,7 @@ var IntelligenceService = class {
|
|
|
33541
33886
|
}
|
|
33542
33887
|
this.db.transaction((tx) => {
|
|
33543
33888
|
for (const id of staleIds) {
|
|
33544
|
-
tx.delete(insights).where(
|
|
33889
|
+
tx.delete(insights).where(eq36(insights.id, id)).run();
|
|
33545
33890
|
}
|
|
33546
33891
|
for (const insight of gbpInsights) {
|
|
33547
33892
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -33619,7 +33964,7 @@ var IntelligenceService = class {
|
|
|
33619
33964
|
* create per run + aggregate). DB is left untouched.
|
|
33620
33965
|
*/
|
|
33621
33966
|
backfill(projectName, opts, onProgress) {
|
|
33622
|
-
const project = this.db.select().from(projects).where(
|
|
33967
|
+
const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
|
|
33623
33968
|
if (!project) {
|
|
33624
33969
|
throw new Error(`Project "${projectName}" not found`);
|
|
33625
33970
|
}
|
|
@@ -33632,9 +33977,9 @@ var IntelligenceService = class {
|
|
|
33632
33977
|
sinceTimestamp = parsed;
|
|
33633
33978
|
}
|
|
33634
33979
|
const allRuns = this.db.select().from(runs).where(
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
or5(
|
|
33980
|
+
and26(
|
|
33981
|
+
eq36(runs.projectId, project.id),
|
|
33982
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33638
33983
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
33639
33984
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33640
33985
|
)
|
|
@@ -33713,7 +34058,7 @@ var IntelligenceService = class {
|
|
|
33713
34058
|
return { processed, skipped, totalInsights };
|
|
33714
34059
|
}
|
|
33715
34060
|
loadTrackedCompetitors(projectId) {
|
|
33716
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34061
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
33717
34062
|
}
|
|
33718
34063
|
/**
|
|
33719
34064
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -33734,15 +34079,15 @@ var IntelligenceService = class {
|
|
|
33734
34079
|
}
|
|
33735
34080
|
persistResult(result, runId, projectId) {
|
|
33736
34081
|
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(
|
|
34082
|
+
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
34083
|
for (const row of existingInsights) {
|
|
33739
34084
|
if (row.dismissed) {
|
|
33740
34085
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
33741
34086
|
}
|
|
33742
34087
|
}
|
|
33743
34088
|
this.db.transaction((tx) => {
|
|
33744
|
-
tx.delete(insights).where(
|
|
33745
|
-
tx.delete(healthSnapshots).where(
|
|
34089
|
+
tx.delete(insights).where(eq36(insights.runId, runId)).run();
|
|
34090
|
+
tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
|
|
33746
34091
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33747
34092
|
for (const insight of result.insights) {
|
|
33748
34093
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -33793,24 +34138,24 @@ var IntelligenceService = class {
|
|
|
33793
34138
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
33794
34139
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
33795
34140
|
if (regressions.length === 0) return rawInsights;
|
|
33796
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
34141
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
|
|
33797
34142
|
const gscConnected = gscRows.length > 0;
|
|
33798
34143
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
33799
34144
|
for (const row of gscRows) {
|
|
33800
34145
|
const key = row.query.toLowerCase();
|
|
33801
34146
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
33802
34147
|
}
|
|
33803
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
34148
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33804
34149
|
const locationCount = Math.max(
|
|
33805
34150
|
1,
|
|
33806
34151
|
(projectRow?.locations ?? []).length
|
|
33807
34152
|
);
|
|
33808
34153
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
33809
34154
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
33810
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
33813
|
-
or5(
|
|
34155
|
+
and26(
|
|
34156
|
+
eq36(runs.projectId, projectId),
|
|
34157
|
+
eq36(runs.kind, RunKinds["answer-visibility"]),
|
|
34158
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
|
|
33814
34159
|
// Defensive — see top of file.
|
|
33815
34160
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33816
34161
|
)
|
|
@@ -33830,7 +34175,7 @@ var IntelligenceService = class {
|
|
|
33830
34175
|
const haveHistory = recentRunIds.length > 0;
|
|
33831
34176
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
33832
34177
|
if (haveHistory) {
|
|
33833
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
34178
|
+
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
34179
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
33835
34180
|
for (const row of priorRows) {
|
|
33836
34181
|
if (!row.runId) continue;
|
|
@@ -33859,7 +34204,7 @@ var IntelligenceService = class {
|
|
|
33859
34204
|
});
|
|
33860
34205
|
}
|
|
33861
34206
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
33862
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
34207
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
33863
34208
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
33864
34209
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
33865
34210
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -33875,7 +34220,7 @@ var IntelligenceService = class {
|
|
|
33875
34220
|
citedDomains: querySnapshots.citedDomains,
|
|
33876
34221
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
33877
34222
|
snapshotLocation: querySnapshots.location
|
|
33878
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
34223
|
+
}).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
|
|
33879
34224
|
const snapshots = [];
|
|
33880
34225
|
let orphanCount = 0;
|
|
33881
34226
|
for (const r of rows) {
|
|
@@ -33987,6 +34332,8 @@ export {
|
|
|
33987
34332
|
hashLodging,
|
|
33988
34333
|
getUrlInfo,
|
|
33989
34334
|
getCrawlIssues,
|
|
34335
|
+
getLinkCounts,
|
|
34336
|
+
getUrlLinks,
|
|
33990
34337
|
PLUGIN_DIR,
|
|
33991
34338
|
DUCKDB_SPEC,
|
|
33992
34339
|
CC_CACHE_DIR,
|