@ainyc/canonry 4.78.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.
Files changed (26) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +38 -12
  2. package/assets/assets/{BacklinksPage-CwXveumn.js → BacklinksPage-DHShKKpo.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-DntKGI5J.js → ChartPrimitives-udHScxjY.js} +1 -1
  4. package/assets/assets/ProjectPage-BsS1anh7.js +6 -0
  5. package/assets/assets/{RunRow-DMtYXaxG.js → RunRow-CXyPHMVQ.js} +1 -1
  6. package/assets/assets/{RunsPage-Cz-YlucO.js → RunsPage-BpQ_NpFt.js} +1 -1
  7. package/assets/assets/{SettingsPage-BCuG3C-0.js → SettingsPage-1ep4ch7n.js} +1 -1
  8. package/assets/assets/{TrafficPage-DV8X47wa.js → TrafficPage-C3Hx-sE7.js} +1 -1
  9. package/assets/assets/TrafficSourceDetailPage-B26n2R6G.js +1 -0
  10. package/assets/assets/{arrow-left-CUmHyNnF.js → arrow-left-Dc_IPJxw.js} +1 -1
  11. package/assets/assets/{extract-error-message-DFjy9_zi.js → extract-error-message-B3PoKkHW.js} +1 -1
  12. package/assets/assets/{index-D9smxU6R.js → index-DhdFTQkU.js} +86 -86
  13. package/assets/assets/{trash-2-B_UtEEm8.js → trash-2-BQ69cGl0.js} +1 -1
  14. package/assets/index.html +1 -1
  15. package/dist/{chunk-XI6YSTGE.js → chunk-6XOZSS3Y.js} +258 -8
  16. package/dist/{chunk-KPN22EWK.js → chunk-GMT3YPLT.js} +214 -4
  17. package/dist/{chunk-3WXARKUE.js → chunk-UAQ42NVJ.js} +1346 -357
  18. package/dist/{chunk-QKTIP6GC.js → chunk-VX5C7DK7.js} +902 -313
  19. package/dist/cli.js +468 -152
  20. package/dist/index.d.ts +17 -0
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-CDVUUG7O.js → intelligence-service-CAAQAKPN.js} +2 -2
  23. package/dist/mcp.js +9 -3
  24. package/package.json +9 -8
  25. package/assets/assets/ProjectPage-CVudiU8X.js +0 -6
  26. package/assets/assets/TrafficSourceDetailPage-BmYhK9jm.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,
@@ -34,6 +35,16 @@ import {
34
35
  __export,
35
36
  absolutizeProjectUrl,
36
37
  actionConfidenceLabel,
38
+ adsCampaignListResponseSchema,
39
+ adsConnectRequestSchema,
40
+ adsConnectionStatusDtoSchema,
41
+ adsCpcMicros,
42
+ adsCtr,
43
+ adsDisconnectResponseSchema,
44
+ adsInsightLevelSchema,
45
+ adsInsightsResponseSchema,
46
+ adsSummaryDtoSchema,
47
+ adsSyncResponseSchema,
37
48
  agentProvidersResponseDtoSchema,
38
49
  apiKeyDtoSchema,
39
50
  apiKeyListDtoSchema,
@@ -42,6 +53,8 @@ import {
42
53
  authRequired,
43
54
  backlinkHistoryEntrySchema,
44
55
  backlinkListResponseSchema,
56
+ backlinkSourceSchema,
57
+ backlinkSourcesResponseSchema,
45
58
  backlinkSummaryDtoSchema,
46
59
  backlinksInstallResultDtoSchema,
47
60
  backlinksInstallStatusDtoSchema,
@@ -232,10 +245,10 @@ import {
232
245
  wordpressSchemaDeployResultDtoSchema,
233
246
  wordpressSchemaStatusResultDtoSchema,
234
247
  wordpressStatusDtoSchema
235
- } from "./chunk-KPN22EWK.js";
248
+ } from "./chunk-GMT3YPLT.js";
236
249
 
237
250
  // src/intelligence-service.ts
238
- import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
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";
239
252
 
240
253
  // ../db/src/client.ts
241
254
  import { mkdirSync } from "fs";
@@ -246,6 +259,11 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
246
259
  // ../db/src/schema.ts
247
260
  var schema_exports = {};
248
261
  __export(schema_exports, {
262
+ adsAdGroups: () => adsAdGroups,
263
+ adsAds: () => adsAds,
264
+ adsCampaigns: () => adsCampaigns,
265
+ adsConnections: () => adsConnections,
266
+ adsInsightsDaily: () => adsInsightsDaily,
249
267
  agentMemory: () => agentMemory,
250
268
  agentSessions: () => agentSessions,
251
269
  aiReferralEventsHourly: () => aiReferralEventsHourly,
@@ -835,7 +853,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
835
853
  var backlinkDomains = sqliteTable("backlink_domains", {
836
854
  id: text("id").primaryKey(),
837
855
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
838
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
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"),
839
859
  release: text("release").notNull(),
840
860
  targetDomain: text("target_domain").notNull(),
841
861
  linkingDomain: text("linking_domain").notNull(),
@@ -846,12 +866,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
846
866
  index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
847
867
  index("idx_backlink_domains_project_release").on(table.projectId, table.release),
848
868
  index("idx_backlink_domains_hosts").on(table.numHosts),
849
- 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)
850
870
  ]);
851
871
  var backlinkSummaries = sqliteTable("backlink_summaries", {
852
872
  id: text("id").primaryKey(),
853
873
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
854
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
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"),
855
877
  release: text("release").notNull(),
856
878
  targetDomain: text("target_domain").notNull(),
857
879
  totalLinkingDomains: integer("total_linking_domains").notNull(),
@@ -860,7 +882,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
860
882
  queriedAt: text("queried_at").notNull(),
861
883
  createdAt: text("created_at").notNull()
862
884
  }, (table) => [
863
- 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),
864
886
  index("idx_backlink_summaries_project").on(table.projectId)
865
887
  ]);
866
888
  var agentMemory = sqliteTable("agent_memory", {
@@ -1219,6 +1241,83 @@ var gbpPlaceDetails = sqliteTable("gbp_place_details", {
1219
1241
  }, (table) => [
1220
1242
  index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
1221
1243
  ]);
1244
+ var adsConnections = sqliteTable("ads_connections", {
1245
+ id: text("id").primaryKey(),
1246
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1247
+ adAccountId: text("ad_account_id").notNull(),
1248
+ displayName: text("display_name"),
1249
+ currencyCode: text("currency_code"),
1250
+ timezone: text("timezone"),
1251
+ status: text("status"),
1252
+ lastSyncedAt: text("last_synced_at"),
1253
+ createdAt: text("created_at").notNull(),
1254
+ updatedAt: text("updated_at").notNull()
1255
+ }, (table) => [
1256
+ uniqueIndex("idx_ads_conn_project").on(table.projectId)
1257
+ ]);
1258
+ var adsCampaigns = sqliteTable("ads_campaigns", {
1259
+ id: text("id").primaryKey(),
1260
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1261
+ name: text("name").notNull(),
1262
+ status: text("status").notNull(),
1263
+ biddingType: text("bidding_type"),
1264
+ dailySpendLimitMicros: integer("daily_spend_limit_micros"),
1265
+ lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
1266
+ targeting: text("targeting", { mode: "json" }).$type(),
1267
+ upstreamCreatedAt: integer("upstream_created_at"),
1268
+ upstreamUpdatedAt: integer("upstream_updated_at"),
1269
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
1270
+ syncedAt: text("synced_at").notNull()
1271
+ }, (table) => [
1272
+ index("idx_ads_campaigns_project").on(table.projectId)
1273
+ ]);
1274
+ var adsAdGroups = sqliteTable("ads_ad_groups", {
1275
+ id: text("id").primaryKey(),
1276
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1277
+ campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
1278
+ name: text("name").notNull(),
1279
+ status: text("status").notNull(),
1280
+ billingEventType: text("billing_event_type"),
1281
+ maxBidMicros: integer("max_bid_micros"),
1282
+ contextHints: text("context_hints", { mode: "json" }).$type().notNull().default([]),
1283
+ upstreamCreatedAt: integer("upstream_created_at"),
1284
+ upstreamUpdatedAt: integer("upstream_updated_at"),
1285
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
1286
+ syncedAt: text("synced_at").notNull()
1287
+ }, (table) => [
1288
+ index("idx_ads_ad_groups_project").on(table.projectId),
1289
+ index("idx_ads_ad_groups_campaign").on(table.campaignId)
1290
+ ]);
1291
+ var adsAds = sqliteTable("ads_ads", {
1292
+ id: text("id").primaryKey(),
1293
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1294
+ adGroupId: text("ad_group_id").notNull().references(() => adsAdGroups.id, { onDelete: "cascade" }),
1295
+ name: text("name").notNull(),
1296
+ status: text("status").notNull(),
1297
+ creative: text("creative", { mode: "json" }).$type(),
1298
+ reviewStatus: text("review_status"),
1299
+ upstreamCreatedAt: integer("upstream_created_at"),
1300
+ upstreamUpdatedAt: integer("upstream_updated_at"),
1301
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" }),
1302
+ syncedAt: text("synced_at").notNull()
1303
+ }, (table) => [
1304
+ index("idx_ads_ads_project").on(table.projectId),
1305
+ index("idx_ads_ads_group").on(table.adGroupId)
1306
+ ]);
1307
+ var adsInsightsDaily = sqliteTable("ads_insights_daily", {
1308
+ id: text("id").primaryKey(),
1309
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1310
+ level: text("level").notNull(),
1311
+ entityId: text("entity_id").notNull(),
1312
+ date: text("date").notNull(),
1313
+ impressions: integer("impressions").notNull().default(0),
1314
+ clicks: integer("clicks").notNull().default(0),
1315
+ spendMicros: integer("spend_micros").notNull().default(0),
1316
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
1317
+ }, (table) => [
1318
+ uniqueIndex("uniq_ads_insights_daily").on(table.projectId, table.level, table.entityId, table.date),
1319
+ index("idx_ads_insights_project_date").on(table.projectId, table.date)
1320
+ ]);
1222
1321
 
1223
1322
  // ../db/src/client.ts
1224
1323
  function createClient(databasePath) {
@@ -2876,8 +2975,169 @@ var MIGRATION_VERSIONS = [
2876
2975
  statements: [
2877
2976
  `ALTER TABLE discovery_sessions ADD COLUMN warning TEXT`
2878
2977
  ]
2978
+ },
2979
+ {
2980
+ // OpenAI Advertiser API (ChatGPT ads) — connection metadata, entity
2981
+ // snapshots (campaigns / ad groups / ads), and daily paid-performance
2982
+ // rollups. One connection per project (ad accounts are not domain-bound).
2983
+ // Money columns are integer micros; ads-sync normalizes the insights
2984
+ // API's decimal-dollar spend at ingest. Credentials live in config.yaml.
2985
+ version: 77,
2986
+ name: "openai-ads-tables",
2987
+ statements: [
2988
+ `CREATE TABLE IF NOT EXISTS ads_connections (
2989
+ id TEXT PRIMARY KEY,
2990
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2991
+ ad_account_id TEXT NOT NULL,
2992
+ display_name TEXT,
2993
+ currency_code TEXT,
2994
+ timezone TEXT,
2995
+ status TEXT,
2996
+ last_synced_at TEXT,
2997
+ created_at TEXT NOT NULL,
2998
+ updated_at TEXT NOT NULL
2999
+ )`,
3000
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_conn_project ON ads_connections(project_id)`,
3001
+ `CREATE TABLE IF NOT EXISTS ads_campaigns (
3002
+ id TEXT PRIMARY KEY,
3003
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3004
+ name TEXT NOT NULL,
3005
+ status TEXT NOT NULL,
3006
+ bidding_type TEXT,
3007
+ daily_spend_limit_micros INTEGER,
3008
+ lifetime_spend_limit_micros INTEGER,
3009
+ targeting TEXT,
3010
+ upstream_created_at INTEGER,
3011
+ upstream_updated_at INTEGER,
3012
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
3013
+ synced_at TEXT NOT NULL
3014
+ )`,
3015
+ `CREATE INDEX IF NOT EXISTS idx_ads_campaigns_project ON ads_campaigns(project_id)`,
3016
+ `CREATE TABLE IF NOT EXISTS ads_ad_groups (
3017
+ id TEXT PRIMARY KEY,
3018
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3019
+ campaign_id TEXT NOT NULL REFERENCES ads_campaigns(id) ON DELETE CASCADE,
3020
+ name TEXT NOT NULL,
3021
+ status TEXT NOT NULL,
3022
+ billing_event_type TEXT,
3023
+ max_bid_micros INTEGER,
3024
+ context_hints TEXT NOT NULL DEFAULT '[]',
3025
+ upstream_created_at INTEGER,
3026
+ upstream_updated_at INTEGER,
3027
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
3028
+ synced_at TEXT NOT NULL
3029
+ )`,
3030
+ `CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_project ON ads_ad_groups(project_id)`,
3031
+ `CREATE INDEX IF NOT EXISTS idx_ads_ad_groups_campaign ON ads_ad_groups(campaign_id)`,
3032
+ `CREATE TABLE IF NOT EXISTS ads_ads (
3033
+ id TEXT PRIMARY KEY,
3034
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3035
+ ad_group_id TEXT NOT NULL REFERENCES ads_ad_groups(id) ON DELETE CASCADE,
3036
+ name TEXT NOT NULL,
3037
+ status TEXT NOT NULL,
3038
+ creative TEXT,
3039
+ review_status TEXT,
3040
+ upstream_created_at INTEGER,
3041
+ upstream_updated_at INTEGER,
3042
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
3043
+ synced_at TEXT NOT NULL
3044
+ )`,
3045
+ `CREATE INDEX IF NOT EXISTS idx_ads_ads_project ON ads_ads(project_id)`,
3046
+ `CREATE INDEX IF NOT EXISTS idx_ads_ads_group ON ads_ads(ad_group_id)`,
3047
+ `CREATE TABLE IF NOT EXISTS ads_insights_daily (
3048
+ id TEXT PRIMARY KEY,
3049
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3050
+ level TEXT NOT NULL,
3051
+ entity_id TEXT NOT NULL,
3052
+ date TEXT NOT NULL,
3053
+ impressions INTEGER NOT NULL DEFAULT 0,
3054
+ clicks INTEGER NOT NULL DEFAULT 0,
3055
+ spend_micros INTEGER NOT NULL DEFAULT 0,
3056
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
3057
+ )`,
3058
+ `CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
3059
+ `CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
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
+ }
2879
3077
  }
2880
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
+ }
2881
3141
  function isDuplicateColumnError(err) {
2882
3142
  if (!(err instanceof Error)) return false;
2883
3143
  if (err.message.includes("duplicate column name")) return true;
@@ -13020,11 +13280,18 @@ function makeSnippet(text2, query) {
13020
13280
  import { z } from "zod";
13021
13281
  var SCHEMA_TABLE = {
13022
13282
  AgentProvidersResponseDto: agentProvidersResponseDtoSchema,
13283
+ AdsCampaignListResponse: adsCampaignListResponseSchema,
13284
+ AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
13285
+ AdsDisconnectResponse: adsDisconnectResponseSchema,
13286
+ AdsInsightsResponse: adsInsightsResponseSchema,
13287
+ AdsSummaryDto: adsSummaryDtoSchema,
13288
+ AdsSyncResponse: adsSyncResponseSchema,
13023
13289
  ApiKeyDto: apiKeyDtoSchema,
13024
13290
  ApiKeyListDto: apiKeyListDtoSchema,
13025
13291
  AuditLogEntry: auditLogEntrySchema,
13026
13292
  BacklinkHistoryEntry: backlinkHistoryEntrySchema,
13027
13293
  BacklinkListResponse: backlinkListResponseSchema,
13294
+ BacklinkSourcesResponse: backlinkSourcesResponseSchema,
13028
13295
  BacklinkSummaryDto: backlinkSummaryDtoSchema,
13029
13296
  BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
13030
13297
  BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
@@ -15061,6 +15328,106 @@ var routeCatalog = [
15061
15328
  404: errorResponse("Project not found.")
15062
15329
  }
15063
15330
  },
15331
+ {
15332
+ method: "post",
15333
+ path: "/api/v1/projects/{name}/ads/connect",
15334
+ summary: "Connect an OpenAI ad account (ChatGPT ads) with an Ads Manager SDK key",
15335
+ tags: ["ads"],
15336
+ parameters: [nameParameter],
15337
+ requestBody: {
15338
+ required: true,
15339
+ content: {
15340
+ "application/json": {
15341
+ schema: {
15342
+ type: "object",
15343
+ required: ["apiKey"],
15344
+ properties: {
15345
+ apiKey: stringSchema
15346
+ }
15347
+ }
15348
+ }
15349
+ }
15350
+ },
15351
+ responses: {
15352
+ 200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
15353
+ 400: errorResponse("Missing/invalid key or credential storage unavailable."),
15354
+ 404: errorResponse("Project not found.")
15355
+ }
15356
+ },
15357
+ {
15358
+ method: "delete",
15359
+ path: "/api/v1/projects/{name}/ads/connection",
15360
+ summary: "Disconnect the OpenAI ad account (removes the stored credential)",
15361
+ tags: ["ads"],
15362
+ parameters: [nameParameter],
15363
+ responses: {
15364
+ 200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
15365
+ 404: errorResponse("Project not found.")
15366
+ }
15367
+ },
15368
+ {
15369
+ method: "get",
15370
+ path: "/api/v1/projects/{name}/ads/status",
15371
+ summary: "OpenAI ads connection status and last sync time",
15372
+ tags: ["ads"],
15373
+ parameters: [nameParameter],
15374
+ responses: {
15375
+ 200: jsonResponse("Connection status.", "AdsConnectionStatusDto"),
15376
+ 404: errorResponse("Project not found.")
15377
+ }
15378
+ },
15379
+ {
15380
+ method: "post",
15381
+ path: "/api/v1/projects/{name}/ads/sync",
15382
+ summary: "Trigger an ads-sync run (entity snapshots + daily paid-performance rollups)",
15383
+ tags: ["ads"],
15384
+ parameters: [nameParameter],
15385
+ responses: {
15386
+ 200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
15387
+ 400: errorResponse("No ads connection for this project."),
15388
+ 404: errorResponse("Project not found.")
15389
+ }
15390
+ },
15391
+ {
15392
+ method: "get",
15393
+ path: "/api/v1/projects/{name}/ads/campaigns",
15394
+ summary: "Synced campaign snapshots with nested ad groups (context hints) and ads",
15395
+ tags: ["ads"],
15396
+ parameters: [nameParameter],
15397
+ responses: {
15398
+ 200: jsonResponse("Campaign snapshots.", "AdsCampaignListResponse"),
15399
+ 404: errorResponse("Project not found.")
15400
+ }
15401
+ },
15402
+ {
15403
+ method: "get",
15404
+ path: "/api/v1/projects/{name}/ads/insights",
15405
+ summary: "Daily paid-performance rollups (spend in integer micros; ctr/cpc derived server-side)",
15406
+ tags: ["ads"],
15407
+ parameters: [
15408
+ nameParameter,
15409
+ { in: "query", name: "level", required: false, description: "campaign | ad_group", schema: stringSchema },
15410
+ { in: "query", name: "entityId", required: false, description: "Scope to one upstream entity id", schema: stringSchema },
15411
+ { in: "query", name: "from", required: false, description: "Inclusive start date (YYYY-MM-DD)", schema: stringSchema },
15412
+ { in: "query", name: "to", required: false, description: "Inclusive end date (YYYY-MM-DD)", schema: stringSchema }
15413
+ ],
15414
+ responses: {
15415
+ 200: jsonResponse("Daily rollup rows.", "AdsInsightsResponse"),
15416
+ 400: errorResponse("Invalid level filter."),
15417
+ 404: errorResponse("Project not found.")
15418
+ }
15419
+ },
15420
+ {
15421
+ method: "get",
15422
+ path: "/api/v1/projects/{name}/ads/summary",
15423
+ summary: "Composite paid-performance summary (campaign-level totals; all derived metrics)",
15424
+ tags: ["ads"],
15425
+ parameters: [nameParameter],
15426
+ responses: {
15427
+ 200: jsonResponse("Summary returned.", "AdsSummaryDto"),
15428
+ 404: errorResponse("Project not found.")
15429
+ }
15430
+ },
15064
15431
  {
15065
15432
  method: "post",
15066
15433
  path: "/api/v1/projects/{name}/bing/connect",
@@ -16385,7 +16752,8 @@ var routeCatalog = [
16385
16752
  tags: ["backlinks"],
16386
16753
  parameters: [
16387
16754
  nameParameter,
16388
- { 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 }
16389
16757
  ],
16390
16758
  responses: {
16391
16759
  200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
@@ -16403,7 +16771,8 @@ var routeCatalog = [
16403
16771
  nameParameter,
16404
16772
  { name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
16405
16773
  { name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
16406
- { 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 }
16407
16776
  ],
16408
16777
  responses: {
16409
16778
  200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
@@ -16415,12 +16784,44 @@ var routeCatalog = [
16415
16784
  path: "/api/v1/projects/{name}/backlinks/history",
16416
16785
  summary: "Get per-release backlink summaries for a project",
16417
16786
  tags: ["backlinks"],
16418
- parameters: [nameParameter],
16787
+ parameters: [
16788
+ nameParameter,
16789
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16790
+ ],
16419
16791
  responses: {
16420
16792
  200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
16421
16793
  404: errorResponse("Project not found.")
16422
16794
  }
16423
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
+ },
16424
16825
  {
16425
16826
  method: "post",
16426
16827
  path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
@@ -20351,15 +20752,276 @@ async function googleRoutes(app, opts) {
20351
20752
  });
20352
20753
  }
20353
20754
 
20354
- // ../api-routes/src/bing.ts
20755
+ // ../api-routes/src/ads.ts
20355
20756
  import crypto18 from "crypto";
20356
- import { eq as eq21, and as and14, desc as desc11 } from "drizzle-orm";
20757
+ import { eq as eq21, and as and14, asc as asc2, gte as gte3, lte as lte2, inArray as inArray10 } from "drizzle-orm";
20758
+ function statusDto(row) {
20759
+ if (!row) return { connected: false };
20760
+ return {
20761
+ connected: true,
20762
+ adAccountId: row.adAccountId,
20763
+ displayName: row.displayName,
20764
+ currencyCode: row.currencyCode,
20765
+ timezone: row.timezone,
20766
+ status: row.status,
20767
+ lastSyncedAt: row.lastSyncedAt
20768
+ };
20769
+ }
20770
+ function creativeDto(raw) {
20771
+ if (raw == null || typeof raw !== "object") return null;
20772
+ const c = raw;
20773
+ return {
20774
+ type: typeof c.type === "string" ? c.type : null,
20775
+ title: typeof c.title === "string" ? c.title : null,
20776
+ body: typeof c.body === "string" ? c.body : null,
20777
+ targetUrl: typeof c.target_url === "string" ? c.target_url : null
20778
+ };
20779
+ }
20780
+ async function adsRoutes(app, opts) {
20781
+ app.post(
20782
+ "/projects/:name/ads/connect",
20783
+ async (request) => {
20784
+ const project = resolveProject(app.db, request.params.name);
20785
+ const parsed = adsConnectRequestSchema.safeParse(request.body ?? {});
20786
+ if (!parsed.success) throw validationError('"apiKey" is required');
20787
+ if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
20788
+ throw validationError("Ads credential storage is not configured for this deployment");
20789
+ }
20790
+ let account;
20791
+ try {
20792
+ account = await opts.verifyAdsAccount(parsed.data.apiKey);
20793
+ } catch (err) {
20794
+ const message = err instanceof Error ? err.message : String(err);
20795
+ throw validationError(`OpenAI Ads API rejected the key: ${message}`);
20796
+ }
20797
+ const now = (/* @__PURE__ */ new Date()).toISOString();
20798
+ const existingCfg = opts.adsCredentialStore.getConnection(project.name);
20799
+ opts.adsCredentialStore.upsertConnection({
20800
+ projectName: project.name,
20801
+ apiKey: parsed.data.apiKey,
20802
+ adAccountId: account.id,
20803
+ createdAt: existingCfg?.createdAt ?? now,
20804
+ updatedAt: now
20805
+ });
20806
+ const existingRow = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20807
+ app.db.transaction((tx) => {
20808
+ if (existingRow) {
20809
+ tx.update(adsConnections).set({
20810
+ adAccountId: account.id,
20811
+ displayName: account.name,
20812
+ currencyCode: account.currencyCode,
20813
+ timezone: account.timezone,
20814
+ status: account.status,
20815
+ updatedAt: now
20816
+ }).where(eq21(adsConnections.id, existingRow.id)).run();
20817
+ } else {
20818
+ tx.insert(adsConnections).values({
20819
+ id: crypto18.randomUUID(),
20820
+ projectId: project.id,
20821
+ adAccountId: account.id,
20822
+ displayName: account.name,
20823
+ currencyCode: account.currencyCode,
20824
+ timezone: account.timezone,
20825
+ status: account.status,
20826
+ createdAt: now,
20827
+ updatedAt: now
20828
+ }).run();
20829
+ }
20830
+ writeAuditLog(tx, auditFromRequest(request, {
20831
+ projectId: project.id,
20832
+ actor: "api",
20833
+ action: "ads.connected",
20834
+ entityType: "ads-connection",
20835
+ entityId: account.id
20836
+ }));
20837
+ });
20838
+ const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20839
+ return statusDto(row);
20840
+ }
20841
+ );
20842
+ app.delete("/projects/:name/ads/connection", async (request) => {
20843
+ const project = resolveProject(app.db, request.params.name);
20844
+ const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20845
+ if (row) {
20846
+ app.db.transaction((tx) => {
20847
+ tx.delete(adsConnections).where(eq21(adsConnections.id, row.id)).run();
20848
+ writeAuditLog(tx, auditFromRequest(request, {
20849
+ projectId: project.id,
20850
+ actor: "api",
20851
+ action: "ads.disconnected",
20852
+ entityType: "ads-connection",
20853
+ entityId: row.adAccountId
20854
+ }));
20855
+ });
20856
+ }
20857
+ const removedFromConfig = opts.adsCredentialStore?.removeConnection(project.name) ?? false;
20858
+ const response = { disconnected: Boolean(row) || removedFromConfig };
20859
+ return response;
20860
+ });
20861
+ app.get("/projects/:name/ads/status", async (request) => {
20862
+ const project = resolveProject(app.db, request.params.name);
20863
+ const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20864
+ return statusDto(row);
20865
+ });
20866
+ app.post("/projects/:name/ads/sync", async (request) => {
20867
+ const project = resolveProject(app.db, request.params.name);
20868
+ const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20869
+ if (!row) {
20870
+ throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
20871
+ }
20872
+ const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and14(
20873
+ eq21(runs.projectId, project.id),
20874
+ eq21(runs.kind, RunKinds["ads-sync"]),
20875
+ inArray10(runs.status, [RunStatuses.queued, RunStatuses.running])
20876
+ )).get();
20877
+ if (inFlight) {
20878
+ const existing = { runId: inFlight.id, status: inFlight.status };
20879
+ return existing;
20880
+ }
20881
+ const runId = crypto18.randomUUID();
20882
+ app.db.insert(runs).values({
20883
+ id: runId,
20884
+ projectId: project.id,
20885
+ kind: RunKinds["ads-sync"],
20886
+ status: RunStatuses.queued,
20887
+ trigger: RunTriggers.manual,
20888
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
20889
+ }).run();
20890
+ opts.onAdsSyncRequested?.(runId, project.id);
20891
+ const response = { runId, status: RunStatuses.queued };
20892
+ return response;
20893
+ });
20894
+ app.get("/projects/:name/ads/campaigns", async (request) => {
20895
+ const project = resolveProject(app.db, request.params.name);
20896
+ const campaignRows = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all();
20897
+ const groupRows = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all();
20898
+ const adRows = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all();
20899
+ const adsByGroup = /* @__PURE__ */ new Map();
20900
+ for (const ad of adRows) {
20901
+ const dto = {
20902
+ id: ad.id,
20903
+ adGroupId: ad.adGroupId,
20904
+ name: ad.name,
20905
+ status: ad.status,
20906
+ reviewStatus: ad.reviewStatus,
20907
+ creative: creativeDto(ad.creative)
20908
+ };
20909
+ const list = adsByGroup.get(ad.adGroupId) ?? [];
20910
+ list.push(dto);
20911
+ adsByGroup.set(ad.adGroupId, list);
20912
+ }
20913
+ const groupsByCampaign = /* @__PURE__ */ new Map();
20914
+ for (const group of groupRows) {
20915
+ const dto = {
20916
+ id: group.id,
20917
+ campaignId: group.campaignId,
20918
+ name: group.name,
20919
+ status: group.status,
20920
+ billingEventType: group.billingEventType,
20921
+ maxBidMicros: group.maxBidMicros,
20922
+ contextHints: group.contextHints,
20923
+ ads: adsByGroup.get(group.id) ?? []
20924
+ };
20925
+ const list = groupsByCampaign.get(group.campaignId) ?? [];
20926
+ list.push(dto);
20927
+ groupsByCampaign.set(group.campaignId, list);
20928
+ }
20929
+ const campaigns = campaignRows.map((campaign) => ({
20930
+ id: campaign.id,
20931
+ name: campaign.name,
20932
+ status: campaign.status,
20933
+ biddingType: campaign.biddingType,
20934
+ dailySpendLimitMicros: campaign.dailySpendLimitMicros,
20935
+ lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
20936
+ adGroups: groupsByCampaign.get(campaign.id) ?? []
20937
+ }));
20938
+ const response = { campaigns };
20939
+ return response;
20940
+ });
20941
+ app.get("/projects/:name/ads/insights", async (request) => {
20942
+ const project = resolveProject(app.db, request.params.name);
20943
+ const { level, entityId, from, to } = request.query;
20944
+ let parsedLevel;
20945
+ if (level !== void 0) {
20946
+ const result = adsInsightLevelSchema.safeParse(level);
20947
+ if (!result.success) {
20948
+ throw validationError('"level" must be one of: campaign, ad_group');
20949
+ }
20950
+ parsedLevel = result.data;
20951
+ }
20952
+ const conditions = [eq21(adsInsightsDaily.projectId, project.id)];
20953
+ if (parsedLevel) conditions.push(eq21(adsInsightsDaily.level, parsedLevel));
20954
+ if (entityId) conditions.push(eq21(adsInsightsDaily.entityId, entityId));
20955
+ if (from) conditions.push(gte3(adsInsightsDaily.date, from));
20956
+ if (to) conditions.push(lte2(adsInsightsDaily.date, to));
20957
+ const rows = app.db.select().from(adsInsightsDaily).where(and14(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
20958
+ const dtoRows = rows.map((row) => ({
20959
+ level: row.level,
20960
+ entityId: row.entityId,
20961
+ date: row.date,
20962
+ impressions: row.impressions,
20963
+ clicks: row.clicks,
20964
+ spendMicros: row.spendMicros,
20965
+ ctr: adsCtr(row.clicks, row.impressions),
20966
+ cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
20967
+ }));
20968
+ const conn = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20969
+ const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
20970
+ return response;
20971
+ });
20972
+ app.get("/projects/:name/ads/summary", async (request) => {
20973
+ const project = resolveProject(app.db, request.params.name);
20974
+ const row = app.db.select().from(adsConnections).where(eq21(adsConnections.projectId, project.id)).get();
20975
+ const campaignCount = app.db.select().from(adsCampaigns).where(eq21(adsCampaigns.projectId, project.id)).all().length;
20976
+ const adGroupCount = app.db.select().from(adsAdGroups).where(eq21(adsAdGroups.projectId, project.id)).all().length;
20977
+ const adCount = app.db.select().from(adsAds).where(eq21(adsAds.projectId, project.id)).all().length;
20978
+ const campaignInsights = app.db.select().from(adsInsightsDaily).where(and14(
20979
+ eq21(adsInsightsDaily.projectId, project.id),
20980
+ eq21(adsInsightsDaily.level, "campaign")
20981
+ )).all();
20982
+ let impressions = 0;
20983
+ let clicks = 0;
20984
+ let spendMicros = 0;
20985
+ let fromDate = null;
20986
+ let toDate = null;
20987
+ for (const insight of campaignInsights) {
20988
+ impressions += insight.impressions;
20989
+ clicks += insight.clicks;
20990
+ spendMicros += insight.spendMicros;
20991
+ if (fromDate === null || insight.date < fromDate) fromDate = insight.date;
20992
+ if (toDate === null || insight.date > toDate) toDate = insight.date;
20993
+ }
20994
+ const response = {
20995
+ connected: Boolean(row),
20996
+ displayName: row?.displayName ?? null,
20997
+ currencyCode: row?.currencyCode ?? null,
20998
+ lastSyncedAt: row?.lastSyncedAt ?? null,
20999
+ campaignCount,
21000
+ adGroupCount,
21001
+ adCount,
21002
+ window: { from: fromDate, to: toDate },
21003
+ totals: {
21004
+ impressions,
21005
+ clicks,
21006
+ spendMicros,
21007
+ ctr: adsCtr(clicks, impressions),
21008
+ cpcMicros: adsCpcMicros(spendMicros, clicks)
21009
+ }
21010
+ };
21011
+ return response;
21012
+ });
21013
+ }
21014
+
21015
+ // ../api-routes/src/bing.ts
21016
+ import crypto19 from "crypto";
21017
+ import { eq as eq22, and as and15, desc as desc11 } from "drizzle-orm";
20357
21018
 
20358
21019
  // ../integration-bing/src/constants.ts
20359
21020
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
20360
21021
  var BING_SUBMIT_URL_BATCH_LIMIT = 500;
20361
21022
  var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
20362
21023
  var BING_REQUEST_TIMEOUT_MS = 3e4;
21024
+ var BING_LINKS_MAX_PAGES = 20;
20363
21025
 
20364
21026
  // ../integration-bing/src/types.ts
20365
21027
  var BingApiError = class extends Error {
@@ -20523,6 +21185,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
20523
21185
  const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
20524
21186
  return data ?? [];
20525
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
+ }
20526
21233
 
20527
21234
  // ../api-routes/src/bing.ts
20528
21235
  function parseBingDate(value) {
@@ -20681,7 +21388,7 @@ async function bingRoutes(app, opts) {
20681
21388
  const store = requireConnectionStore();
20682
21389
  const project = resolveProject(app.db, request.params.name);
20683
21390
  requireConnection(store, project.canonicalDomain);
20684
- const allInspections = app.db.select().from(bingUrlInspections).where(eq21(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
21391
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
20685
21392
  const latestByUrl = /* @__PURE__ */ new Map();
20686
21393
  const definitiveByUrl = /* @__PURE__ */ new Map();
20687
21394
  for (const row of allInspections) {
@@ -20738,7 +21445,7 @@ async function bingRoutes(app, opts) {
20738
21445
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
20739
21446
  const now = (/* @__PURE__ */ new Date()).toISOString();
20740
21447
  app.db.insert(bingCoverageSnapshots).values({
20741
- id: crypto18.randomUUID(),
21448
+ id: crypto19.randomUUID(),
20742
21449
  projectId: project.id,
20743
21450
  syncRunId: snapshotRunId,
20744
21451
  date: snapshotDate,
@@ -20770,7 +21477,7 @@ async function bingRoutes(app, opts) {
20770
21477
  const project = resolveProject(app.db, request.params.name);
20771
21478
  const parsed = parseInt(request.query.limit ?? "90", 10);
20772
21479
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
20773
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq21(bingCoverageSnapshots.projectId, project.id)).orderBy(desc11(bingCoverageSnapshots.date)).limit(limit).all();
21480
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq22(bingCoverageSnapshots.projectId, project.id)).orderBy(desc11(bingCoverageSnapshots.date)).limit(limit).all();
20774
21481
  return rows.map((r) => ({
20775
21482
  date: r.date,
20776
21483
  indexed: r.indexed,
@@ -20782,7 +21489,7 @@ async function bingRoutes(app, opts) {
20782
21489
  requireConnectionStore();
20783
21490
  const project = resolveProject(app.db, request.params.name);
20784
21491
  const { url, limit } = request.query;
20785
- const whereClause = url ? and14(eq21(bingUrlInspections.projectId, project.id), eq21(bingUrlInspections.url, url)) : eq21(bingUrlInspections.projectId, project.id);
21492
+ const whereClause = url ? and15(eq22(bingUrlInspections.projectId, project.id), eq22(bingUrlInspections.url, url)) : eq22(bingUrlInspections.projectId, project.id);
20786
21493
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc11(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
20787
21494
  return filtered.map((r) => ({
20788
21495
  id: r.id,
@@ -20809,7 +21516,7 @@ async function bingRoutes(app, opts) {
20809
21516
  throw validationError("url is required");
20810
21517
  }
20811
21518
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
20812
- const runId = crypto18.randomUUID();
21519
+ const runId = crypto19.randomUUID();
20813
21520
  app.db.insert(runs).values({
20814
21521
  id: runId,
20815
21522
  projectId: project.id,
@@ -20830,7 +21537,7 @@ async function bingRoutes(app, opts) {
20830
21537
  discoveryDate: result.DiscoveryDate ?? null
20831
21538
  });
20832
21539
  const now = (/* @__PURE__ */ new Date()).toISOString();
20833
- const id = crypto18.randomUUID();
21540
+ const id = crypto19.randomUUID();
20834
21541
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
20835
21542
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
20836
21543
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -20872,7 +21579,7 @@ async function bingRoutes(app, opts) {
20872
21579
  anchorCount: result.AnchorCount ?? null,
20873
21580
  discoveryDate
20874
21581
  }).run();
20875
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq21(runs.id, runId)).run();
21582
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq22(runs.id, runId)).run();
20876
21583
  return {
20877
21584
  id,
20878
21585
  url,
@@ -20888,7 +21595,7 @@ async function bingRoutes(app, opts) {
20888
21595
  } catch (e) {
20889
21596
  const msg = e instanceof Error ? e.message : String(e);
20890
21597
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
20891
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
21598
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
20892
21599
  throw e;
20893
21600
  }
20894
21601
  });
@@ -20900,7 +21607,7 @@ async function bingRoutes(app, opts) {
20900
21607
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
20901
21608
  }
20902
21609
  const now = (/* @__PURE__ */ new Date()).toISOString();
20903
- const runId = crypto18.randomUUID();
21610
+ const runId = crypto19.randomUUID();
20904
21611
  app.db.insert(runs).values({
20905
21612
  id: runId,
20906
21613
  projectId: project.id,
@@ -20915,7 +21622,7 @@ async function bingRoutes(app, opts) {
20915
21622
  } else {
20916
21623
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
20917
21624
  }
20918
- const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
21625
+ const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
20919
21626
  return run;
20920
21627
  });
20921
21628
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -20927,7 +21634,7 @@ async function bingRoutes(app, opts) {
20927
21634
  }
20928
21635
  let urlsToSubmit = request.body?.urls ?? [];
20929
21636
  if (request.body?.allUnindexed) {
20930
- const allInspections = app.db.select().from(bingUrlInspections).where(eq21(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
21637
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, project.id)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
20931
21638
  const latestByUrl = /* @__PURE__ */ new Map();
20932
21639
  for (const row of allInspections) {
20933
21640
  if (!latestByUrl.has(row.url)) {
@@ -21014,14 +21721,14 @@ async function bingRoutes(app, opts) {
21014
21721
  import fs from "fs";
21015
21722
  import path from "path";
21016
21723
  import os from "os";
21017
- import { eq as eq22, and as and15 } from "drizzle-orm";
21724
+ import { eq as eq23, and as and16 } from "drizzle-orm";
21018
21725
  function getScreenshotDir() {
21019
21726
  return path.join(os.homedir(), ".canonry", "screenshots");
21020
21727
  }
21021
21728
  async function cdpRoutes(app, opts) {
21022
21729
  app.get("/screenshots/:snapshotId", async (request, reply) => {
21023
21730
  const { snapshotId } = request.params;
21024
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq22(querySnapshots.id, snapshotId)).get();
21731
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq23(querySnapshots.id, snapshotId)).get();
21025
21732
  if (!snapshot?.screenshotPath) {
21026
21733
  const err = notFound("Screenshot", snapshotId);
21027
21734
  return reply.code(err.statusCode).send(err.toJSON());
@@ -21088,7 +21795,7 @@ async function cdpRoutes(app, opts) {
21088
21795
  async (request, reply) => {
21089
21796
  const project = resolveProject(app.db, request.params.name);
21090
21797
  const { runId } = request.params;
21091
- const run = app.db.select().from(runs).where(and15(eq22(runs.id, runId), eq22(runs.projectId, project.id))).get();
21798
+ const run = app.db.select().from(runs).where(and16(eq23(runs.id, runId), eq23(runs.projectId, project.id))).get();
21092
21799
  if (!run) {
21093
21800
  const err = notFound("Run", runId);
21094
21801
  return reply.code(err.statusCode).send(err.toJSON());
@@ -21101,8 +21808,8 @@ async function cdpRoutes(app, opts) {
21101
21808
  citedDomains: querySnapshots.citedDomains,
21102
21809
  screenshotPath: querySnapshots.screenshotPath,
21103
21810
  rawResponse: querySnapshots.rawResponse
21104
- }).from(querySnapshots).where(eq22(querySnapshots.runId, runId)).all());
21105
- const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq22(queries.projectId, project.id)).all();
21811
+ }).from(querySnapshots).where(eq23(querySnapshots.runId, runId)).all());
21812
+ const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq23(queries.projectId, project.id)).all();
21106
21813
  const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
21107
21814
  const byQuery = /* @__PURE__ */ new Map();
21108
21815
  for (const snap of snapshots) {
@@ -21184,8 +21891,8 @@ async function cdpRoutes(app, opts) {
21184
21891
  }
21185
21892
 
21186
21893
  // ../api-routes/src/ga.ts
21187
- import crypto19 from "crypto";
21188
- import { eq as eq23, desc as desc12, and as and16, sql as sql9 } from "drizzle-orm";
21894
+ import crypto20 from "crypto";
21895
+ import { eq as eq24, desc as desc12, and as and17, sql as sql9 } from "drizzle-orm";
21189
21896
  function gaLog(level, action, ctx) {
21190
21897
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
21191
21898
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -21392,10 +22099,10 @@ async function ga4Routes(app, opts) {
21392
22099
  if (!saConn && !oauthConn) {
21393
22100
  throw notFound("GA4 connection", project.name);
21394
22101
  }
21395
- app.db.delete(gaTrafficSnapshots).where(eq23(gaTrafficSnapshots.projectId, project.id)).run();
21396
- app.db.delete(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).run();
21397
- app.db.delete(gaAiReferrals).where(eq23(gaAiReferrals.projectId, project.id)).run();
21398
- app.db.delete(gaSocialReferrals).where(eq23(gaSocialReferrals.projectId, project.id)).run();
22102
+ app.db.delete(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).run();
22103
+ app.db.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
22104
+ app.db.delete(gaAiReferrals).where(eq24(gaAiReferrals.projectId, project.id)).run();
22105
+ app.db.delete(gaSocialReferrals).where(eq24(gaSocialReferrals.projectId, project.id)).run();
21399
22106
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
21400
22107
  opts.ga4CredentialStore?.deleteConnection(project.name);
21401
22108
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -21416,7 +22123,7 @@ async function ga4Routes(app, opts) {
21416
22123
  if (!connected) {
21417
22124
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
21418
22125
  }
21419
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
22126
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
21420
22127
  return {
21421
22128
  connected: true,
21422
22129
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -21440,7 +22147,7 @@ async function ga4Routes(app, opts) {
21440
22147
  const syncAi = !only || only === "ai";
21441
22148
  const syncSocial = !only || only === "social";
21442
22149
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21443
- const runId = crypto19.randomUUID();
22150
+ const runId = crypto20.randomUUID();
21444
22151
  app.db.insert(runs).values({
21445
22152
  id: runId,
21446
22153
  projectId: project.id,
@@ -21480,15 +22187,15 @@ async function ga4Routes(app, opts) {
21480
22187
  app.db.transaction((tx) => {
21481
22188
  if (syncTraffic) {
21482
22189
  tx.delete(gaTrafficSnapshots).where(
21483
- and16(
21484
- eq23(gaTrafficSnapshots.projectId, project.id),
22190
+ and17(
22191
+ eq24(gaTrafficSnapshots.projectId, project.id),
21485
22192
  sql9`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
21486
22193
  sql9`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
21487
22194
  )
21488
22195
  ).run();
21489
22196
  for (const row of rows) {
21490
22197
  tx.insert(gaTrafficSnapshots).values({
21491
- id: crypto19.randomUUID(),
22198
+ id: crypto20.randomUUID(),
21492
22199
  projectId: project.id,
21493
22200
  date: row.date,
21494
22201
  landingPage: row.landingPage,
@@ -21504,15 +22211,15 @@ async function ga4Routes(app, opts) {
21504
22211
  }
21505
22212
  if (syncAi) {
21506
22213
  tx.delete(gaAiReferrals).where(
21507
- and16(
21508
- eq23(gaAiReferrals.projectId, project.id),
22214
+ and17(
22215
+ eq24(gaAiReferrals.projectId, project.id),
21509
22216
  sql9`${gaAiReferrals.date} >= ${summary.periodStart}`,
21510
22217
  sql9`${gaAiReferrals.date} <= ${summary.periodEnd}`
21511
22218
  )
21512
22219
  ).run();
21513
22220
  for (const row of aiReferrals) {
21514
22221
  tx.insert(gaAiReferrals).values({
21515
- id: crypto19.randomUUID(),
22222
+ id: crypto20.randomUUID(),
21516
22223
  projectId: project.id,
21517
22224
  date: row.date,
21518
22225
  source: row.source,
@@ -21530,15 +22237,15 @@ async function ga4Routes(app, opts) {
21530
22237
  }
21531
22238
  if (syncSocial) {
21532
22239
  tx.delete(gaSocialReferrals).where(
21533
- and16(
21534
- eq23(gaSocialReferrals.projectId, project.id),
22240
+ and17(
22241
+ eq24(gaSocialReferrals.projectId, project.id),
21535
22242
  sql9`${gaSocialReferrals.date} >= ${summary.periodStart}`,
21536
22243
  sql9`${gaSocialReferrals.date} <= ${summary.periodEnd}`
21537
22244
  )
21538
22245
  ).run();
21539
22246
  for (const row of socialReferrals) {
21540
22247
  tx.insert(gaSocialReferrals).values({
21541
- id: crypto19.randomUUID(),
22248
+ id: crypto20.randomUUID(),
21542
22249
  projectId: project.id,
21543
22250
  date: row.date,
21544
22251
  source: row.source,
@@ -21552,9 +22259,9 @@ async function ga4Routes(app, opts) {
21552
22259
  }
21553
22260
  }
21554
22261
  if (syncSummary) {
21555
- tx.delete(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).run();
22262
+ tx.delete(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).run();
21556
22263
  tx.insert(gaTrafficSummaries).values({
21557
- id: crypto19.randomUUID(),
22264
+ id: crypto20.randomUUID(),
21558
22265
  projectId: project.id,
21559
22266
  periodStart: summary.periodStart,
21560
22267
  periodEnd: summary.periodEnd,
@@ -21564,10 +22271,10 @@ async function ga4Routes(app, opts) {
21564
22271
  syncedAt: now,
21565
22272
  syncRunId: runId
21566
22273
  }).run();
21567
- tx.delete(gaTrafficWindowSummaries).where(eq23(gaTrafficWindowSummaries.projectId, project.id)).run();
22274
+ tx.delete(gaTrafficWindowSummaries).where(eq24(gaTrafficWindowSummaries.projectId, project.id)).run();
21568
22275
  for (const ws of windowSummaries) {
21569
22276
  tx.insert(gaTrafficWindowSummaries).values({
21570
- id: crypto19.randomUUID(),
22277
+ id: crypto20.randomUUID(),
21571
22278
  projectId: project.id,
21572
22279
  windowKey: ws.windowKey,
21573
22280
  periodStart: ws.periodStart,
@@ -21582,7 +22289,7 @@ async function ga4Routes(app, opts) {
21582
22289
  }
21583
22290
  }
21584
22291
  });
21585
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq23(runs.id, runId)).run();
22292
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq24(runs.id, runId)).run();
21586
22293
  const syncedComponents = only ? [
21587
22294
  ...syncTraffic ? ["traffic"] : [],
21588
22295
  ...syncSummary ? ["summary"] : [],
@@ -21611,7 +22318,7 @@ async function ga4Routes(app, opts) {
21611
22318
  } catch (e) {
21612
22319
  const msg = e instanceof Error ? e.message : String(e);
21613
22320
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
21614
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
22321
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
21615
22322
  throw e;
21616
22323
  }
21617
22324
  });
@@ -21622,11 +22329,11 @@ async function ga4Routes(app, opts) {
21622
22329
  const window = parseWindow(request.query.window);
21623
22330
  const cutoff = windowCutoff(window);
21624
22331
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
21625
- const snapshotConditions = [eq23(gaTrafficSnapshots.projectId, project.id)];
22332
+ const snapshotConditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
21626
22333
  if (cutoffDate) snapshotConditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
21627
- const aiConditions = [eq23(gaAiReferrals.projectId, project.id)];
22334
+ const aiConditions = [eq24(gaAiReferrals.projectId, project.id)];
21628
22335
  if (cutoffDate) aiConditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
21629
- const socialConditions = [eq23(gaSocialReferrals.projectId, project.id)];
22336
+ const socialConditions = [eq24(gaSocialReferrals.projectId, project.id)];
21630
22337
  if (cutoffDate) socialConditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
21631
22338
  const windowSummaryRow = cutoffDate ? app.db.select({
21632
22339
  totalSessions: gaTrafficWindowSummaries.totalSessions,
@@ -21634,42 +22341,42 @@ async function ga4Routes(app, opts) {
21634
22341
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
21635
22342
  totalUsers: gaTrafficWindowSummaries.totalUsers
21636
22343
  }).from(gaTrafficWindowSummaries).where(
21637
- and16(
21638
- eq23(gaTrafficWindowSummaries.projectId, project.id),
21639
- eq23(gaTrafficWindowSummaries.windowKey, window)
22344
+ and17(
22345
+ eq24(gaTrafficWindowSummaries.projectId, project.id),
22346
+ eq24(gaTrafficWindowSummaries.windowKey, window)
21640
22347
  )
21641
22348
  ).get() : null;
21642
22349
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
21643
22350
  totalSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
21644
22351
  totalOrganicSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
21645
22352
  totalUsers: sql9`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
21646
- }).from(gaTrafficSnapshots).where(and16(...snapshotConditions)).get() : null;
22353
+ }).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get() : null;
21647
22354
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
21648
22355
  totalSessions: gaTrafficSummaries.totalSessions,
21649
22356
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
21650
22357
  totalUsers: gaTrafficSummaries.totalUsers
21651
- }).from(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).get();
22358
+ }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
21652
22359
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
21653
22360
  totalDirectSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
21654
- }).from(gaTrafficSnapshots).where(and16(...snapshotConditions)).get();
22361
+ }).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).get();
21655
22362
  const summaryMeta = app.db.select({
21656
22363
  periodStart: gaTrafficSummaries.periodStart,
21657
22364
  periodEnd: gaTrafficSummaries.periodEnd
21658
- }).from(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).get();
22365
+ }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).get();
21659
22366
  const rows = app.db.select({
21660
22367
  landingPage: sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
21661
22368
  sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
21662
22369
  organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
21663
22370
  directSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
21664
22371
  users: sql9`SUM(${gaTrafficSnapshots.users})`
21665
- }).from(gaTrafficSnapshots).where(and16(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
22372
+ }).from(gaTrafficSnapshots).where(and17(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
21666
22373
  const aiReferralRows = app.db.select({
21667
22374
  source: gaAiReferrals.source,
21668
22375
  medium: gaAiReferrals.medium,
21669
22376
  sourceDimension: gaAiReferrals.sourceDimension,
21670
22377
  sessions: sql9`SUM(${gaAiReferrals.sessions})`,
21671
22378
  users: sql9`SUM(${gaAiReferrals.users})`
21672
- }).from(gaAiReferrals).where(and16(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
22379
+ }).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
21673
22380
  const aiReferralLandingPageRows = app.db.select({
21674
22381
  source: gaAiReferrals.source,
21675
22382
  medium: gaAiReferrals.medium,
@@ -21677,7 +22384,7 @@ async function ga4Routes(app, opts) {
21677
22384
  landingPage: sql9`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
21678
22385
  sessions: sql9`SUM(${gaAiReferrals.sessions})`,
21679
22386
  users: sql9`SUM(${gaAiReferrals.users})`
21680
- }).from(gaAiReferrals).where(and16(...aiConditions)).groupBy(
22387
+ }).from(gaAiReferrals).where(and17(...aiConditions)).groupBy(
21681
22388
  gaAiReferrals.source,
21682
22389
  gaAiReferrals.medium,
21683
22390
  gaAiReferrals.sourceDimension,
@@ -21714,7 +22421,7 @@ async function ga4Routes(app, opts) {
21714
22421
  channelGroup: gaAiReferrals.channelGroup,
21715
22422
  sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
21716
22423
  users: sql9`COALESCE(SUM(${gaAiReferrals.users}), 0)`
21717
- }).from(gaAiReferrals).where(and16(...aiConditions, eq23(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
22424
+ }).from(gaAiReferrals).where(and17(...aiConditions, eq24(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
21718
22425
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
21719
22426
  let aiBySessionUsers = 0;
21720
22427
  for (const row of aiBySessionRows) {
@@ -21728,12 +22435,12 @@ async function ga4Routes(app, opts) {
21728
22435
  channelGroup: gaSocialReferrals.channelGroup,
21729
22436
  sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
21730
22437
  users: sql9`SUM(${gaSocialReferrals.users})`
21731
- }).from(gaSocialReferrals).where(and16(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
22438
+ }).from(gaSocialReferrals).where(and17(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
21732
22439
  const socialTotals = app.db.select({
21733
22440
  sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
21734
22441
  users: sql9`SUM(${gaSocialReferrals.users})`
21735
- }).from(gaSocialReferrals).where(and16(...socialConditions)).get();
21736
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq23(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
22442
+ }).from(gaSocialReferrals).where(and17(...socialConditions)).get();
22443
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq24(gaTrafficSummaries.projectId, project.id)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
21737
22444
  const total = summaryRow?.totalSessions ?? 0;
21738
22445
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
21739
22446
  const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
@@ -21813,7 +22520,7 @@ async function ga4Routes(app, opts) {
21813
22520
  const project = resolveProject(app.db, request.params.name);
21814
22521
  requireGa4Connection(opts, project.name, project.canonicalDomain);
21815
22522
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
21816
- const conditions = [eq23(gaAiReferrals.projectId, project.id)];
22523
+ const conditions = [eq24(gaAiReferrals.projectId, project.id)];
21817
22524
  if (cutoffDate) conditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
21818
22525
  const rows = app.db.select({
21819
22526
  date: gaAiReferrals.date,
@@ -21823,7 +22530,7 @@ async function ga4Routes(app, opts) {
21823
22530
  sourceDimension: gaAiReferrals.sourceDimension,
21824
22531
  sessions: sql9`SUM(${gaAiReferrals.sessions})`,
21825
22532
  users: sql9`SUM(${gaAiReferrals.users})`
21826
- }).from(gaAiReferrals).where(and16(...conditions)).groupBy(
22533
+ }).from(gaAiReferrals).where(and17(...conditions)).groupBy(
21827
22534
  gaAiReferrals.date,
21828
22535
  gaAiReferrals.source,
21829
22536
  gaAiReferrals.medium,
@@ -21836,7 +22543,7 @@ async function ga4Routes(app, opts) {
21836
22543
  const project = resolveProject(app.db, request.params.name);
21837
22544
  requireGa4Connection(opts, project.name, project.canonicalDomain);
21838
22545
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
21839
- const conditions = [eq23(gaSocialReferrals.projectId, project.id)];
22546
+ const conditions = [eq24(gaSocialReferrals.projectId, project.id)];
21840
22547
  if (cutoffDate) conditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
21841
22548
  const rows = app.db.select({
21842
22549
  date: gaSocialReferrals.date,
@@ -21845,7 +22552,7 @@ async function ga4Routes(app, opts) {
21845
22552
  channelGroup: gaSocialReferrals.channelGroup,
21846
22553
  sessions: gaSocialReferrals.sessions,
21847
22554
  users: gaSocialReferrals.users
21848
- }).from(gaSocialReferrals).where(and16(...conditions)).orderBy(gaSocialReferrals.date).all();
22555
+ }).from(gaSocialReferrals).where(and17(...conditions)).orderBy(gaSocialReferrals.date).all();
21849
22556
  return rows;
21850
22557
  });
21851
22558
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -21858,8 +22565,8 @@ async function ga4Routes(app, opts) {
21858
22565
  d.setDate(d.getDate() - n);
21859
22566
  return fmt(d);
21860
22567
  };
21861
- const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and16(
21862
- eq23(gaSocialReferrals.projectId, project.id),
22568
+ const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and17(
22569
+ eq24(gaSocialReferrals.projectId, project.id),
21863
22570
  sql9`${gaSocialReferrals.date} >= ${from}`,
21864
22571
  sql9`${gaSocialReferrals.date} < ${to}`
21865
22572
  )).get();
@@ -21871,16 +22578,16 @@ async function ga4Routes(app, opts) {
21871
22578
  const sourceCurrent = app.db.select({
21872
22579
  source: gaSocialReferrals.source,
21873
22580
  sessions: sql9`SUM(${gaSocialReferrals.sessions})`
21874
- }).from(gaSocialReferrals).where(and16(
21875
- eq23(gaSocialReferrals.projectId, project.id),
22581
+ }).from(gaSocialReferrals).where(and17(
22582
+ eq24(gaSocialReferrals.projectId, project.id),
21876
22583
  sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
21877
22584
  sql9`${gaSocialReferrals.date} < ${fmt(today)}`
21878
22585
  )).groupBy(gaSocialReferrals.source).all();
21879
22586
  const sourcePrev = app.db.select({
21880
22587
  source: gaSocialReferrals.source,
21881
22588
  sessions: sql9`SUM(${gaSocialReferrals.sessions})`
21882
- }).from(gaSocialReferrals).where(and16(
21883
- eq23(gaSocialReferrals.projectId, project.id),
22589
+ }).from(gaSocialReferrals).where(and17(
22590
+ eq24(gaSocialReferrals.projectId, project.id),
21884
22591
  sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
21885
22592
  sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`
21886
22593
  )).groupBy(gaSocialReferrals.source).all();
@@ -21921,16 +22628,16 @@ async function ga4Routes(app, opts) {
21921
22628
  return fmt(d);
21922
22629
  };
21923
22630
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
21924
- const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and16(eq23(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
21925
- const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and16(eq23(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
21926
- const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and16(eq23(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
21927
- const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
21928
- eq23(gaAiReferrals.projectId, project.id),
22631
+ const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
22632
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
22633
+ const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and17(eq24(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
22634
+ const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
22635
+ eq24(gaAiReferrals.projectId, project.id),
21929
22636
  sql9`${gaAiReferrals.date} >= ${from}`,
21930
22637
  sql9`${gaAiReferrals.date} < ${to}`,
21931
- eq23(gaAiReferrals.sourceDimension, "session")
22638
+ eq24(gaAiReferrals.sourceDimension, "session")
21932
22639
  )).get();
21933
- const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and16(eq23(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${from}`, sql9`${gaSocialReferrals.date} < ${to}`)).get();
22640
+ const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${from}`, sql9`${gaSocialReferrals.date} < ${to}`)).get();
21934
22641
  const todayStr = fmt(today);
21935
22642
  const buildTrend = (sum) => {
21936
22643
  const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
@@ -21939,17 +22646,17 @@ async function ga4Routes(app, opts) {
21939
22646
  const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
21940
22647
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
21941
22648
  };
21942
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
21943
- eq23(gaAiReferrals.projectId, project.id),
22649
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
22650
+ eq24(gaAiReferrals.projectId, project.id),
21944
22651
  sql9`${gaAiReferrals.date} >= ${daysAgo(7)}`,
21945
22652
  sql9`${gaAiReferrals.date} < ${todayStr}`,
21946
- eq23(gaAiReferrals.sourceDimension, "session")
22653
+ eq24(gaAiReferrals.sourceDimension, "session")
21947
22654
  )).groupBy(gaAiReferrals.source).all();
21948
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
21949
- eq23(gaAiReferrals.projectId, project.id),
22655
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and17(
22656
+ eq24(gaAiReferrals.projectId, project.id),
21950
22657
  sql9`${gaAiReferrals.date} >= ${daysAgo(14)}`,
21951
22658
  sql9`${gaAiReferrals.date} < ${daysAgo(7)}`,
21952
- eq23(gaAiReferrals.sourceDimension, "session")
22659
+ eq24(gaAiReferrals.sourceDimension, "session")
21953
22660
  )).groupBy(gaAiReferrals.source).all();
21954
22661
  const findBiggestMover = (current, prev) => {
21955
22662
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
@@ -21965,8 +22672,8 @@ async function ga4Routes(app, opts) {
21965
22672
  }
21966
22673
  return mover;
21967
22674
  };
21968
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq23(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql9`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
21969
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq23(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
22675
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql9`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
22676
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and17(eq24(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
21970
22677
  return {
21971
22678
  total: buildTrend(sumTotal),
21972
22679
  organic: buildTrend(sumOrganic),
@@ -21981,14 +22688,14 @@ async function ga4Routes(app, opts) {
21981
22688
  const project = resolveProject(app.db, request.params.name);
21982
22689
  requireGa4Connection(opts, project.name, project.canonicalDomain);
21983
22690
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
21984
- const conditions = [eq23(gaTrafficSnapshots.projectId, project.id)];
22691
+ const conditions = [eq24(gaTrafficSnapshots.projectId, project.id)];
21985
22692
  if (cutoffDate) conditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
21986
22693
  const rows = app.db.select({
21987
22694
  date: gaTrafficSnapshots.date,
21988
22695
  sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
21989
22696
  organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
21990
22697
  users: sql9`SUM(${gaTrafficSnapshots.users})`
21991
- }).from(gaTrafficSnapshots).where(and16(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
22698
+ }).from(gaTrafficSnapshots).where(and17(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
21992
22699
  return rows.map((r) => ({
21993
22700
  date: r.date,
21994
22701
  sessions: r.sessions ?? 0,
@@ -22004,7 +22711,7 @@ async function ga4Routes(app, opts) {
22004
22711
  sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
22005
22712
  organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
22006
22713
  users: sql9`SUM(${gaTrafficSnapshots.users})`
22007
- }).from(gaTrafficSnapshots).where(eq23(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
22714
+ }).from(gaTrafficSnapshots).where(eq24(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
22008
22715
  return {
22009
22716
  pages: trafficPages.map((r) => ({
22010
22717
  landingPage: r.landingPage,
@@ -22138,7 +22845,7 @@ function parseSchemaPageEntry(entry) {
22138
22845
  }
22139
22846
 
22140
22847
  // ../integration-wordpress/src/wordpress-client.ts
22141
- import crypto20 from "crypto";
22848
+ import crypto21 from "crypto";
22142
22849
  function validateUsername(username) {
22143
22850
  if (!username || typeof username !== "string" || username.trim().length === 0) {
22144
22851
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -22351,7 +23058,7 @@ function buildSnippet(content) {
22351
23058
  return `${text2.slice(0, 157)}...`;
22352
23059
  }
22353
23060
  function contentHash(content) {
22354
- return crypto20.createHash("sha256").update(content).digest("hex");
23061
+ return crypto21.createHash("sha256").update(content).digest("hex");
22355
23062
  }
22356
23063
  function buildAmbiguousSlugMessage(slug, pages) {
22357
23064
  const candidates = pages.map((page) => {
@@ -23651,8 +24358,8 @@ async function wordpressRoutes(app, opts) {
23651
24358
  }
23652
24359
 
23653
24360
  // ../api-routes/src/backlinks.ts
23654
- import crypto21 from "crypto";
23655
- import { and as and18, asc as asc2, desc as desc13, eq as eq24, sql as sql10 } from "drizzle-orm";
24361
+ import crypto22 from "crypto";
24362
+ import { and as and19, asc as asc3, desc as desc13, eq as eq25, sql as sql10 } from "drizzle-orm";
23656
24363
 
23657
24364
  // ../integration-commoncrawl/src/constants.ts
23658
24365
  import os2 from "os";
@@ -24055,7 +24762,7 @@ function pruneCachedRelease(release, opts = {}) {
24055
24762
  }
24056
24763
 
24057
24764
  // ../api-routes/src/backlinks-filter.ts
24058
- import { and as and17, ne as ne3, notLike } from "drizzle-orm";
24765
+ import { and as and18, ne as ne3, notLike } from "drizzle-orm";
24059
24766
  var BACKLINK_FILTER_PATTERNS = [
24060
24767
  "*.google.com",
24061
24768
  "*.googleusercontent.com",
@@ -24078,7 +24785,7 @@ function backlinkCrawlerExclusionClause() {
24078
24785
  conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
24079
24786
  }
24080
24787
  }
24081
- const combined = and17(...conditions);
24788
+ const combined = and18(...conditions);
24082
24789
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
24083
24790
  return combined;
24084
24791
  }
@@ -24121,9 +24828,18 @@ function mapSummaryRow(row) {
24121
24828
  totalLinkingDomains: row.totalLinkingDomains,
24122
24829
  totalHosts: row.totalHosts,
24123
24830
  top10HostsShare: row.top10HostsShare,
24124
- queriedAt: row.queriedAt
24831
+ queriedAt: row.queriedAt,
24832
+ source: row.source
24125
24833
  };
24126
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
+ }
24127
24843
  function mapRunRow(row) {
24128
24844
  return {
24129
24845
  id: row.id,
@@ -24138,9 +24854,13 @@ function mapRunRow(row) {
24138
24854
  createdAt: row.createdAt
24139
24855
  };
24140
24856
  }
24141
- function latestSummaryForProject(db, projectId, release) {
24142
- const condition = release ? and18(eq24(backlinkSummaries.projectId, projectId), eq24(backlinkSummaries.release, release)) : eq24(backlinkSummaries.projectId, projectId);
24143
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
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();
24144
24864
  }
24145
24865
  function parseExcludeCrawlers(value) {
24146
24866
  if (!value) return false;
@@ -24148,11 +24868,12 @@ function parseExcludeCrawlers(value) {
24148
24868
  return lower === "1" || lower === "true" || lower === "yes";
24149
24869
  }
24150
24870
  function computeFilteredSummary(db, base) {
24151
- const baseDomainCondition = and18(
24152
- eq24(backlinkDomains.projectId, base.projectId),
24153
- eq24(backlinkDomains.release, base.release)
24871
+ const baseDomainCondition = and19(
24872
+ eq25(backlinkDomains.projectId, base.projectId),
24873
+ eq25(backlinkDomains.source, base.source),
24874
+ eq25(backlinkDomains.release, base.release)
24154
24875
  );
24155
- const filteredCondition = and18(baseDomainCondition, backlinkCrawlerExclusionClause());
24876
+ const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
24156
24877
  const unfilteredAgg = db.select({
24157
24878
  count: sql10`count(*)`,
24158
24879
  total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -24176,10 +24897,48 @@ function computeFilteredSummary(db, base) {
24176
24897
  totalHosts,
24177
24898
  top10HostsShare: top10Share.toFixed(6),
24178
24899
  queriedAt: base.queriedAt,
24900
+ source: base.source,
24179
24901
  excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
24180
24902
  excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
24181
24903
  };
24182
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
+ }
24183
24942
  async function backlinksRoutes(app, opts) {
24184
24943
  app.get("/backlinks/status", async (_request, reply) => {
24185
24944
  if (!opts.getBacklinksStatus) {
@@ -24221,7 +24980,7 @@ async function backlinksRoutes(app, opts) {
24221
24980
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
24222
24981
  );
24223
24982
  }
24224
- const existing = app.db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.release, release)).get();
24983
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, release)).get();
24225
24984
  const now = (/* @__PURE__ */ new Date()).toISOString();
24226
24985
  if (existing) {
24227
24986
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -24232,12 +24991,12 @@ async function backlinksRoutes(app, opts) {
24232
24991
  phaseDetail: null,
24233
24992
  error: null,
24234
24993
  updatedAt: now
24235
- }).where(eq24(ccReleaseSyncs.id, existing.id)).run();
24994
+ }).where(eq25(ccReleaseSyncs.id, existing.id)).run();
24236
24995
  opts.onReleaseSyncRequested(existing.id, release);
24237
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.id, existing.id)).get();
24996
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, existing.id)).get();
24238
24997
  return reply.status(200).send(mapSyncRow(refreshed));
24239
24998
  }
24240
- const id = crypto21.randomUUID();
24999
+ const id = crypto22.randomUUID();
24241
25000
  app.db.insert(ccReleaseSyncs).values({
24242
25001
  id,
24243
25002
  release,
@@ -24246,7 +25005,7 @@ async function backlinksRoutes(app, opts) {
24246
25005
  updatedAt: now
24247
25006
  }).run();
24248
25007
  opts.onReleaseSyncRequested(id, release);
24249
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.id, id)).get();
25008
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.id, id)).get();
24250
25009
  return reply.status(201).send(mapSyncRow(inserted));
24251
25010
  });
24252
25011
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
@@ -24294,7 +25053,7 @@ async function backlinksRoutes(app, opts) {
24294
25053
  throw validationError("Invalid release id");
24295
25054
  }
24296
25055
  const now = (/* @__PURE__ */ new Date()).toISOString();
24297
- const runId = crypto21.randomUUID();
25056
+ const runId = crypto22.randomUUID();
24298
25057
  app.db.insert(runs).values({
24299
25058
  id: runId,
24300
25059
  projectId: project.id,
@@ -24304,14 +25063,15 @@ async function backlinksRoutes(app, opts) {
24304
25063
  createdAt: now
24305
25064
  }).run();
24306
25065
  opts.onBacklinkExtractRequested(runId, project.id, release);
24307
- const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
25066
+ const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
24308
25067
  return reply.status(201).send(mapRunRow(run));
24309
25068
  });
24310
25069
  app.get(
24311
25070
  "/projects/:name/backlinks/summary",
24312
25071
  async (request, reply) => {
24313
25072
  const project = resolveProject(app.db, request.params.name);
24314
- const row = latestSummaryForProject(app.db, project.id, request.query.release);
25073
+ const source = parseSourceParam(request.query.source);
25074
+ const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
24315
25075
  if (!row) return reply.send(null);
24316
25076
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24317
25077
  return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
@@ -24319,30 +25079,34 @@ async function backlinksRoutes(app, opts) {
24319
25079
  );
24320
25080
  app.get("/projects/:name/backlinks/domains", async (request, reply) => {
24321
25081
  const project = resolveProject(app.db, request.params.name);
24322
- const summaryRow = latestSummaryForProject(app.db, project.id, request.query.release);
25082
+ const source = parseSourceParam(request.query.source);
25083
+ const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
24323
25084
  const targetRelease = request.query.release ?? summaryRow?.release;
24324
25085
  if (!targetRelease) {
24325
- const response2 = { summary: null, total: 0, rows: [] };
25086
+ const response2 = { source, summary: null, total: 0, rows: [] };
24326
25087
  return reply.send(response2);
24327
25088
  }
24328
25089
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
24329
25090
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
24330
25091
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24331
- const baseDomainCondition = and18(
24332
- eq24(backlinkDomains.projectId, project.id),
24333
- eq24(backlinkDomains.release, targetRelease)
25092
+ const baseDomainCondition = and19(
25093
+ eq25(backlinkDomains.projectId, project.id),
25094
+ eq25(backlinkDomains.source, source),
25095
+ eq25(backlinkDomains.release, targetRelease)
24334
25096
  );
24335
- const domainCondition = excludeCrawlers ? and18(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
25097
+ const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
24336
25098
  const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
24337
25099
  const rows = app.db.select({
24338
25100
  linkingDomain: backlinkDomains.linkingDomain,
24339
- numHosts: backlinkDomains.numHosts
25101
+ numHosts: backlinkDomains.numHosts,
25102
+ source: backlinkDomains.source
24340
25103
  }).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
24341
25104
  let summary = null;
24342
25105
  if (summaryRow) {
24343
25106
  summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
24344
25107
  }
24345
25108
  const response = {
25109
+ source,
24346
25110
  summary,
24347
25111
  total: Number(totalRow?.count ?? 0),
24348
25112
  rows
@@ -24353,26 +25117,67 @@ async function backlinksRoutes(app, opts) {
24353
25117
  "/projects/:name/backlinks/history",
24354
25118
  async (request, reply) => {
24355
25119
  const project = resolveProject(app.db, request.params.name);
24356
- const rows = app.db.select().from(backlinkSummaries).where(eq24(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
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();
24357
25122
  const response = rows.map((r) => ({
24358
25123
  release: r.release,
24359
25124
  totalLinkingDomains: r.totalLinkingDomains,
24360
25125
  totalHosts: r.totalHosts,
24361
25126
  top10HostsShare: r.top10HostsShare,
24362
- queriedAt: r.queriedAt
25127
+ queriedAt: r.queriedAt,
25128
+ source: r.source
24363
25129
  }));
24364
25130
  return reply.send(response);
24365
25131
  }
24366
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
+ );
24367
25172
  }
24368
25173
 
24369
25174
  // ../api-routes/src/traffic.ts
24370
- import crypto23 from "crypto";
25175
+ import crypto24 from "crypto";
24371
25176
  import { Agent as UndiciAgent } from "undici";
24372
- import { and as and19, desc as desc14, eq as eq25, gte as gte3, lte as lte2, sql as sql11 } from "drizzle-orm";
25177
+ import { and as and20, desc as desc14, eq as eq26, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
24373
25178
 
24374
25179
  // ../integration-cloud-run/src/auth.ts
24375
- import crypto22 from "crypto";
25180
+ import crypto23 from "crypto";
24376
25181
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
24377
25182
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
24378
25183
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -24403,7 +25208,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
24403
25208
  const headerB64 = encode(header);
24404
25209
  const payloadB64 = encode(payload);
24405
25210
  const signingInput = `${headerB64}.${payloadB64}`;
24406
- const sign = crypto22.createSign("RSA-SHA256");
25211
+ const sign = crypto23.createSign("RSA-SHA256");
24407
25212
  sign.update(signingInput);
24408
25213
  const signature = sign.sign(privateKey, "base64url");
24409
25214
  return `${signingInput}.${signature}`;
@@ -28186,8 +28991,8 @@ async function runBackfillTask(options) {
28186
28991
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
28187
28992
  try {
28188
28993
  app.db.transaction((tx) => {
28189
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq25(runs.id, runId)).run();
28190
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq25(trafficSources.id, sourceRow.id)).run();
28994
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
28995
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
28191
28996
  });
28192
28997
  } catch {
28193
28998
  }
@@ -28202,7 +29007,7 @@ async function runBackfillTask(options) {
28202
29007
  if (allEvents.length === 0) {
28203
29008
  const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
28204
29009
  try {
28205
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq25(runs.id, runId)).run();
29010
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq26(runs.id, runId)).run();
28206
29011
  } catch {
28207
29012
  }
28208
29013
  return;
@@ -28224,31 +29029,31 @@ async function runBackfillTask(options) {
28224
29029
  try {
28225
29030
  app.db.transaction((tx) => {
28226
29031
  tx.delete(crawlerEventsHourly).where(
28227
- and19(
28228
- eq25(crawlerEventsHourly.sourceId, sourceRow.id),
28229
- gte3(crawlerEventsHourly.tsHour, windowStartIso),
28230
- lte2(crawlerEventsHourly.tsHour, windowEndIso)
29032
+ and20(
29033
+ eq26(crawlerEventsHourly.sourceId, sourceRow.id),
29034
+ gte4(crawlerEventsHourly.tsHour, windowStartIso),
29035
+ lte3(crawlerEventsHourly.tsHour, windowEndIso)
28231
29036
  )
28232
29037
  ).run();
28233
29038
  tx.delete(aiUserFetchEventsHourly).where(
28234
- and19(
28235
- eq25(aiUserFetchEventsHourly.sourceId, sourceRow.id),
28236
- gte3(aiUserFetchEventsHourly.tsHour, windowStartIso),
28237
- lte2(aiUserFetchEventsHourly.tsHour, windowEndIso)
29039
+ and20(
29040
+ eq26(aiUserFetchEventsHourly.sourceId, sourceRow.id),
29041
+ gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
29042
+ lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
28238
29043
  )
28239
29044
  ).run();
28240
29045
  tx.delete(aiReferralEventsHourly).where(
28241
- and19(
28242
- eq25(aiReferralEventsHourly.sourceId, sourceRow.id),
28243
- gte3(aiReferralEventsHourly.tsHour, windowStartIso),
28244
- lte2(aiReferralEventsHourly.tsHour, windowEndIso)
29046
+ and20(
29047
+ eq26(aiReferralEventsHourly.sourceId, sourceRow.id),
29048
+ gte4(aiReferralEventsHourly.tsHour, windowStartIso),
29049
+ lte3(aiReferralEventsHourly.tsHour, windowEndIso)
28245
29050
  )
28246
29051
  ).run();
28247
29052
  tx.delete(rawEventSamples).where(
28248
- and19(
28249
- eq25(rawEventSamples.sourceId, sourceRow.id),
28250
- gte3(rawEventSamples.ts, windowStartIso),
28251
- lte2(rawEventSamples.ts, windowEndIso)
29053
+ and20(
29054
+ eq26(rawEventSamples.sourceId, sourceRow.id),
29055
+ gte4(rawEventSamples.ts, windowStartIso),
29056
+ lte3(rawEventSamples.ts, windowEndIso)
28252
29057
  )
28253
29058
  ).run();
28254
29059
  for (const bucket of report.crawlerEventsHourly) {
@@ -28311,7 +29116,7 @@ async function runBackfillTask(options) {
28311
29116
  }
28312
29117
  })();
28313
29118
  tx.insert(rawEventSamples).values({
28314
- id: crypto23.randomUUID(),
29119
+ id: crypto24.randomUUID(),
28315
29120
  projectId: project.id,
28316
29121
  sourceId: sourceRow.id,
28317
29122
  ts: sample.observedAt,
@@ -28335,8 +29140,8 @@ async function runBackfillTask(options) {
28335
29140
  lastError: null,
28336
29141
  lastEventIds: newRingBuffer,
28337
29142
  updatedAt: finishedAt
28338
- }).where(eq25(trafficSources.id, sourceRow.id)).run();
28339
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
29143
+ }).where(eq26(trafficSources.id, sourceRow.id)).run();
29144
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
28340
29145
  });
28341
29146
  } catch (e) {
28342
29147
  markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -28418,7 +29223,7 @@ async function trafficRoutes(app, opts) {
28418
29223
  createdAt: existing?.createdAt ?? now,
28419
29224
  updatedAt: now
28420
29225
  });
28421
- const activeSource = app.db.select().from(trafficSources).where(eq25(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
29226
+ const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
28422
29227
  const config = {
28423
29228
  gcpProjectId,
28424
29229
  serviceName: serviceName ?? null,
@@ -28434,10 +29239,10 @@ async function trafficRoutes(app, opts) {
28434
29239
  lastError: null,
28435
29240
  configJson: config,
28436
29241
  updatedAt: now
28437
- }).where(eq25(trafficSources.id, activeSource.id)).run();
28438
- sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, activeSource.id)).get();
29242
+ }).where(eq26(trafficSources.id, activeSource.id)).run();
29243
+ sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
28439
29244
  } else {
28440
- const newId = crypto23.randomUUID();
29245
+ const newId = crypto24.randomUUID();
28441
29246
  app.db.insert(trafficSources).values({
28442
29247
  id: newId,
28443
29248
  projectId: project.id,
@@ -28452,7 +29257,7 @@ async function trafficRoutes(app, opts) {
28452
29257
  createdAt: now,
28453
29258
  updatedAt: now
28454
29259
  }).run();
28455
- sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, newId)).get();
29260
+ sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
28456
29261
  }
28457
29262
  writeAuditLog(app.db, {
28458
29263
  projectId: project.id,
@@ -28504,7 +29309,7 @@ async function trafficRoutes(app, opts) {
28504
29309
  createdAt: existing?.createdAt ?? now,
28505
29310
  updatedAt: now
28506
29311
  });
28507
- const activeSource = app.db.select().from(trafficSources).where(eq25(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
29312
+ const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
28508
29313
  const config = { baseUrl, username };
28509
29314
  const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
28510
29315
  let sourceRow;
@@ -28515,10 +29320,10 @@ async function trafficRoutes(app, opts) {
28515
29320
  lastError: null,
28516
29321
  configJson: config,
28517
29322
  updatedAt: now
28518
- }).where(eq25(trafficSources.id, activeSource.id)).run();
28519
- sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, activeSource.id)).get();
29323
+ }).where(eq26(trafficSources.id, activeSource.id)).run();
29324
+ sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
28520
29325
  } else {
28521
- const newId = crypto23.randomUUID();
29326
+ const newId = crypto24.randomUUID();
28522
29327
  app.db.insert(trafficSources).values({
28523
29328
  id: newId,
28524
29329
  projectId: project.id,
@@ -28533,7 +29338,7 @@ async function trafficRoutes(app, opts) {
28533
29338
  createdAt: now,
28534
29339
  updatedAt: now
28535
29340
  }).run();
28536
- sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, newId)).get();
29341
+ sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
28537
29342
  }
28538
29343
  writeAuditLog(app.db, {
28539
29344
  projectId: project.id,
@@ -28587,7 +29392,7 @@ async function trafficRoutes(app, opts) {
28587
29392
  createdAt: existing?.createdAt ?? now,
28588
29393
  updatedAt: now
28589
29394
  });
28590
- const activeSource = app.db.select().from(trafficSources).where(eq25(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
29395
+ const activeSource = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
28591
29396
  const config = { projectId, teamId, environment };
28592
29397
  const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
28593
29398
  const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
@@ -28599,10 +29404,10 @@ async function trafficRoutes(app, opts) {
28599
29404
  lastError: null,
28600
29405
  configJson: config,
28601
29406
  updatedAt: now
28602
- }).where(eq25(trafficSources.id, activeSource.id)).run();
28603
- row = tx.select().from(trafficSources).where(eq25(trafficSources.id, activeSource.id)).get();
29407
+ }).where(eq26(trafficSources.id, activeSource.id)).run();
29408
+ row = tx.select().from(trafficSources).where(eq26(trafficSources.id, activeSource.id)).get();
28604
29409
  } else {
28605
- const newId = crypto23.randomUUID();
29410
+ const newId = crypto24.randomUUID();
28606
29411
  tx.insert(trafficSources).values({
28607
29412
  id: newId,
28608
29413
  projectId: project.id,
@@ -28625,18 +29430,18 @@ async function trafficRoutes(app, opts) {
28625
29430
  createdAt: now,
28626
29431
  updatedAt: now
28627
29432
  }).run();
28628
- row = tx.select().from(trafficSources).where(eq25(trafficSources.id, newId)).get();
29433
+ row = tx.select().from(trafficSources).where(eq26(trafficSources.id, newId)).get();
28629
29434
  }
28630
29435
  const existingSchedule = tx.select().from(schedules).where(
28631
- and19(
28632
- eq25(schedules.projectId, project.id),
28633
- eq25(schedules.kind, SchedulableRunKinds["traffic-sync"])
29436
+ and20(
29437
+ eq26(schedules.projectId, project.id),
29438
+ eq26(schedules.kind, SchedulableRunKinds["traffic-sync"])
28634
29439
  )
28635
29440
  ).get();
28636
29441
  let created = false;
28637
29442
  if (!existingSchedule) {
28638
29443
  tx.insert(schedules).values({
28639
- id: crypto23.randomUUID(),
29444
+ id: crypto24.randomUUID(),
28640
29445
  projectId: project.id,
28641
29446
  kind: SchedulableRunKinds["traffic-sync"],
28642
29447
  cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
@@ -28679,7 +29484,7 @@ async function trafficRoutes(app, opts) {
28679
29484
  });
28680
29485
  app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
28681
29486
  const project = resolveProject(app.db, request.params.name);
28682
- const sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, request.params.id)).get();
29487
+ const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
28683
29488
  if (!sourceRow || sourceRow.projectId !== project.id) {
28684
29489
  throw notFound("Traffic source", request.params.id);
28685
29490
  }
@@ -28691,7 +29496,7 @@ async function trafficRoutes(app, opts) {
28691
29496
  const windowEnd = /* @__PURE__ */ new Date();
28692
29497
  const startedAt = windowEnd.toISOString();
28693
29498
  const syncStartedAtMs = windowEnd.getTime();
28694
- const runId = crypto23.randomUUID();
29499
+ const runId = crypto24.randomUUID();
28695
29500
  app.db.insert(runs).values({
28696
29501
  id: runId,
28697
29502
  projectId: project.id,
@@ -28705,8 +29510,8 @@ async function trafficRoutes(app, opts) {
28705
29510
  const markFailed = (msg, errorCode) => {
28706
29511
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
28707
29512
  app.db.transaction((tx) => {
28708
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq25(runs.id, runId)).run();
28709
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq25(trafficSources.id, sourceRow.id)).run();
29513
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq26(runs.id, runId)).run();
29514
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq26(trafficSources.id, sourceRow.id)).run();
28710
29515
  });
28711
29516
  try {
28712
29517
  opts.onTrafficSynced?.({
@@ -28787,7 +29592,7 @@ async function trafficRoutes(app, opts) {
28787
29592
  }
28788
29593
  const credential = credentialStore.getConnection(project.name);
28789
29594
  if (!credential) {
28790
- app.db.delete(runs).where(eq25(runs.id, runId)).run();
29595
+ app.db.delete(runs).where(eq26(runs.id, runId)).run();
28791
29596
  throw validationError(
28792
29597
  `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
28793
29598
  );
@@ -28836,12 +29641,12 @@ async function trafficRoutes(app, opts) {
28836
29641
  auditAction = "traffic.vercel.synced";
28837
29642
  const credentialStore = opts.vercelTrafficCredentialStore;
28838
29643
  if (!credentialStore) {
28839
- app.db.delete(runs).where(eq25(runs.id, runId)).run();
29644
+ app.db.delete(runs).where(eq26(runs.id, runId)).run();
28840
29645
  throw validationError("Vercel traffic credential storage is not configured for this deployment");
28841
29646
  }
28842
29647
  const credential = credentialStore.getConnection(project.name);
28843
29648
  if (!credential) {
28844
- app.db.delete(runs).where(eq25(runs.id, runId)).run();
29649
+ app.db.delete(runs).where(eq26(runs.id, runId)).run();
28845
29650
  throw validationError(
28846
29651
  `No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
28847
29652
  );
@@ -28941,7 +29746,7 @@ async function trafficRoutes(app, opts) {
28941
29746
  let aiReferralHitsCount = 0;
28942
29747
  let unknownHitsCount = 0;
28943
29748
  app.db.transaction((tx) => {
28944
- const latestRow = tx.select().from(trafficSources).where(eq25(trafficSources.id, sourceRow.id)).get();
29749
+ const latestRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
28945
29750
  const previousIds = latestRow.lastEventIds ?? [];
28946
29751
  const seenEventIds = new Set(previousIds);
28947
29752
  const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
@@ -29074,7 +29879,7 @@ async function trafficRoutes(app, opts) {
29074
29879
  }
29075
29880
  })();
29076
29881
  tx.insert(rawEventSamples).values({
29077
- id: crypto23.randomUUID(),
29882
+ id: crypto24.randomUUID(),
29078
29883
  projectId: project.id,
29079
29884
  sourceId: sourceRow.id,
29080
29885
  ts: sample.observedAt,
@@ -29109,8 +29914,8 @@ async function trafficRoutes(app, opts) {
29109
29914
  if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
29110
29915
  sourceUpdate.lastCursor = nextCursor ?? null;
29111
29916
  }
29112
- tx.update(trafficSources).set(sourceUpdate).where(eq25(trafficSources.id, sourceRow.id)).run();
29113
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
29917
+ tx.update(trafficSources).set(sourceUpdate).where(eq26(trafficSources.id, sourceRow.id)).run();
29918
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
29114
29919
  });
29115
29920
  writeAuditLog(app.db, {
29116
29921
  projectId: project.id,
@@ -29162,7 +29967,7 @@ async function trafficRoutes(app, opts) {
29162
29967
  });
29163
29968
  app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
29164
29969
  const project = resolveProject(app.db, request.params.name);
29165
- const sourceRow = app.db.select().from(trafficSources).where(eq25(trafficSources.id, request.params.id)).get();
29970
+ const sourceRow = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
29166
29971
  if (!sourceRow || sourceRow.projectId !== project.id) {
29167
29972
  throw notFound("Traffic source", request.params.id);
29168
29973
  }
@@ -29312,7 +30117,7 @@ async function trafficRoutes(app, opts) {
29312
30117
  };
29313
30118
  }
29314
30119
  const startedAt = windowEnd.toISOString();
29315
- const runId = crypto23.randomUUID();
30120
+ const runId = crypto24.randomUUID();
29316
30121
  app.db.insert(runs).values({
29317
30122
  id: runId,
29318
30123
  projectId: project.id,
@@ -29347,34 +30152,34 @@ async function trafficRoutes(app, opts) {
29347
30152
  });
29348
30153
  function buildSourceDetail(projectId, row, since) {
29349
30154
  const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
29350
- and19(
29351
- eq25(crawlerEventsHourly.sourceId, row.id),
29352
- gte3(crawlerEventsHourly.tsHour, since)
30155
+ and20(
30156
+ eq26(crawlerEventsHourly.sourceId, row.id),
30157
+ gte4(crawlerEventsHourly.tsHour, since)
29353
30158
  )
29354
30159
  ).get();
29355
30160
  const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
29356
- and19(
29357
- eq25(aiUserFetchEventsHourly.sourceId, row.id),
29358
- gte3(aiUserFetchEventsHourly.tsHour, since)
30161
+ and20(
30162
+ eq26(aiUserFetchEventsHourly.sourceId, row.id),
30163
+ gte4(aiUserFetchEventsHourly.tsHour, since)
29359
30164
  )
29360
30165
  ).get();
29361
30166
  const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
29362
- and19(
29363
- eq25(aiReferralEventsHourly.sourceId, row.id),
29364
- gte3(aiReferralEventsHourly.tsHour, since)
30167
+ and20(
30168
+ eq26(aiReferralEventsHourly.sourceId, row.id),
30169
+ gte4(aiReferralEventsHourly.tsHour, since)
29365
30170
  )
29366
30171
  ).get();
29367
30172
  const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
29368
- and19(
29369
- eq25(rawEventSamples.sourceId, row.id),
29370
- gte3(rawEventSamples.ts, since)
30173
+ and20(
30174
+ eq26(rawEventSamples.sourceId, row.id),
30175
+ gte4(rawEventSamples.ts, since)
29371
30176
  )
29372
30177
  ).get();
29373
30178
  const latestRun = app.db.select().from(runs).where(
29374
- and19(
29375
- eq25(runs.projectId, projectId),
29376
- eq25(runs.kind, RunKinds["traffic-sync"]),
29377
- eq25(runs.sourceId, row.id)
30179
+ and20(
30180
+ eq26(runs.projectId, projectId),
30181
+ eq26(runs.kind, RunKinds["traffic-sync"]),
30182
+ eq26(runs.sourceId, row.id)
29378
30183
  )
29379
30184
  ).orderBy(desc14(runs.startedAt)).limit(1).get();
29380
30185
  return {
@@ -29402,7 +30207,7 @@ async function trafficRoutes(app, opts) {
29402
30207
  "`advanceToNow` must be `true`. There is no implicit reset."
29403
30208
  );
29404
30209
  }
29405
- const sourceRow = app.db.select().from(trafficSources).where(and19(eq25(trafficSources.projectId, project.id), eq25(trafficSources.id, request.params.id))).get();
30210
+ const sourceRow = app.db.select().from(trafficSources).where(and20(eq26(trafficSources.projectId, project.id), eq26(trafficSources.id, request.params.id))).get();
29406
30211
  if (!sourceRow) {
29407
30212
  throw notFound("traffic source", request.params.id);
29408
30213
  }
@@ -29419,7 +30224,7 @@ async function trafficRoutes(app, opts) {
29419
30224
  status: TrafficSourceStatuses.connected,
29420
30225
  lastError: null,
29421
30226
  updatedAt: now
29422
- }).where(eq25(trafficSources.id, sourceRow.id)).run();
30227
+ }).where(eq26(trafficSources.id, sourceRow.id)).run();
29423
30228
  writeAuditLog(tx, auditFromRequest(request, {
29424
30229
  projectId: project.id,
29425
30230
  actor: "api",
@@ -29427,20 +30232,20 @@ async function trafficRoutes(app, opts) {
29427
30232
  entityType: "traffic_source",
29428
30233
  entityId: sourceRow.id
29429
30234
  }));
29430
- updatedRow = tx.select().from(trafficSources).where(eq25(trafficSources.id, sourceRow.id)).get();
30235
+ updatedRow = tx.select().from(trafficSources).where(eq26(trafficSources.id, sourceRow.id)).get();
29431
30236
  });
29432
30237
  return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
29433
30238
  });
29434
30239
  app.get("/projects/:name/traffic/sources", async (request) => {
29435
30240
  const project = resolveProject(app.db, request.params.name);
29436
- const rows = app.db.select().from(trafficSources).where(eq25(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
30241
+ const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
29437
30242
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
29438
30243
  const response = { sources };
29439
30244
  return response;
29440
30245
  });
29441
30246
  app.get("/projects/:name/traffic/status", async (request) => {
29442
30247
  const project = resolveProject(app.db, request.params.name);
29443
- const rows = app.db.select().from(trafficSources).where(eq25(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
30248
+ const rows = app.db.select().from(trafficSources).where(eq26(trafficSources.projectId, project.id)).orderBy(desc14(trafficSources.createdAt)).all();
29444
30249
  const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
29445
30250
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
29446
30251
  const response = { sources };
@@ -29450,7 +30255,7 @@ async function trafficRoutes(app, opts) {
29450
30255
  "/projects/:name/traffic/sources/:id",
29451
30256
  async (request) => {
29452
30257
  const project = resolveProject(app.db, request.params.name);
29453
- const row = app.db.select().from(trafficSources).where(eq25(trafficSources.id, request.params.id)).get();
30258
+ const row = app.db.select().from(trafficSources).where(eq26(trafficSources.id, request.params.id)).get();
29454
30259
  if (!row || row.projectId !== project.id) {
29455
30260
  throw notFound("Traffic source", request.params.id);
29456
30261
  }
@@ -29501,12 +30306,12 @@ async function trafficRoutes(app, opts) {
29501
30306
  let aiReferralTotal = 0;
29502
30307
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
29503
30308
  const crawlerFilters = [
29504
- eq25(crawlerEventsHourly.projectId, project.id),
29505
- gte3(crawlerEventsHourly.tsHour, sinceIso),
29506
- lte2(crawlerEventsHourly.tsHour, untilIso)
30309
+ eq26(crawlerEventsHourly.projectId, project.id),
30310
+ gte4(crawlerEventsHourly.tsHour, sinceIso),
30311
+ lte3(crawlerEventsHourly.tsHour, untilIso)
29507
30312
  ];
29508
- if (sourceIdParam) crawlerFilters.push(eq25(crawlerEventsHourly.sourceId, sourceIdParam));
29509
- const crawlerWhere = and19(...crawlerFilters);
30313
+ if (sourceIdParam) crawlerFilters.push(eq26(crawlerEventsHourly.sourceId, sourceIdParam));
30314
+ const crawlerWhere = and20(...crawlerFilters);
29510
30315
  const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
29511
30316
  crawlerTotal = Number(total?.total ?? 0);
29512
30317
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc14(crawlerEventsHourly.tsHour)).limit(limit).all();
@@ -29526,12 +30331,12 @@ async function trafficRoutes(app, opts) {
29526
30331
  }
29527
30332
  if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
29528
30333
  const userFetchFilters = [
29529
- eq25(aiUserFetchEventsHourly.projectId, project.id),
29530
- gte3(aiUserFetchEventsHourly.tsHour, sinceIso),
29531
- lte2(aiUserFetchEventsHourly.tsHour, untilIso)
30334
+ eq26(aiUserFetchEventsHourly.projectId, project.id),
30335
+ gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
30336
+ lte3(aiUserFetchEventsHourly.tsHour, untilIso)
29532
30337
  ];
29533
- if (sourceIdParam) userFetchFilters.push(eq25(aiUserFetchEventsHourly.sourceId, sourceIdParam));
29534
- const userFetchWhere = and19(...userFetchFilters);
30338
+ if (sourceIdParam) userFetchFilters.push(eq26(aiUserFetchEventsHourly.sourceId, sourceIdParam));
30339
+ const userFetchWhere = and20(...userFetchFilters);
29535
30340
  const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
29536
30341
  aiUserFetchTotal = Number(total?.total ?? 0);
29537
30342
  const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc14(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
@@ -29551,12 +30356,12 @@ async function trafficRoutes(app, opts) {
29551
30356
  }
29552
30357
  if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
29553
30358
  const aiFilters = [
29554
- eq25(aiReferralEventsHourly.projectId, project.id),
29555
- gte3(aiReferralEventsHourly.tsHour, sinceIso),
29556
- lte2(aiReferralEventsHourly.tsHour, untilIso)
30359
+ eq26(aiReferralEventsHourly.projectId, project.id),
30360
+ gte4(aiReferralEventsHourly.tsHour, sinceIso),
30361
+ lte3(aiReferralEventsHourly.tsHour, untilIso)
29557
30362
  ];
29558
- if (sourceIdParam) aiFilters.push(eq25(aiReferralEventsHourly.sourceId, sourceIdParam));
29559
- const aiWhere = and19(...aiFilters);
30363
+ if (sourceIdParam) aiFilters.push(eq26(aiReferralEventsHourly.sourceId, sourceIdParam));
30364
+ const aiWhere = and20(...aiFilters);
29560
30365
  const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
29561
30366
  aiReferralTotal = Number(total?.total ?? 0);
29562
30367
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc14(aiReferralEventsHourly.tsHour)).limit(limit).all();
@@ -29592,7 +30397,7 @@ async function trafficRoutes(app, opts) {
29592
30397
  }
29593
30398
 
29594
30399
  // ../api-routes/src/doctor/checks/agent.ts
29595
- import crypto24 from "crypto";
30400
+ import crypto25 from "crypto";
29596
30401
  import fs6 from "fs";
29597
30402
  import path7 from "path";
29598
30403
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -29745,7 +30550,7 @@ function isInstalled(dir) {
29745
30550
  }
29746
30551
  function hashInstalledFile(filePath) {
29747
30552
  try {
29748
- return crypto24.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
30553
+ return crypto25.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
29749
30554
  } catch {
29750
30555
  return void 0;
29751
30556
  }
@@ -29759,6 +30564,54 @@ function readInstalledManifest(skillDir) {
29759
30564
  }
29760
30565
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
29761
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
+
29762
30615
  // ../api-routes/src/doctor/checks/bing-auth.ts
29763
30616
  var BING_AUTH_CHECKS = [
29764
30617
  {
@@ -29906,10 +30759,10 @@ var BING_AUTH_CHECKS = [
29906
30759
  ];
29907
30760
 
29908
30761
  // ../api-routes/src/doctor/checks/content.ts
29909
- import { eq as eq26 } from "drizzle-orm";
30762
+ import { eq as eq28 } from "drizzle-orm";
29910
30763
  var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
29911
30764
  var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
29912
- function skippedNoProject() {
30765
+ function skippedNoProject2() {
29913
30766
  return {
29914
30767
  status: CheckStatuses.skipped,
29915
30768
  code: "content.winnability.no-project",
@@ -29919,7 +30772,7 @@ function skippedNoProject() {
29919
30772
  }
29920
30773
  function loadProject(ctx) {
29921
30774
  if (!ctx.project) return null;
29922
- return ctx.db.select().from(projects).where(eq26(projects.id, ctx.project.id)).get() ?? null;
30775
+ return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
29923
30776
  }
29924
30777
  function percent(value) {
29925
30778
  return Math.round(value * 100);
@@ -29930,7 +30783,7 @@ var winnabilityCoverageCheck = {
29930
30783
  scope: CheckScopes.project,
29931
30784
  title: "Content winnability classification coverage",
29932
30785
  run: (ctx) => {
29933
- if (!ctx.project) return skippedNoProject();
30786
+ if (!ctx.project) return skippedNoProject2();
29934
30787
  const project = loadProject(ctx);
29935
30788
  if (!project) {
29936
30789
  return {
@@ -30010,6 +30863,123 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
30010
30863
  CONTENT_CHECKS.map((check) => [check.id, check])
30011
30864
  );
30012
30865
 
30866
+ // ../api-routes/src/doctor/checks/ads.ts
30867
+ import { eq as eq29 } from "drizzle-orm";
30868
+ var RECENT_SYNC_WARN_DAYS = 7;
30869
+ var RECENT_SYNC_FAIL_DAYS = 30;
30870
+ var adsConnectionCheck = {
30871
+ id: "ads.auth.connection",
30872
+ category: CheckCategories.auth,
30873
+ scope: CheckScopes.project,
30874
+ title: "OpenAI ads connection",
30875
+ run: (ctx) => {
30876
+ if (!ctx.project) {
30877
+ return {
30878
+ status: CheckStatuses.skipped,
30879
+ code: "ads.auth.no-project",
30880
+ summary: "Project context required.",
30881
+ remediation: null
30882
+ };
30883
+ }
30884
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30885
+ if (!row) {
30886
+ return {
30887
+ status: CheckStatuses.skipped,
30888
+ code: "ads.auth.not-connected",
30889
+ summary: "No OpenAI ads connection for this project.",
30890
+ remediation: null
30891
+ };
30892
+ }
30893
+ if (!ctx.adsCredentialStore) {
30894
+ return {
30895
+ status: CheckStatuses.skipped,
30896
+ code: "ads.auth.store-unavailable",
30897
+ summary: "No ads credential store configured for this deployment.",
30898
+ remediation: null
30899
+ };
30900
+ }
30901
+ const cfg = ctx.adsCredentialStore.getConnection(ctx.project.name);
30902
+ if (!cfg?.apiKey) {
30903
+ return {
30904
+ status: CheckStatuses.fail,
30905
+ code: "ads.auth.missing-key",
30906
+ summary: "An ads connection row exists but no SDK key is stored in the local config.",
30907
+ remediation: `Re-run \`canonry ads connect ${ctx.project.name} --api-key <sdk-key>\` to restore the credential.`,
30908
+ details: { adAccountId: row.adAccountId }
30909
+ };
30910
+ }
30911
+ return {
30912
+ status: CheckStatuses.ok,
30913
+ code: "ads.auth.ok",
30914
+ summary: `Connected to ad account ${row.displayName ?? row.adAccountId}.`,
30915
+ remediation: null,
30916
+ details: { adAccountId: row.adAccountId }
30917
+ };
30918
+ }
30919
+ };
30920
+ var adsRecentSyncCheck = {
30921
+ id: "ads.data.recent-sync",
30922
+ category: CheckCategories.integrations,
30923
+ scope: CheckScopes.project,
30924
+ title: "OpenAI ads recent sync",
30925
+ run: (ctx) => {
30926
+ if (!ctx.project) {
30927
+ return {
30928
+ status: CheckStatuses.skipped,
30929
+ code: "ads.data.no-project",
30930
+ summary: "Project context required.",
30931
+ remediation: null
30932
+ };
30933
+ }
30934
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30935
+ if (!row) {
30936
+ return {
30937
+ status: CheckStatuses.skipped,
30938
+ code: "ads.data.not-connected",
30939
+ summary: "No OpenAI ads connection for this project.",
30940
+ remediation: null
30941
+ };
30942
+ }
30943
+ if (!row.lastSyncedAt) {
30944
+ return {
30945
+ status: CheckStatuses.warn,
30946
+ code: "ads.data.never-synced",
30947
+ summary: "The connected ad account has never been synced.",
30948
+ remediation: `Run \`canonry ads sync ${ctx.project.name}\` (and schedule it: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`).`
30949
+ };
30950
+ }
30951
+ const syncedAtMs = new Date(row.lastSyncedAt).getTime();
30952
+ const ageDays = (Date.now() - syncedAtMs) / (1e3 * 60 * 60 * 24);
30953
+ const details = { lastSyncedAt: row.lastSyncedAt, ageDays: Math.round(ageDays) };
30954
+ if (ageDays > RECENT_SYNC_FAIL_DAYS) {
30955
+ return {
30956
+ status: CheckStatuses.fail,
30957
+ code: "ads.data.stale",
30958
+ summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS}d).`,
30959
+ remediation: `Run \`canonry ads sync ${ctx.project.name}\` and check the ads-sync schedule.`,
30960
+ details
30961
+ };
30962
+ }
30963
+ if (ageDays > RECENT_SYNC_WARN_DAYS) {
30964
+ return {
30965
+ status: CheckStatuses.warn,
30966
+ code: "ads.data.aging",
30967
+ summary: `Last ads sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS}d).`,
30968
+ remediation: `Schedule daily syncs: \`canonry schedule set ${ctx.project.name} --kind ads-sync --preset daily\`.`,
30969
+ details
30970
+ };
30971
+ }
30972
+ return {
30973
+ status: CheckStatuses.ok,
30974
+ code: "ads.data.ok",
30975
+ summary: `Last ads sync ${Math.round(ageDays)} day(s) ago.`,
30976
+ remediation: null,
30977
+ details
30978
+ };
30979
+ }
30980
+ };
30981
+ var ADS_CHECKS = [adsConnectionCheck, adsRecentSyncCheck];
30982
+
30013
30983
  // ../api-routes/src/doctor/checks/ga-auth.ts
30014
30984
  async function checkServiceAccount(conn) {
30015
30985
  if (!conn.propertyId) {
@@ -30152,10 +31122,10 @@ var ga4ConnectionCheck = {
30152
31122
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
30153
31123
 
30154
31124
  // ../api-routes/src/doctor/checks/gbp-auth.ts
30155
- import { and as and20, eq as eq27 } from "drizzle-orm";
30156
- var RECENT_SYNC_WARN_DAYS = 7;
30157
- var RECENT_SYNC_FAIL_DAYS = 30;
30158
- function skippedNoProject2() {
31125
+ import { and as and22, eq as eq30 } from "drizzle-orm";
31126
+ var RECENT_SYNC_WARN_DAYS2 = 7;
31127
+ var RECENT_SYNC_FAIL_DAYS2 = 30;
31128
+ function skippedNoProject3() {
30159
31129
  return {
30160
31130
  status: CheckStatuses.skipped,
30161
31131
  code: "gbp.auth.no-project",
@@ -30172,7 +31142,7 @@ function storeUnavailable() {
30172
31142
  };
30173
31143
  }
30174
31144
  async function resolveGbpToken(ctx) {
30175
- if (!ctx.project) return { ok: false, output: skippedNoProject2() };
31145
+ if (!ctx.project) return { ok: false, output: skippedNoProject3() };
30176
31146
  const store = ctx.googleConnectionStore;
30177
31147
  if (!store) return { ok: false, output: storeUnavailable() };
30178
31148
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
@@ -30250,7 +31220,7 @@ var scopesCheck = {
30250
31220
  scope: CheckScopes.project,
30251
31221
  title: "GBP granted scopes",
30252
31222
  run: async (ctx) => {
30253
- if (!ctx.project) return skippedNoProject2();
31223
+ if (!ctx.project) return skippedNoProject3();
30254
31224
  const store = ctx.googleConnectionStore;
30255
31225
  if (!store) return storeUnavailable();
30256
31226
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -30287,7 +31257,7 @@ var accountAccessCheck = {
30287
31257
  scope: CheckScopes.project,
30288
31258
  title: "GBP account access",
30289
31259
  run: async (ctx) => {
30290
- if (!ctx.project) return skippedNoProject2();
31260
+ if (!ctx.project) return skippedNoProject3();
30291
31261
  const store = ctx.googleConnectionStore;
30292
31262
  if (!store) return storeUnavailable();
30293
31263
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -30384,8 +31354,8 @@ var recentSyncCheck = {
30384
31354
  scope: CheckScopes.project,
30385
31355
  title: "GBP recent sync",
30386
31356
  run: (ctx) => {
30387
- if (!ctx.project) return skippedNoProject2();
30388
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and20(eq27(gbpLocations.projectId, ctx.project.id), eq27(gbpLocations.selected, true))).all();
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();
30389
31359
  if (selected.length === 0) {
30390
31360
  return {
30391
31361
  status: CheckStatuses.skipped,
@@ -30407,20 +31377,20 @@ var recentSyncCheck = {
30407
31377
  const newest = Math.max(...syncTimes);
30408
31378
  const ageDays = (Date.now() - newest) / (1e3 * 60 * 60 * 24);
30409
31379
  const details = { selectedLocations: selected.length, newestSyncAgeDays: Math.round(ageDays) };
30410
- if (ageDays > RECENT_SYNC_FAIL_DAYS) {
31380
+ if (ageDays > RECENT_SYNC_FAIL_DAYS2) {
30411
31381
  return {
30412
31382
  status: CheckStatuses.fail,
30413
31383
  code: "gbp.data.stale",
30414
- summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS}d).`,
31384
+ summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_FAIL_DAYS2}d).`,
30415
31385
  remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule.`,
30416
31386
  details
30417
31387
  };
30418
31388
  }
30419
- if (ageDays > RECENT_SYNC_WARN_DAYS) {
31389
+ if (ageDays > RECENT_SYNC_WARN_DAYS2) {
30420
31390
  return {
30421
31391
  status: CheckStatuses.warn,
30422
31392
  code: "gbp.data.aging",
30423
- summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS}d).`,
31393
+ summary: `Most recent GBP sync was ${Math.round(ageDays)} days ago (> ${RECENT_SYNC_WARN_DAYS2}d).`,
30424
31394
  remediation: `Run \`canonry gbp sync ${ctx.project.name}\` or set a gbp-sync schedule to keep data fresh.`,
30425
31395
  details
30426
31396
  };
@@ -30445,7 +31415,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
30445
31415
  );
30446
31416
 
30447
31417
  // ../api-routes/src/doctor/checks/places.ts
30448
- import { eq as eq28 } from "drizzle-orm";
31418
+ import { eq as eq31 } from "drizzle-orm";
30449
31419
  var apiKeyCheck = {
30450
31420
  id: "gbp.places.api-key",
30451
31421
  category: CheckCategories.auth,
@@ -30490,7 +31460,7 @@ var apiKeyCheck = {
30490
31460
  details: { tier: cfg.tier }
30491
31461
  };
30492
31462
  }
30493
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq28(gbpLocations.projectId, ctx.project.id)).all();
31463
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
30494
31464
  const selected = rows.filter((r) => r.selected);
30495
31465
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
30496
31466
  const details = {
@@ -30526,7 +31496,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
30526
31496
  var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
30527
31497
  async function resolveAccessToken(ctx) {
30528
31498
  if (!ctx.project) {
30529
- return { ok: false, output: skippedNoProject3() };
31499
+ return { ok: false, output: skippedNoProject4() };
30530
31500
  }
30531
31501
  const store = ctx.googleConnectionStore;
30532
31502
  if (!store) {
@@ -30593,7 +31563,7 @@ async function resolveAccessToken(ctx) {
30593
31563
  };
30594
31564
  }
30595
31565
  }
30596
- function skippedNoProject3() {
31566
+ function skippedNoProject4() {
30597
31567
  return {
30598
31568
  status: CheckStatuses.skipped,
30599
31569
  code: "google.auth.no-project",
@@ -30623,7 +31593,7 @@ var propertyAccessCheck = {
30623
31593
  scope: CheckScopes.project,
30624
31594
  title: "GSC property access",
30625
31595
  run: async (ctx) => {
30626
- if (!ctx.project) return skippedNoProject3();
31596
+ if (!ctx.project) return skippedNoProject4();
30627
31597
  const store = ctx.googleConnectionStore;
30628
31598
  if (!store) {
30629
31599
  return {
@@ -30724,7 +31694,7 @@ var redirectUriCheck = {
30724
31694
  scope: CheckScopes.project,
30725
31695
  title: "OAuth redirect URI",
30726
31696
  run: async (ctx) => {
30727
- if (!ctx.project) return skippedNoProject3();
31697
+ if (!ctx.project) return skippedNoProject4();
30728
31698
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
30729
31699
  if (!auth.clientId || !auth.clientSecret) {
30730
31700
  return {
@@ -30778,7 +31748,7 @@ var scopesCheck2 = {
30778
31748
  scope: CheckScopes.project,
30779
31749
  title: "GSC granted scopes",
30780
31750
  run: async (ctx) => {
30781
- if (!ctx.project) return skippedNoProject3();
31751
+ if (!ctx.project) return skippedNoProject4();
30782
31752
  const store = ctx.googleConnectionStore;
30783
31753
  if (!store) {
30784
31754
  return {
@@ -30941,10 +31911,10 @@ var RUNTIME_STATE_CHECKS = [
30941
31911
  ];
30942
31912
 
30943
31913
  // ../api-routes/src/doctor/checks/traffic-source.ts
30944
- import { and as and21, eq as eq29, gte as gte4, ne as ne4, sql as sql12 } from "drizzle-orm";
31914
+ import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
30945
31915
  var RECENT_DATA_WARN_DAYS = 7;
30946
31916
  var RECENT_DATA_FAIL_DAYS = 30;
30947
- function skippedNoProject4() {
31917
+ function skippedNoProject5() {
30948
31918
  return {
30949
31919
  status: CheckStatuses.skipped,
30950
31920
  code: "traffic.no-project",
@@ -30955,8 +31925,8 @@ function skippedNoProject4() {
30955
31925
  function loadProbes(ctx) {
30956
31926
  if (!ctx.project) return [];
30957
31927
  const rows = ctx.db.select().from(trafficSources).where(
30958
- and21(
30959
- eq29(trafficSources.projectId, ctx.project.id),
31928
+ and23(
31929
+ eq32(trafficSources.projectId, ctx.project.id),
30960
31930
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
30961
31931
  )
30962
31932
  ).all();
@@ -30978,7 +31948,7 @@ var sourceConnectedCheck = {
30978
31948
  scope: CheckScopes.project,
30979
31949
  title: "Traffic source connected",
30980
31950
  run: (ctx) => {
30981
- if (!ctx.project) return skippedNoProject4();
31951
+ if (!ctx.project) return skippedNoProject5();
30982
31952
  const sources = loadProbes(ctx);
30983
31953
  if (sources.length === 0) {
30984
31954
  return {
@@ -31022,7 +31992,7 @@ var recentDataCheck = {
31022
31992
  scope: CheckScopes.project,
31023
31993
  title: "Traffic source recent data",
31024
31994
  run: (ctx) => {
31025
- if (!ctx.project) return skippedNoProject4();
31995
+ if (!ctx.project) return skippedNoProject5();
31026
31996
  const sources = loadProbes(ctx);
31027
31997
  if (sources.length === 0) {
31028
31998
  return {
@@ -31036,17 +32006,17 @@ var recentDataCheck = {
31036
32006
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
31037
32007
  const recentCrawlers = Number(
31038
32008
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31039
- and21(
31040
- eq29(crawlerEventsHourly.projectId, ctx.project.id),
31041
- gte4(crawlerEventsHourly.tsHour, warnCutoff)
32009
+ and23(
32010
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
32011
+ gte5(crawlerEventsHourly.tsHour, warnCutoff)
31042
32012
  )
31043
32013
  ).get()?.total ?? 0
31044
32014
  );
31045
32015
  const recentReferrals = Number(
31046
32016
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31047
- and21(
31048
- eq29(aiReferralEventsHourly.projectId, ctx.project.id),
31049
- gte4(aiReferralEventsHourly.tsHour, warnCutoff)
32017
+ and23(
32018
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
32019
+ gte5(aiReferralEventsHourly.tsHour, warnCutoff)
31050
32020
  )
31051
32021
  ).get()?.total ?? 0
31052
32022
  );
@@ -31060,17 +32030,17 @@ var recentDataCheck = {
31060
32030
  }
31061
32031
  const olderCrawlers = Number(
31062
32032
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31063
- and21(
31064
- eq29(crawlerEventsHourly.projectId, ctx.project.id),
31065
- gte4(crawlerEventsHourly.tsHour, failCutoff)
32033
+ and23(
32034
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
32035
+ gte5(crawlerEventsHourly.tsHour, failCutoff)
31066
32036
  )
31067
32037
  ).get()?.total ?? 0
31068
32038
  );
31069
32039
  const olderReferrals = Number(
31070
32040
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31071
- and21(
31072
- eq29(aiReferralEventsHourly.projectId, ctx.project.id),
31073
- gte4(aiReferralEventsHourly.tsHour, failCutoff)
32041
+ and23(
32042
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
32043
+ gte5(aiReferralEventsHourly.tsHour, failCutoff)
31074
32044
  )
31075
32045
  ).get()?.total ?? 0
31076
32046
  );
@@ -31184,7 +32154,7 @@ var credentialsCheck = {
31184
32154
  scope: CheckScopes.project,
31185
32155
  title: "Traffic source credentials",
31186
32156
  run: async (ctx) => {
31187
- if (!ctx.project) return skippedNoProject4();
32157
+ if (!ctx.project) return skippedNoProject5();
31188
32158
  const sources = loadProbes(ctx);
31189
32159
  if (sources.length === 0) {
31190
32160
  return {
@@ -31213,7 +32183,7 @@ var scopesCheck3 = {
31213
32183
  scope: CheckScopes.project,
31214
32184
  title: "Traffic source scopes",
31215
32185
  run: async (ctx) => {
31216
- if (!ctx.project) return skippedNoProject4();
32186
+ if (!ctx.project) return skippedNoProject5();
31217
32187
  const sources = loadProbes(ctx);
31218
32188
  if (sources.length === 0) {
31219
32189
  return {
@@ -31242,7 +32212,7 @@ var cacheBlindSpotCheck = {
31242
32212
  scope: CheckScopes.project,
31243
32213
  title: "WordPress traffic cache blind spot",
31244
32214
  run: (ctx) => {
31245
- if (!ctx.project) return skippedNoProject4();
32215
+ if (!ctx.project) return skippedNoProject5();
31246
32216
  const wpSources = loadProbes(ctx).filter(
31247
32217
  (s) => s.sourceType === TrafficSourceTypes.wordpress
31248
32218
  );
@@ -31354,8 +32324,10 @@ var ALL_CHECKS = [
31354
32324
  ...BING_AUTH_CHECKS,
31355
32325
  ...WORDPRESS_PUBLISH_CHECKS,
31356
32326
  ...GA_AUTH_CHECKS,
32327
+ ...ADS_CHECKS,
31357
32328
  ...PROVIDERS_CHECKS,
31358
32329
  ...TRAFFIC_SOURCE_CHECKS,
32330
+ ...BACKLINKS_CHECKS,
31359
32331
  ...CONTENT_CHECKS,
31360
32332
  ...AGENT_CHECKS
31361
32333
  ];
@@ -31440,6 +32412,7 @@ async function doctorRoutes(app, opts) {
31440
32412
  bingConnectionStore: opts.bingConnectionStore,
31441
32413
  wordpressConnectionStore: opts.wordpressConnectionStore,
31442
32414
  ga4CredentialStore: opts.ga4CredentialStore,
32415
+ adsCredentialStore: opts.adsCredentialStore,
31443
32416
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
31444
32417
  getPlacesConfig: opts.getPlacesConfig,
31445
32418
  redirectUri,
@@ -31465,6 +32438,7 @@ async function doctorRoutes(app, opts) {
31465
32438
  bingConnectionStore: opts.bingConnectionStore,
31466
32439
  wordpressConnectionStore: opts.wordpressConnectionStore,
31467
32440
  ga4CredentialStore: opts.ga4CredentialStore,
32441
+ adsCredentialStore: opts.adsCredentialStore,
31468
32442
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
31469
32443
  getPlacesConfig: opts.getPlacesConfig,
31470
32444
  redirectUri,
@@ -31478,8 +32452,8 @@ async function doctorRoutes(app, opts) {
31478
32452
  }
31479
32453
 
31480
32454
  // ../api-routes/src/discovery/routes.ts
31481
- import crypto25 from "crypto";
31482
- import { and as and22, desc as desc15, eq as eq30, gte as gte5, inArray as inArray10 } from "drizzle-orm";
32455
+ import crypto26 from "crypto";
32456
+ import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
31483
32457
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
31484
32458
  async function discoveryRoutes(app, opts) {
31485
32459
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -31511,21 +32485,21 @@ async function discoveryRoutes(app, opts) {
31511
32485
  const now = (/* @__PURE__ */ new Date()).toISOString();
31512
32486
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
31513
32487
  const decision = app.db.transaction((tx) => {
31514
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and22(
31515
- eq30(discoverySessions.projectId, project.id),
31516
- eq30(discoverySessions.icpDescription, icpDescription),
31517
- inArray10(discoverySessions.status, [
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),
32491
+ inArray11(discoverySessions.status, [
31518
32492
  DiscoverySessionStatuses.queued,
31519
32493
  DiscoverySessionStatuses.seeding,
31520
32494
  DiscoverySessionStatuses.probing
31521
32495
  ]),
31522
- gte5(discoverySessions.createdAt, ageFloorIso)
32496
+ gte6(discoverySessions.createdAt, ageFloorIso)
31523
32497
  )).orderBy(desc15(discoverySessions.createdAt)).get();
31524
32498
  if (existing && existing.runId) {
31525
32499
  return { reused: true, sessionId: existing.id, runId: existing.runId };
31526
32500
  }
31527
- const sessionId = crypto25.randomUUID();
31528
- const runId = crypto25.randomUUID();
32501
+ const sessionId = crypto26.randomUUID();
32502
+ const runId = crypto26.randomUUID();
31529
32503
  tx.insert(discoverySessions).values({
31530
32504
  id: sessionId,
31531
32505
  projectId: project.id,
@@ -31583,7 +32557,7 @@ async function discoveryRoutes(app, opts) {
31583
32557
  const project = resolveProject(app.db, request.params.name);
31584
32558
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
31585
32559
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
31586
- const rows = app.db.select().from(discoverySessions).where(eq30(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
32560
+ const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
31587
32561
  return reply.send(rows.map(serializeSession));
31588
32562
  }
31589
32563
  );
@@ -31591,11 +32565,11 @@ async function discoveryRoutes(app, opts) {
31591
32565
  "/projects/:name/discover/sessions/:id",
31592
32566
  async (request, reply) => {
31593
32567
  const project = resolveProject(app.db, request.params.name);
31594
- const session = app.db.select().from(discoverySessions).where(eq30(discoverySessions.id, request.params.id)).get();
32568
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
31595
32569
  if (!session || session.projectId !== project.id) {
31596
32570
  throw notFound("Discovery session", request.params.id);
31597
32571
  }
31598
- const probeRows = app.db.select().from(discoveryProbes).where(eq30(discoveryProbes.sessionId, session.id)).all();
32572
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
31599
32573
  const detail = {
31600
32574
  ...serializeSession(session),
31601
32575
  probes: probeRows.map(serializeProbe)
@@ -31607,12 +32581,12 @@ async function discoveryRoutes(app, opts) {
31607
32581
  "/projects/:name/discover/sessions/:id/promote",
31608
32582
  async (request, reply) => {
31609
32583
  const project = resolveProject(app.db, request.params.name);
31610
- const session = app.db.select().from(discoverySessions).where(eq30(discoverySessions.id, request.params.id)).get();
32584
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
31611
32585
  if (!session || session.projectId !== project.id) {
31612
32586
  throw notFound("Discovery session", request.params.id);
31613
32587
  }
31614
- const probeRows = app.db.select().from(discoveryProbes).where(eq30(discoveryProbes.sessionId, session.id)).all();
31615
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq30(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
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());
31616
32590
  const seenCompetitors = new Set(existingCompetitors);
31617
32591
  const cited = /* @__PURE__ */ new Set();
31618
32592
  const aspirational = /* @__PURE__ */ new Set();
@@ -31641,7 +32615,7 @@ async function discoveryRoutes(app, opts) {
31641
32615
  );
31642
32616
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
31643
32617
  const project = resolveProject(app.db, request.params.name);
31644
- const session = app.db.select().from(discoverySessions).where(eq30(discoverySessions.id, request.params.id)).get();
32618
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
31645
32619
  if (!session || session.projectId !== project.id) {
31646
32620
  throw notFound("Discovery session", request.params.id);
31647
32621
  }
@@ -31664,7 +32638,7 @@ async function discoveryRoutes(app, opts) {
31664
32638
  const bucketSet = new Set(buckets);
31665
32639
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
31666
32640
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
31667
- const probeRows = app.db.select().from(discoveryProbes).where(eq30(discoveryProbes.sessionId, session.id)).all();
32641
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
31668
32642
  const candidateQueries = /* @__PURE__ */ new Set();
31669
32643
  for (const probe of probeRows) {
31670
32644
  if (!probe.bucket) continue;
@@ -31672,7 +32646,7 @@ async function discoveryRoutes(app, opts) {
31672
32646
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
31673
32647
  }
31674
32648
  const existingQueries = new Set(
31675
- app.db.select({ query: queries.query }).from(queries).where(eq30(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
32649
+ app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
31676
32650
  );
31677
32651
  const promotedQueries = [];
31678
32652
  const skippedQueries = [];
@@ -31688,7 +32662,7 @@ async function discoveryRoutes(app, opts) {
31688
32662
  const skippedCompetitors = [];
31689
32663
  if (includeCompetitors) {
31690
32664
  const existingCompetitors = new Set(
31691
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq30(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
32665
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
31692
32666
  );
31693
32667
  const competitorMap = parseCompetitorMap(session.competitorMap);
31694
32668
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -31707,7 +32681,7 @@ async function discoveryRoutes(app, opts) {
31707
32681
  app.db.transaction((tx) => {
31708
32682
  for (const query of promotedQueries) {
31709
32683
  tx.insert(queries).values({
31710
- id: crypto25.randomUUID(),
32684
+ id: crypto26.randomUUID(),
31711
32685
  projectId: project.id,
31712
32686
  query,
31713
32687
  provenance,
@@ -31716,7 +32690,7 @@ async function discoveryRoutes(app, opts) {
31716
32690
  }
31717
32691
  for (const domain of promotedCompetitors) {
31718
32692
  tx.insert(competitors).values({
31719
- id: crypto25.randomUUID(),
32693
+ id: crypto26.randomUUID(),
31720
32694
  projectId: project.id,
31721
32695
  domain,
31722
32696
  provenance,
@@ -31791,8 +32765,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
31791
32765
  }
31792
32766
 
31793
32767
  // ../api-routes/src/discovery/orchestrate.ts
31794
- import crypto26 from "crypto";
31795
- import { eq as eq31 } from "drizzle-orm";
32768
+ import crypto27 from "crypto";
32769
+ import { eq as eq34 } from "drizzle-orm";
31796
32770
  var DEFAULT_MAX_PROBES = 100;
31797
32771
  var ABSOLUTE_MAX_PROBES = 500;
31798
32772
  function classifyProbeBucket(input) {
@@ -31846,7 +32820,7 @@ async function executeDiscovery(opts) {
31846
32820
  status: DiscoverySessionStatuses.seeding,
31847
32821
  dedupThreshold,
31848
32822
  startedAt
31849
- }).where(eq31(discoverySessions.id, opts.sessionId)).run();
32823
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
31850
32824
  const seedResult = await opts.deps.seed({
31851
32825
  project: opts.project,
31852
32826
  icpDescription: opts.icpDescription,
@@ -31872,7 +32846,7 @@ async function executeDiscovery(opts) {
31872
32846
  seedCountRaw,
31873
32847
  seedCount,
31874
32848
  warning
31875
- }).where(eq31(discoverySessions.id, opts.sessionId)).run();
32849
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
31876
32850
  const probeRows = [];
31877
32851
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
31878
32852
  for (const query of probedCanonicals) {
@@ -31885,7 +32859,7 @@ async function executeDiscovery(opts) {
31885
32859
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
31886
32860
  buckets[bucket]++;
31887
32861
  opts.db.insert(discoveryProbes).values({
31888
- id: crypto26.randomUUID(),
32862
+ id: crypto27.randomUUID(),
31889
32863
  sessionId: opts.sessionId,
31890
32864
  projectId: opts.project.id,
31891
32865
  query,
@@ -31912,7 +32886,7 @@ async function executeDiscovery(opts) {
31912
32886
  wastedCount: buckets["wasted-surface"],
31913
32887
  competitorMap,
31914
32888
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
31915
- }).where(eq31(discoverySessions.id, opts.sessionId)).run();
32889
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
31916
32890
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
31917
32891
  return {
31918
32892
  buckets,
@@ -31929,7 +32903,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
31929
32903
  const domain = normalizeDomain(entry.domain);
31930
32904
  if (!domain) continue;
31931
32905
  db.insert(domainClassifications).values({
31932
- id: crypto26.randomUUID(),
32906
+ id: crypto27.randomUUID(),
31933
32907
  projectId,
31934
32908
  domain,
31935
32909
  competitorType: entry.competitorType,
@@ -31952,7 +32926,7 @@ function markSessionFailed(db, sessionId, error) {
31952
32926
  status: DiscoverySessionStatuses.failed,
31953
32927
  error,
31954
32928
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
31955
- }).where(eq31(discoverySessions.id, sessionId)).run();
32929
+ }).where(eq34(discoverySessions.id, sessionId)).run();
31956
32930
  }
31957
32931
  function dedupeStrings(input) {
31958
32932
  const seen = /* @__PURE__ */ new Set();
@@ -31969,8 +32943,8 @@ function dedupeStrings(input) {
31969
32943
  }
31970
32944
 
31971
32945
  // ../api-routes/src/technical-aeo.ts
31972
- import crypto27 from "crypto";
31973
- import { and as and23, asc as asc3, count, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
32946
+ import crypto28 from "crypto";
32947
+ import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
31974
32948
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
31975
32949
  function emptyScore(projectName) {
31976
32950
  return {
@@ -32002,10 +32976,10 @@ function parsePositiveInt(value, fallback, max) {
32002
32976
  async function technicalAeoRoutes(app, opts) {
32003
32977
  app.get("/projects/:name/technical-aeo", async (request) => {
32004
32978
  const project = resolveProject(app.db, request.params.name);
32005
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
32006
- eq32(siteAuditSnapshots.projectId, project.id),
32007
- eq32(runs.kind, RunKinds["site-audit"]),
32008
- inArray11(runs.status, SURFACEABLE_STATUSES),
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"]),
32982
+ inArray12(runs.status, SURFACEABLE_STATUSES),
32009
32983
  notProbeRun()
32010
32984
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
32011
32985
  const latest = rows[0];
@@ -32037,24 +33011,24 @@ async function technicalAeoRoutes(app, opts) {
32037
33011
  });
32038
33012
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
32039
33013
  const project = resolveProject(app.db, request.params.name);
32040
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
32041
- eq32(siteAuditSnapshots.projectId, project.id),
32042
- eq32(runs.kind, RunKinds["site-audit"]),
32043
- inArray11(runs.status, SURFACEABLE_STATUSES),
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"]),
33017
+ inArray12(runs.status, SURFACEABLE_STATUSES),
32044
33018
  notProbeRun()
32045
33019
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
32046
33020
  if (!latest) {
32047
33021
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
32048
33022
  }
32049
33023
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
32050
- const conds = [eq32(siteAuditPages.runId, latest.runId)];
32051
- if (statusFilter) conds.push(eq32(siteAuditPages.status, statusFilter));
32052
- const where = and23(...conds);
33024
+ const conds = [eq35(siteAuditPages.runId, latest.runId)];
33025
+ if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
33026
+ const where = and25(...conds);
32053
33027
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
32054
33028
  const total = totalRow?.value ?? 0;
32055
33029
  const limit = parsePositiveInt(request.query.limit, 100, 500);
32056
33030
  const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
32057
- const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc3(siteAuditPages.url) : asc3(siteAuditPages.overallScore);
33031
+ const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
32058
33032
  const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
32059
33033
  const pages = rows.map((row) => ({
32060
33034
  url: row.url,
@@ -32073,10 +33047,10 @@ async function technicalAeoRoutes(app, opts) {
32073
33047
  auditedAt: siteAuditSnapshots.auditedAt,
32074
33048
  aggregateScore: siteAuditSnapshots.aggregateScore,
32075
33049
  pagesAudited: siteAuditSnapshots.pagesAudited
32076
- }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
32077
- eq32(siteAuditSnapshots.projectId, project.id),
32078
- eq32(runs.kind, RunKinds["site-audit"]),
32079
- inArray11(runs.status, SURFACEABLE_STATUSES),
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"]),
33053
+ inArray12(runs.status, SURFACEABLE_STATUSES),
32080
33054
  notProbeRun()
32081
33055
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
32082
33056
  return { project: project.name, points: rows.reverse() };
@@ -32087,16 +33061,16 @@ async function technicalAeoRoutes(app, opts) {
32087
33061
  if (!parsed.success) {
32088
33062
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
32089
33063
  }
32090
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and23(
32091
- eq32(runs.projectId, project.id),
32092
- eq32(runs.kind, RunKinds["site-audit"]),
32093
- inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
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"]),
33067
+ inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
32094
33068
  )).get();
32095
33069
  if (existing) {
32096
33070
  return { runId: existing.id, status: existing.status };
32097
33071
  }
32098
33072
  const now = (/* @__PURE__ */ new Date()).toISOString();
32099
- const runId = crypto27.randomUUID();
33073
+ const runId = crypto28.randomUUID();
32100
33074
  app.db.insert(runs).values({
32101
33075
  id: runId,
32102
33076
  projectId: project.id,
@@ -32223,6 +33197,11 @@ async function apiRoutes(app, opts) {
32223
33197
  getTelemetryStatus: opts.getTelemetryStatus,
32224
33198
  setTelemetryEnabled: opts.setTelemetryEnabled
32225
33199
  });
33200
+ await api.register(adsRoutes, {
33201
+ adsCredentialStore: opts.adsCredentialStore,
33202
+ verifyAdsAccount: opts.verifyAdsAccount,
33203
+ onAdsSyncRequested: opts.onAdsSyncRequested
33204
+ });
32226
33205
  await api.register(bingRoutes, {
32227
33206
  bingConnectionStore: opts.bingConnectionStore,
32228
33207
  onInspectSitemapRequested: opts.onBingInspectSitemapRequested
@@ -32271,6 +33250,8 @@ async function apiRoutes(app, opts) {
32271
33250
  onInstallBacklinks: opts.onInstallBacklinks,
32272
33251
  onReleaseSyncRequested: opts.onReleaseSyncRequested,
32273
33252
  onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
33253
+ onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
33254
+ bingConnectionStore: opts.bingConnectionStore,
32274
33255
  onBacklinksPruneCache: opts.onBacklinksPruneCache,
32275
33256
  listCachedReleases: opts.listCachedReleases,
32276
33257
  discoverLatestRelease: opts.discoverLatestRelease
@@ -32286,6 +33267,7 @@ async function apiRoutes(app, opts) {
32286
33267
  bingConnectionStore: opts.bingConnectionStore,
32287
33268
  wordpressConnectionStore: opts.wordpressConnectionStore,
32288
33269
  ga4CredentialStore: opts.ga4CredentialStore,
33270
+ adsCredentialStore: opts.adsCredentialStore,
32289
33271
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
32290
33272
  getPlacesConfig: opts.getPlacesConfig,
32291
33273
  publicUrl: opts.publicUrl,
@@ -32436,7 +33418,7 @@ function buildTrafficSourceValidators(opts) {
32436
33418
  }
32437
33419
 
32438
33420
  // src/intelligence-service.ts
32439
- import crypto28 from "crypto";
33421
+ import crypto29 from "crypto";
32440
33422
 
32441
33423
  // src/logger.ts
32442
33424
  var IS_TTY = process.stdout.isTTY === true;
@@ -32668,9 +33650,9 @@ var IntelligenceService = class {
32668
33650
  */
32669
33651
  analyzeAndPersist(runId, projectId) {
32670
33652
  const recentRuns = this.db.select().from(runs).where(
32671
- and24(
32672
- eq33(runs.projectId, projectId),
32673
- or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
33653
+ and26(
33654
+ eq36(runs.projectId, projectId),
33655
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
32674
33656
  // Defensive: RunCoordinator already skips probes before this is
32675
33657
  // called, but if a future call site invokes analyzeAndPersist
32676
33658
  // directly for a probe, probes still must not pollute the
@@ -32752,7 +33734,7 @@ var IntelligenceService = class {
32752
33734
  * Returns the persisted insights so the coordinator can count critical/high.
32753
33735
  */
32754
33736
  analyzeAndPersistGbp(runId, projectId) {
32755
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq33(runs.id, runId)).get();
33737
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
32756
33738
  if (!runRow) {
32757
33739
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
32758
33740
  this.persistGbpInsights(runId, projectId, [], []);
@@ -32760,11 +33742,11 @@ var IntelligenceService = class {
32760
33742
  }
32761
33743
  const windowStart = runRow.startedAt ?? runRow.createdAt;
32762
33744
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
32763
- const selected = this.db.select().from(gbpLocations).where(and24(
32764
- eq33(gbpLocations.projectId, projectId),
32765
- eq33(gbpLocations.selected, true),
32766
- gte6(gbpLocations.syncedAt, windowStart),
32767
- lte3(gbpLocations.syncedAt, windowEnd)
33745
+ const selected = this.db.select().from(gbpLocations).where(and26(
33746
+ eq36(gbpLocations.projectId, projectId),
33747
+ eq36(gbpLocations.selected, true),
33748
+ gte7(gbpLocations.syncedAt, windowStart),
33749
+ lte4(gbpLocations.syncedAt, windowEnd)
32768
33750
  )).all();
32769
33751
  if (selected.length === 0) {
32770
33752
  log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
@@ -32797,10 +33779,10 @@ var IntelligenceService = class {
32797
33779
  }
32798
33780
  /** Build the per-location signal bundle the GBP analyzer consumes. */
32799
33781
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
32800
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and24(eq33(gbpDailyMetrics.projectId, projectId), eq33(gbpDailyMetrics.locationName, locationName))).all();
32801
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and24(eq33(gbpPlaceActions.projectId, projectId), eq33(gbpPlaceActions.locationName, locationName))).all();
32802
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and24(eq33(gbpLodgingSnapshots.projectId, projectId), eq33(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
32803
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and24(eq33(gbpPlaceDetails.projectId, projectId), eq33(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
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();
32804
33786
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
32805
33787
  const summary = buildGbpSummary({
32806
33788
  locationName,
@@ -32832,7 +33814,7 @@ var IntelligenceService = class {
32832
33814
  /** Build the month-over-month keyword series for a location from the
32833
33815
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
32834
33816
  buildGbpKeywordTrend(projectId, locationName) {
32835
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and24(eq33(gbpKeywordMonthly.projectId, projectId), eq33(gbpKeywordMonthly.locationName, locationName))).all();
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();
32836
33818
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
32837
33819
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
32838
33820
  const recentMonth = months[0] ?? null;
@@ -32863,7 +33845,7 @@ var IntelligenceService = class {
32863
33845
  */
32864
33846
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
32865
33847
  const covered = new Set(coveredLocationNames);
32866
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and24(eq33(insights.projectId, projectId), eq33(insights.provider, GBP_INSIGHT_PROVIDER))).all();
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();
32867
33849
  const staleIds = [];
32868
33850
  const dismissedSlots = /* @__PURE__ */ new Set();
32869
33851
  for (const row of existing) {
@@ -32874,7 +33856,7 @@ var IntelligenceService = class {
32874
33856
  }
32875
33857
  this.db.transaction((tx) => {
32876
33858
  for (const id of staleIds) {
32877
- tx.delete(insights).where(eq33(insights.id, id)).run();
33859
+ tx.delete(insights).where(eq36(insights.id, id)).run();
32878
33860
  }
32879
33861
  for (const insight of gbpInsights) {
32880
33862
  const parsed = parseGbpInsightId(insight.id);
@@ -32952,7 +33934,7 @@ var IntelligenceService = class {
32952
33934
  * create per run + aggregate). DB is left untouched.
32953
33935
  */
32954
33936
  backfill(projectName, opts, onProgress) {
32955
- const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
33937
+ const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
32956
33938
  if (!project) {
32957
33939
  throw new Error(`Project "${projectName}" not found`);
32958
33940
  }
@@ -32965,13 +33947,13 @@ var IntelligenceService = class {
32965
33947
  sinceTimestamp = parsed;
32966
33948
  }
32967
33949
  const allRuns = this.db.select().from(runs).where(
32968
- and24(
32969
- eq33(runs.projectId, project.id),
32970
- or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
33950
+ and26(
33951
+ eq36(runs.projectId, project.id),
33952
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
32971
33953
  // Backfill must not replay probe runs as if they were real sweeps.
32972
33954
  ne5(runs.trigger, RunTriggers.probe)
32973
33955
  )
32974
- ).orderBy(asc4(runs.finishedAt)).all();
33956
+ ).orderBy(asc5(runs.finishedAt)).all();
32975
33957
  let startIdx = 0;
32976
33958
  let endIdx = allRuns.length;
32977
33959
  if (opts?.fromRunId) {
@@ -33000,7 +33982,7 @@ var IntelligenceService = class {
33000
33982
  let wouldDeleteTotal = 0;
33001
33983
  const existingByRunId = /* @__PURE__ */ new Map();
33002
33984
  if (isDryRun && targetRuns.length > 0) {
33003
- const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray12(insights.runId, targetRuns.map((r) => r.id))).all();
33985
+ const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray13(insights.runId, targetRuns.map((r) => r.id))).all();
33004
33986
  for (const r of rows) {
33005
33987
  if (r.runId == null) continue;
33006
33988
  existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
@@ -33046,7 +34028,7 @@ var IntelligenceService = class {
33046
34028
  return { processed, skipped, totalInsights };
33047
34029
  }
33048
34030
  loadTrackedCompetitors(projectId) {
33049
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, projectId)).all().map((r) => r.domain);
34031
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
33050
34032
  }
33051
34033
  /**
33052
34034
  * Wipe transition signals from an analysis result while keeping health.
@@ -33067,15 +34049,15 @@ var IntelligenceService = class {
33067
34049
  }
33068
34050
  persistResult(result, runId, projectId) {
33069
34051
  const previouslyDismissed = /* @__PURE__ */ new Set();
33070
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq33(insights.runId, runId)).all();
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();
33071
34053
  for (const row of existingInsights) {
33072
34054
  if (row.dismissed) {
33073
34055
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
33074
34056
  }
33075
34057
  }
33076
34058
  this.db.transaction((tx) => {
33077
- tx.delete(insights).where(eq33(insights.runId, runId)).run();
33078
- tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
34059
+ tx.delete(insights).where(eq36(insights.runId, runId)).run();
34060
+ tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
33079
34061
  const now = (/* @__PURE__ */ new Date()).toISOString();
33080
34062
  for (const insight of result.insights) {
33081
34063
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -33095,7 +34077,7 @@ var IntelligenceService = class {
33095
34077
  }).run();
33096
34078
  }
33097
34079
  tx.insert(healthSnapshots).values({
33098
- id: crypto28.randomUUID(),
34080
+ id: crypto29.randomUUID(),
33099
34081
  projectId,
33100
34082
  runId,
33101
34083
  overallCitedRate: String(result.health.overallCitedRate),
@@ -33126,24 +34108,24 @@ var IntelligenceService = class {
33126
34108
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
33127
34109
  const regressions = rawInsights.filter((i) => i.type === "regression");
33128
34110
  if (regressions.length === 0) return rawInsights;
33129
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq33(gscSearchData.projectId, projectId)).all();
34111
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
33130
34112
  const gscConnected = gscRows.length > 0;
33131
34113
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
33132
34114
  for (const row of gscRows) {
33133
34115
  const key = row.query.toLowerCase();
33134
34116
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
33135
34117
  }
33136
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq33(projects.id, projectId)).get();
34118
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
33137
34119
  const locationCount = Math.max(
33138
34120
  1,
33139
34121
  (projectRow?.locations ?? []).length
33140
34122
  );
33141
34123
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
33142
34124
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
33143
- and24(
33144
- eq33(runs.projectId, projectId),
33145
- eq33(runs.kind, RunKinds["answer-visibility"]),
33146
- or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
34125
+ and26(
34126
+ eq36(runs.projectId, projectId),
34127
+ eq36(runs.kind, RunKinds["answer-visibility"]),
34128
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33147
34129
  // Defensive — see top of file.
33148
34130
  ne5(runs.trigger, RunTriggers.probe)
33149
34131
  )
@@ -33163,7 +34145,7 @@ var IntelligenceService = class {
33163
34145
  const haveHistory = recentRunIds.length > 0;
33164
34146
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
33165
34147
  if (haveHistory) {
33166
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and24(eq33(insights.type, "regression"), inArray12(insights.runId, recentRunIds))).all();
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();
33167
34149
  const regressionGroups = /* @__PURE__ */ new Map();
33168
34150
  for (const row of priorRows) {
33169
34151
  if (!row.runId) continue;
@@ -33192,7 +34174,7 @@ var IntelligenceService = class {
33192
34174
  });
33193
34175
  }
33194
34176
  buildRunData(runId, projectId, completedAt, location = null) {
33195
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq33(projects.id, projectId)).get();
34177
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
33196
34178
  const projectDomains = projectDomainRow ? effectiveDomains({
33197
34179
  canonicalDomain: projectDomainRow.canonicalDomain,
33198
34180
  ownedDomains: projectDomainRow.ownedDomains
@@ -33208,7 +34190,7 @@ var IntelligenceService = class {
33208
34190
  citedDomains: querySnapshots.citedDomains,
33209
34191
  competitorOverlap: querySnapshots.competitorOverlap,
33210
34192
  snapshotLocation: querySnapshots.location
33211
- }).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
34193
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
33212
34194
  const snapshots = [];
33213
34195
  let orphanCount = 0;
33214
34196
  for (const r of rows) {
@@ -33283,6 +34265,11 @@ export {
33283
34265
  gbpPlaceActions,
33284
34266
  gbpLodgingSnapshots,
33285
34267
  gbpPlaceDetails,
34268
+ adsConnections,
34269
+ adsCampaigns,
34270
+ adsAdGroups,
34271
+ adsAds,
34272
+ adsInsightsDaily,
33286
34273
  createClient,
33287
34274
  parseJsonColumn,
33288
34275
  extractLegacyCredentials,
@@ -33315,6 +34302,8 @@ export {
33315
34302
  hashLodging,
33316
34303
  getUrlInfo,
33317
34304
  getCrawlIssues,
34305
+ getLinkCounts,
34306
+ getUrlLinks,
33318
34307
  PLUGIN_DIR,
33319
34308
  DUCKDB_SPEC,
33320
34309
  CC_CACHE_DIR,