@canonry/canonry 4.117.1 → 4.118.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 (30) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +74 -1
  2. package/assets/assets/AuditHistoryPanel-Yh7VEe4P.js +1 -0
  3. package/assets/assets/{BacklinksPage-Bkafm10m.js → BacklinksPage-BmO77kMs.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-NV_GgqFn.js → ChartPrimitives-H0ICVpw_.js} +1 -1
  5. package/assets/assets/HistoryPage-DkLV_iRc.js +1 -0
  6. package/assets/assets/ProjectPage-R3odr_eA.js +7 -0
  7. package/assets/assets/{RunRow-df0d24S6.js → RunRow-BDH9agxQ.js} +1 -1
  8. package/assets/assets/RunsPage-D9YsEeT3.js +1 -0
  9. package/assets/assets/{SettingsPage-BKw6B6cd.js → SettingsPage-o5as3Ixw.js} +1 -1
  10. package/assets/assets/{TrafficPage-CzpiYHKj.js → TrafficPage-BCK203aQ.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-CC5nuqM1.js → TrafficSourceDetailPage-DsVMAZvS.js} +1 -1
  12. package/assets/assets/{arrow-left-CbQcW2pL.js → arrow-left-B7ZR6VC9.js} +1 -1
  13. package/assets/assets/{extract-error-message-CvqIZ-le.js → extract-error-message-DeYDUpew.js} +1 -1
  14. package/assets/assets/index-Bot6YiD6.css +1 -0
  15. package/assets/assets/index-QCFQv0xh.js +210 -0
  16. package/assets/assets/{trash-2-3J2u0woR.js → trash-2-xxHTqWbF.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-BMBHUFMI.js → chunk-75UBTCKS.js} +540 -16
  19. package/dist/{chunk-V4SW7SVX.js → chunk-CMZIPFNQ.js} +1598 -486
  20. package/dist/{chunk-NY7SC4ES.js → chunk-HGNY5GL4.js} +384 -15
  21. package/dist/{chunk-IAJ5TQ4S.js → chunk-KEZWLP4O.js} +1385 -1145
  22. package/dist/cli.js +390 -113
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-C5YLASIP.js → intelligence-service-FWPXMAVK.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +7 -7
  27. package/assets/assets/ProjectPage-0-NnRxys.js +0 -7
  28. package/assets/assets/RunsPage-CU6-4YTM.js +0 -1
  29. package/assets/assets/index-B4R1-8hR.css +0 -1
  30. package/assets/assets/index-LjcWHaNZ.js +0 -210
@@ -1,7 +1,12 @@
1
1
  import {
2
+ ADS_WRITE_SCOPE,
2
3
  AGENT_PROVIDER_IDS,
3
4
  AI_ENGINE_DOMAINS,
4
5
  AI_PROVIDER_INFRA_DOMAINS,
6
+ AdsEntityStatuses,
7
+ AdsEntityTypes,
8
+ AdsOperationKinds,
9
+ AdsOperationStates,
5
10
  AiReferralTrafficClasses,
6
11
  AppError,
7
12
  BacklinkSources,
@@ -38,19 +43,29 @@ import {
38
43
  __export,
39
44
  absolutizeProjectUrl,
40
45
  actionConfidenceLabel,
46
+ adsAdCreateRequestSchema,
47
+ adsAdGroupCreateRequestSchema,
48
+ adsAdGroupUpdateRequestSchema,
49
+ adsAdUpdateRequestSchema,
50
+ adsCampaignCreateRequestSchema,
41
51
  adsCampaignListResponseSchema,
52
+ adsCampaignUpdateRequestSchema,
42
53
  adsConnectRequestSchema,
43
54
  adsConnectionStatusDtoSchema,
44
55
  adsCpcMicros,
45
56
  adsCtr,
46
57
  adsDisconnectResponseSchema,
58
+ adsImageUploadRequestSchema,
47
59
  adsInsightLevelSchema,
48
60
  adsInsightsResponseSchema,
61
+ adsOperationResponseSchema,
62
+ adsPauseRequestSchema,
49
63
  adsSummaryDtoSchema,
50
64
  adsSyncResponseSchema,
51
65
  agentProvidersResponseDtoSchema,
52
66
  aggregateHarvestedQueries,
53
67
  aiReferralClassCounts,
68
+ alreadyExists,
54
69
  apiKeyDtoSchema,
55
70
  apiKeyListDtoSchema,
56
71
  applyHarvestSemanticNovelty,
@@ -173,6 +188,7 @@ import {
173
188
  gscSitemapListResponseDtoSchema,
174
189
  gscUrlInspectionDtoSchema,
175
190
  hasLocationLabel,
191
+ healthSnapshotDtoSchema,
176
192
  hostOf,
177
193
  indexingRequestResponseDtoSchema,
178
194
  internalError,
@@ -220,11 +236,14 @@ import {
220
236
  resolveConfigSpecQueries,
221
237
  resolveLocations,
222
238
  resolveSnapshotRequestQueries,
239
+ resultsExportDtoSchema,
223
240
  runDetailDtoSchema,
224
241
  runDtoSchema,
225
242
  runInProgress,
226
243
  runNotCancellable,
244
+ runStatusSchema,
227
245
  runTriggerRequestSchema,
246
+ runTriggerSchema,
228
247
  runtimeStateMissing,
229
248
  schedulableRunKindSchema,
230
249
  scheduleDtoSchema,
@@ -280,10 +299,10 @@ import {
280
299
  wordpressSchemaDeployResultDtoSchema,
281
300
  wordpressSchemaStatusResultDtoSchema,
282
301
  wordpressStatusDtoSchema
283
- } from "./chunk-IAJ5TQ4S.js";
302
+ } from "./chunk-KEZWLP4O.js";
284
303
 
285
304
  // src/intelligence-service.ts
286
- import { eq as eq39, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray15, gte as gte7, lte as lte4 } from "drizzle-orm";
305
+ import { eq as eq40, desc as desc18, asc as asc8, and as and31, ne as ne5, or as or6, inArray as inArray15, gte as gte9, lte as lte5 } from "drizzle-orm";
287
306
 
288
307
  // ../db/src/client.ts
289
308
  import { mkdirSync } from "fs";
@@ -299,6 +318,7 @@ __export(schema_exports, {
299
318
  adsCampaigns: () => adsCampaigns,
300
319
  adsConnections: () => adsConnections,
301
320
  adsInsightsDaily: () => adsInsightsDaily,
321
+ adsOperations: () => adsOperations,
302
322
  agentMemory: () => agentMemory,
303
323
  agentSessions: () => agentSessions,
304
324
  agentToolEvents: () => agentToolEvents,
@@ -1441,11 +1461,33 @@ var adsConnections = sqliteTable("ads_connections", {
1441
1461
  }, (table) => [
1442
1462
  uniqueIndex("idx_ads_conn_project").on(table.projectId)
1443
1463
  ]);
1464
+ var adsOperations = sqliteTable("ads_operations", {
1465
+ id: text("id").primaryKey(),
1466
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1467
+ operationKey: text("operation_key").notNull(),
1468
+ requestHash: text("request_hash").notNull(),
1469
+ kind: text("kind").notNull(),
1470
+ state: text("state").notNull(),
1471
+ entityType: text("entity_type"),
1472
+ entityId: text("entity_id"),
1473
+ upstreamUpdatedAt: integer("upstream_updated_at"),
1474
+ errorCode: text("error_code"),
1475
+ errorMessage: text("error_message"),
1476
+ createdAt: text("created_at").notNull(),
1477
+ updatedAt: text("updated_at").notNull()
1478
+ }, (table) => [
1479
+ uniqueIndex("idx_ads_operations_project_key").on(table.projectId, table.operationKey),
1480
+ index("idx_ads_operations_project_created").on(table.projectId, table.createdAt),
1481
+ index("idx_ads_operations_project_state").on(table.projectId, table.state)
1482
+ ]);
1444
1483
  var adsCampaigns = sqliteTable("ads_campaigns", {
1445
1484
  id: text("id").primaryKey(),
1446
1485
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1447
1486
  name: text("name").notNull(),
1487
+ description: text("description"),
1448
1488
  status: text("status").notNull(),
1489
+ startTime: integer("start_time"),
1490
+ endTime: integer("end_time"),
1449
1491
  biddingType: text("bidding_type"),
1450
1492
  dailySpendLimitMicros: integer("daily_spend_limit_micros"),
1451
1493
  lifetimeSpendLimitMicros: integer("lifetime_spend_limit_micros"),
@@ -1462,6 +1504,7 @@ var adsAdGroups = sqliteTable("ads_ad_groups", {
1462
1504
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1463
1505
  campaignId: text("campaign_id").notNull().references(() => adsCampaigns.id, { onDelete: "cascade" }),
1464
1506
  name: text("name").notNull(),
1507
+ description: text("description"),
1465
1508
  status: text("status").notNull(),
1466
1509
  billingEventType: text("billing_event_type"),
1467
1510
  maxBidMicros: integer("max_bid_micros"),
@@ -3597,6 +3640,34 @@ var MIGRATION_VERSIONS = [
3597
3640
  run: (tx) => {
3598
3641
  relinkOrphanedSnapshotQueryIds(tx);
3599
3642
  }
3643
+ },
3644
+ {
3645
+ version: 99,
3646
+ name: "ads-operator-lifecycle",
3647
+ statements: [
3648
+ `ALTER TABLE ads_campaigns ADD COLUMN description TEXT`,
3649
+ `ALTER TABLE ads_campaigns ADD COLUMN start_time INTEGER`,
3650
+ `ALTER TABLE ads_campaigns ADD COLUMN end_time INTEGER`,
3651
+ `ALTER TABLE ads_ad_groups ADD COLUMN description TEXT`,
3652
+ `CREATE TABLE IF NOT EXISTS ads_operations (
3653
+ id TEXT PRIMARY KEY,
3654
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3655
+ operation_key TEXT NOT NULL,
3656
+ request_hash TEXT NOT NULL,
3657
+ kind TEXT NOT NULL,
3658
+ state TEXT NOT NULL,
3659
+ entity_type TEXT,
3660
+ entity_id TEXT,
3661
+ upstream_updated_at INTEGER,
3662
+ error_code TEXT,
3663
+ error_message TEXT,
3664
+ created_at TEXT NOT NULL,
3665
+ updated_at TEXT NOT NULL
3666
+ )`,
3667
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ads_operations_project_key ON ads_operations(project_id, operation_key)`,
3668
+ `CREATE INDEX IF NOT EXISTS idx_ads_operations_project_created ON ads_operations(project_id, created_at)`,
3669
+ `CREATE INDEX IF NOT EXISTS idx_ads_operations_project_state ON ads_operations(project_id, state)`
3670
+ ]
3600
3671
  }
3601
3672
  ];
3602
3673
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -5945,6 +6016,14 @@ import fs8 from "fs";
5945
6016
  import crypto2 from "crypto";
5946
6017
  import { eq } from "drizzle-orm";
5947
6018
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
6019
+ function isAdsWriteOnlyKey(scopes) {
6020
+ const writeGrants = scopes.filter((scope) => scope === "*" || scope === "write" || scope.endsWith(".write"));
6021
+ return writeGrants.length === 1 && writeGrants[0] === ADS_WRITE_SCOPE;
6022
+ }
6023
+ function isAdsWriteRoute(url) {
6024
+ const rest = projectRouteRest(url);
6025
+ return rest !== null && rest.startsWith("ads/");
6026
+ }
5948
6027
  function requireScope(request, scope) {
5949
6028
  const key = request.apiKey;
5950
6029
  if (!key) return;
@@ -6097,6 +6176,9 @@ async function authPlugin(app, opts = {}) {
6097
6176
  if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
6098
6177
  throw forbidden("This API key is read-only and cannot perform write operations.");
6099
6178
  }
6179
+ if (isAdsWriteOnlyKey(scopes) && WRITE_METHODS.has(request.method) && !isAdsWriteRoute(url)) {
6180
+ throw forbidden("This API key can only perform OpenAI Ads write operations.");
6181
+ }
6100
6182
  enforceEmbedProjectTabs(request, opts.embedProjectTabs);
6101
6183
  if (key.projectId) {
6102
6184
  const match = url.match(/\/projects\/([^/?#]+)/);
@@ -7991,7 +8073,7 @@ function normalizeCompetitorList2(domains) {
7991
8073
  }
7992
8074
 
7993
8075
  // ../api-routes/src/history.ts
7994
- import { and as and6, eq as eq10, desc as desc2, inArray as inArray3 } from "drizzle-orm";
8076
+ import { and as and6, asc as asc2, desc as desc2, eq as eq10, gte as gte2, inArray as inArray3 } from "drizzle-orm";
7995
8077
 
7996
8078
  // ../api-routes/src/notification-redaction.ts
7997
8079
  var REDACTED_URL = {
@@ -8037,12 +8119,16 @@ function redactNotificationDiff(value) {
8037
8119
  async function historyRoutes(app) {
8038
8120
  app.get("/projects/:name/history", async (request, reply) => {
8039
8121
  const project = resolveProject(app.db, request.params.name);
8040
- const rows = app.db.select().from(auditLog).where(eq10(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
8122
+ const filters = [eq10(auditLog.projectId, project.id)];
8123
+ addAuditHistoryFilters(filters, request.query);
8124
+ const rows = app.db.select().from(auditLog).where(and6(...filters)).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
8041
8125
  return reply.send(rows.map(formatAuditEntry));
8042
8126
  });
8043
8127
  app.get("/history", async (request, reply) => {
8044
8128
  const scopedProjectId = request.apiKey?.projectId;
8045
- const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq10(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
8129
+ const filters = scopedProjectId ? [eq10(auditLog.projectId, scopedProjectId)] : [];
8130
+ addAuditHistoryFilters(filters, request.query);
8131
+ const rows = app.db.select().from(auditLog).where(filters.length > 0 ? and6(...filters) : void 0).orderBy(desc2(auditLog.createdAt)).limit(parseBoundedInt(request.query.limit, 100, 500)).offset(parseBoundedInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER)).all();
8046
8132
  return reply.send(rows.map(formatAuditEntry));
8047
8133
  });
8048
8134
  app.get("/projects/:name/snapshots", async (request, reply) => {
@@ -8098,7 +8184,8 @@ async function historyRoutes(app) {
8098
8184
  app.get("/projects/:name/timeline", async (request, reply) => {
8099
8185
  const project = resolveProject(app.db, request.params.name);
8100
8186
  const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
8101
- const projectRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
8187
+ const requestedLimit = parseOptionalPositiveInt(request.query.limit, 100);
8188
+ const projectRuns = requestedLimit == null ? app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(asc2(runs.createdAt)).all() : app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc2(runs.createdAt)).limit(requestedLimit).all().reverse();
8102
8189
  if (projectRuns.length === 0 || projectQueries.length === 0) {
8103
8190
  return reply.send([]);
8104
8191
  }
@@ -8248,29 +8335,23 @@ async function historyRoutes(app) {
8248
8335
  }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
8249
8336
  const map1 = /* @__PURE__ */ new Map();
8250
8337
  for (const s of snaps1) {
8251
- const resolved = {
8252
- ...s,
8253
- resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
8254
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
8255
- resolvedMentionState: resolveSnapshotMentionState(s, project)
8256
- };
8257
8338
  const existing = map1.get(s.queryId);
8258
- if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
8259
- map1.set(s.queryId, resolved);
8260
- }
8339
+ map1.set(s.queryId, {
8340
+ ...s,
8341
+ query: existing?.query ?? s.query,
8342
+ citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
8343
+ resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
8344
+ });
8261
8345
  }
8262
8346
  const map2 = /* @__PURE__ */ new Map();
8263
8347
  for (const s of snaps2) {
8264
- const resolved = {
8265
- ...s,
8266
- resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
8267
- resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
8268
- resolvedMentionState: resolveSnapshotMentionState(s, project)
8269
- };
8270
8348
  const existing = map2.get(s.queryId);
8271
- if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
8272
- map2.set(s.queryId, resolved);
8273
- }
8349
+ map2.set(s.queryId, {
8350
+ ...s,
8351
+ query: existing?.query ?? s.query,
8352
+ citationState: existing?.citationState === CitationStates.cited || s.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
8353
+ resolvedAnswerMentioned: (existing?.resolvedAnswerMentioned ?? false) || resolveSnapshotAnswerMentioned(s, project)
8354
+ });
8274
8355
  }
8275
8356
  const allQueryIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
8276
8357
  const diff = [...allQueryIds].map((qId) => {
@@ -8284,10 +8365,10 @@ async function historyRoutes(app) {
8284
8365
  run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
8285
8366
  run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
8286
8367
  // Legacy aliases — same data as run{1,2}MentionState below.
8287
- run1VisibilityState: s1?.resolvedVisibilityState ?? null,
8288
- run2VisibilityState: s2?.resolvedVisibilityState ?? null,
8289
- run1MentionState: s1?.resolvedMentionState ?? null,
8290
- run2MentionState: s2?.resolvedMentionState ?? null,
8368
+ run1VisibilityState: s1 ? visibilityStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
8369
+ run2VisibilityState: s2 ? visibilityStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
8370
+ run1MentionState: s1 ? mentionStateFromAnswerMentioned(s1.resolvedAnswerMentioned) : null,
8371
+ run2MentionState: s2 ? mentionStateFromAnswerMentioned(s2.resolvedAnswerMentioned) : null,
8291
8372
  changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
8292
8373
  visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
8293
8374
  };
@@ -8304,9 +8385,28 @@ function formatAuditEntry(row) {
8304
8385
  entityType: row.entityType,
8305
8386
  entityId: row.entityId,
8306
8387
  diff: row.diff ? row.entityType === "notification" ? redactNotificationDiff(parseJsonColumn(row.diff, null)) : parseJsonColumn(row.diff, null) : null,
8388
+ userAgent: row.userAgent,
8389
+ actorSession: row.actorSession,
8307
8390
  createdAt: row.createdAt
8308
8391
  };
8309
8392
  }
8393
+ function addAuditHistoryFilters(filters, query) {
8394
+ if (query.since && !Number.isNaN(Date.parse(query.since))) filters.push(gte2(auditLog.createdAt, query.since));
8395
+ if (query.action) filters.push(eq10(auditLog.action, query.action));
8396
+ if (query.actor) filters.push(eq10(auditLog.actor, query.actor));
8397
+ if (query.entityType) filters.push(eq10(auditLog.entityType, query.entityType));
8398
+ }
8399
+ function parseBoundedInt(value, fallback, max) {
8400
+ const parsed = Number.parseInt(value ?? "", 10);
8401
+ if (!Number.isFinite(parsed) || parsed < 0) return fallback;
8402
+ return Math.min(parsed, max);
8403
+ }
8404
+ function parseOptionalPositiveInt(value, max) {
8405
+ if (value == null) return void 0;
8406
+ const parsed = Number.parseInt(value, 10);
8407
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
8408
+ return Math.min(parsed, max);
8409
+ }
8310
8410
 
8311
8411
  // ../api-routes/src/analytics.ts
8312
8412
  import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
@@ -8970,7 +9070,7 @@ async function intelligenceRoutes(app) {
8970
9070
  }
8971
9071
 
8972
9072
  // ../api-routes/src/report.ts
8973
- import { and as and12, desc as desc7, eq as eq16, gte as gte2, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
9073
+ import { and as and12, desc as desc7, eq as eq16, gte as gte3, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
8974
9074
 
8975
9075
  // ../api-routes/src/content.ts
8976
9076
  import crypto12 from "crypto";
@@ -9706,7 +9806,7 @@ function winnabilityClassRank(winnabilityClass) {
9706
9806
  }
9707
9807
 
9708
9808
  // ../api-routes/src/gsc-totals.ts
9709
- import { and as and11, asc as asc2, eq as eq15, sql as sql6 } from "drizzle-orm";
9809
+ import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
9710
9810
  function readGscDailyTotals(db, projectId, startDate, endDate) {
9711
9811
  const rows = db.select({
9712
9812
  date: gscDailyTotals.date,
@@ -9719,7 +9819,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
9719
9819
  sql6`${gscDailyTotals.date} >= ${startDate}`,
9720
9820
  sql6`${gscDailyTotals.date} <= ${endDate}`
9721
9821
  )
9722
- ).orderBy(asc2(gscDailyTotals.date)).all();
9822
+ ).orderBy(asc3(gscDailyTotals.date)).all();
9723
9823
  return rows.map((r) => {
9724
9824
  const position = Number(r.position);
9725
9825
  return {
@@ -12186,7 +12286,7 @@ function buildGaSection(db, projectId, windowDays) {
12186
12286
  eq16(gaAiReferrals.sourceDimension, "session")
12187
12287
  ];
12188
12288
  if (snapshotStartDate && snapshotMaxDate) {
12189
- aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
12289
+ aiConditions.push(gte3(gaAiReferrals.date, snapshotStartDate));
12190
12290
  aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
12191
12291
  }
12192
12292
  const aiSessionRows = db.select({
@@ -12403,7 +12503,7 @@ function buildServerActivity(db, projectId, windowDays) {
12403
12503
  and12(
12404
12504
  eq16(crawlerEventsHourly.projectId, projectId),
12405
12505
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12406
- gte2(crawlerEventsHourly.tsHour, windowStartIso),
12506
+ gte3(crawlerEventsHourly.tsHour, windowStartIso),
12407
12507
  exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
12408
12508
  )
12409
12509
  ).get()?.total ?? 0
@@ -12413,7 +12513,7 @@ function buildServerActivity(db, projectId, windowDays) {
12413
12513
  and12(
12414
12514
  eq16(crawlerEventsHourly.projectId, projectId),
12415
12515
  ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12416
- gte2(crawlerEventsHourly.tsHour, windowStartIso),
12516
+ gte3(crawlerEventsHourly.tsHour, windowStartIso),
12417
12517
  exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
12418
12518
  )
12419
12519
  ).get()?.total ?? 0
@@ -12427,7 +12527,7 @@ function buildServerActivity(db, projectId, windowDays) {
12427
12527
  and12(
12428
12528
  eq16(aiReferralEventsHourly.projectId, projectId),
12429
12529
  nonSubresourceReferralPathCondition(),
12430
- gte2(aiReferralEventsHourly.tsHour, windowStartIso),
12530
+ gte3(aiReferralEventsHourly.tsHour, windowStartIso),
12431
12531
  exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
12432
12532
  )
12433
12533
  ).get();
@@ -12437,7 +12537,7 @@ function buildServerActivity(db, projectId, windowDays) {
12437
12537
  db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
12438
12538
  and12(
12439
12539
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12440
- gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
12540
+ gte3(aiUserFetchEventsHourly.tsHour, windowStartIso),
12441
12541
  exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
12442
12542
  )
12443
12543
  ).get()?.total ?? 0
@@ -12457,7 +12557,7 @@ function buildServerActivity(db, projectId, windowDays) {
12457
12557
  }).from(crawlerEventsHourly).where(
12458
12558
  and12(
12459
12559
  eq16(crawlerEventsHourly.projectId, projectId),
12460
- gte2(crawlerEventsHourly.tsHour, headlineStart),
12560
+ gte3(crawlerEventsHourly.tsHour, headlineStart),
12461
12561
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12462
12562
  )
12463
12563
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
@@ -12468,7 +12568,7 @@ function buildServerActivity(db, projectId, windowDays) {
12468
12568
  and12(
12469
12569
  eq16(crawlerEventsHourly.projectId, projectId),
12470
12570
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12471
- gte2(crawlerEventsHourly.tsHour, priorStart),
12571
+ gte3(crawlerEventsHourly.tsHour, priorStart),
12472
12572
  lt(crawlerEventsHourly.tsHour, headlineStart)
12473
12573
  )
12474
12574
  ).groupBy(crawlerEventsHourly.operator).all();
@@ -12479,7 +12579,7 @@ function buildServerActivity(db, projectId, windowDays) {
12479
12579
  and12(
12480
12580
  eq16(aiReferralEventsHourly.projectId, projectId),
12481
12581
  nonSubresourceReferralPathCondition(),
12482
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12582
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12483
12583
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12484
12584
  )
12485
12585
  ).groupBy(aiReferralEventsHourly.operator).all();
@@ -12489,7 +12589,7 @@ function buildServerActivity(db, projectId, windowDays) {
12489
12589
  }).from(aiUserFetchEventsHourly).where(
12490
12590
  and12(
12491
12591
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12492
- gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
12592
+ gte3(aiUserFetchEventsHourly.tsHour, headlineStart),
12493
12593
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
12494
12594
  )
12495
12595
  ).groupBy(aiUserFetchEventsHourly.operator).all();
@@ -12534,7 +12634,7 @@ function buildServerActivity(db, projectId, windowDays) {
12534
12634
  and12(
12535
12635
  eq16(crawlerEventsHourly.projectId, projectId),
12536
12636
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12537
- gte2(crawlerEventsHourly.tsHour, headlineStart),
12637
+ gte3(crawlerEventsHourly.tsHour, headlineStart),
12538
12638
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12539
12639
  )
12540
12640
  ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
@@ -12551,7 +12651,7 @@ function buildServerActivity(db, projectId, windowDays) {
12551
12651
  and12(
12552
12652
  eq16(aiReferralEventsHourly.projectId, projectId),
12553
12653
  nonSubresourceReferralPathCondition(),
12554
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12654
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12555
12655
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12556
12656
  )
12557
12657
  ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
@@ -12568,7 +12668,7 @@ function buildServerActivity(db, projectId, windowDays) {
12568
12668
  and12(
12569
12669
  eq16(aiReferralEventsHourly.projectId, projectId),
12570
12670
  nonSubresourceReferralPathCondition(),
12571
- gte2(aiReferralEventsHourly.tsHour, headlineStart),
12671
+ gte3(aiReferralEventsHourly.tsHour, headlineStart),
12572
12672
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12573
12673
  )
12574
12674
  ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
@@ -12584,7 +12684,7 @@ function buildServerActivity(db, projectId, windowDays) {
12584
12684
  and12(
12585
12685
  eq16(crawlerEventsHourly.projectId, projectId),
12586
12686
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12587
- gte2(crawlerEventsHourly.tsHour, trendStart),
12687
+ gte3(crawlerEventsHourly.tsHour, trendStart),
12588
12688
  lte(crawlerEventsHourly.tsHour, headlineEnd)
12589
12689
  )
12590
12690
  ).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
@@ -12595,7 +12695,7 @@ function buildServerActivity(db, projectId, windowDays) {
12595
12695
  and12(
12596
12696
  eq16(aiReferralEventsHourly.projectId, projectId),
12597
12697
  nonSubresourceReferralPathCondition(),
12598
- gte2(aiReferralEventsHourly.tsHour, trendStart),
12698
+ gte3(aiReferralEventsHourly.tsHour, trendStart),
12599
12699
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
12600
12700
  )
12601
12701
  ).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
@@ -12605,7 +12705,7 @@ function buildServerActivity(db, projectId, windowDays) {
12605
12705
  }).from(aiUserFetchEventsHourly).where(
12606
12706
  and12(
12607
12707
  eq16(aiUserFetchEventsHourly.projectId, projectId),
12608
- gte2(aiUserFetchEventsHourly.tsHour, trendStart),
12708
+ gte3(aiUserFetchEventsHourly.tsHour, trendStart),
12609
12709
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
12610
12710
  )
12611
12711
  ).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
@@ -14239,8 +14339,226 @@ async function visibilityStatsRoutes(app) {
14239
14339
  });
14240
14340
  }
14241
14341
 
14342
+ // ../api-routes/src/results-export.ts
14343
+ import { and as and15, asc as asc4, eq as eq19, gte as gte4, lte as lte2 } from "drizzle-orm";
14344
+ var CSV_COLUMNS = [
14345
+ "export_schema_version",
14346
+ "project_id",
14347
+ "project_name",
14348
+ "project_display_name",
14349
+ "canonical_domain",
14350
+ "run_id",
14351
+ "run_kind",
14352
+ "run_status",
14353
+ "run_trigger",
14354
+ "run_created_at",
14355
+ "run_started_at",
14356
+ "run_finished_at",
14357
+ "snapshot_id",
14358
+ "snapshot_created_at",
14359
+ "query_id",
14360
+ "query",
14361
+ "provider",
14362
+ "model",
14363
+ "location",
14364
+ "citation_state",
14365
+ "cited",
14366
+ "answer_mentioned",
14367
+ "mention_state",
14368
+ "cited_domains_json",
14369
+ "competitor_overlap_json",
14370
+ "recommended_competitors_json",
14371
+ "answer_text",
14372
+ "grounding_sources_json",
14373
+ "search_queries_json"
14374
+ ];
14375
+ function parseFormat(value) {
14376
+ if (value === void 0 || value === "" || value === "json") return "json";
14377
+ if (value === "csv") return "csv";
14378
+ throw validationError('"format" must be "json" or "csv"');
14379
+ }
14380
+ function parseBoolean(value, name) {
14381
+ if (value === void 0 || value === "") return false;
14382
+ if (value === "true" || value === "1") return true;
14383
+ if (value === "false" || value === "0") return false;
14384
+ throw validationError(`"${name}" must be "true" or "false"`);
14385
+ }
14386
+ function parseDateBounds(since, until) {
14387
+ let sinceIso = null;
14388
+ let untilIso = null;
14389
+ if (since !== void 0 && since !== "") {
14390
+ const ms = Date.parse(since);
14391
+ if (Number.isNaN(ms)) throw validationError('"since" must be an ISO 8601 date/time');
14392
+ sinceIso = new Date(ms).toISOString();
14393
+ }
14394
+ if (until !== void 0 && until !== "") {
14395
+ const ms = parseInclusiveEndMs(until);
14396
+ if (ms === null) throw validationError('"until" must be an ISO 8601 date/time');
14397
+ untilIso = new Date(ms).toISOString();
14398
+ }
14399
+ if (sinceIso !== null && untilIso !== null && untilIso < sinceIso) {
14400
+ throw validationError('"until" must be on or after "since"');
14401
+ }
14402
+ return { sinceIso, untilIso };
14403
+ }
14404
+ function readEvidence(rawResponse) {
14405
+ const parsed = parseJsonColumn(rawResponse, {});
14406
+ const groundingSources = Array.isArray(parsed.groundingSources) ? parsed.groundingSources.filter(
14407
+ (source) => typeof source === "object" && source !== null && typeof source.uri === "string"
14408
+ ).map((source) => ({ uri: source.uri, title: typeof source.title === "string" ? source.title : "" })) : [];
14409
+ const searchQueries = Array.isArray(parsed.searchQueries) ? parsed.searchQueries.filter((query) => typeof query === "string") : [];
14410
+ return { groundingSources, searchQueries };
14411
+ }
14412
+ function recordMentionState(answerMentioned) {
14413
+ if (answerMentioned === null) return null;
14414
+ return answerMentioned ? "mentioned" : "not-mentioned";
14415
+ }
14416
+ function spreadsheetSafe(value) {
14417
+ return /^[\t\r ]*[=+\-@]/.test(value) ? `'${value}` : value;
14418
+ }
14419
+ function csvCell(value) {
14420
+ if (value === null) return "";
14421
+ const text2 = typeof value === "string" ? spreadsheetSafe(value) : String(value);
14422
+ return /[",\r\n]/.test(text2) ? `"${text2.replace(/"/g, '""')}"` : text2;
14423
+ }
14424
+ function renderResultsExportCsv(report) {
14425
+ const rows = report.records.map((record) => [
14426
+ report.schemaVersion,
14427
+ report.project.id,
14428
+ report.project.name,
14429
+ report.project.displayName,
14430
+ report.project.canonicalDomain,
14431
+ record.runId,
14432
+ record.runKind,
14433
+ record.runStatus,
14434
+ record.runTrigger,
14435
+ record.runCreatedAt,
14436
+ record.runStartedAt,
14437
+ record.runFinishedAt,
14438
+ record.snapshotId,
14439
+ record.snapshotCreatedAt,
14440
+ record.queryId,
14441
+ record.query,
14442
+ record.provider,
14443
+ record.model,
14444
+ record.location,
14445
+ record.citationState,
14446
+ record.cited,
14447
+ record.answerMentioned,
14448
+ record.mentionState,
14449
+ JSON.stringify(record.citedDomains),
14450
+ JSON.stringify(record.competitorOverlap),
14451
+ JSON.stringify(record.recommendedCompetitors),
14452
+ record.answerText,
14453
+ JSON.stringify(record.groundingSources),
14454
+ JSON.stringify(record.searchQueries)
14455
+ ].map(csvCell).join(","));
14456
+ return [CSV_COLUMNS.join(","), ...rows].join("\r\n") + "\r\n";
14457
+ }
14458
+ function filenameFor(projectName, format) {
14459
+ const safeName = projectName.replace(/[^\w.-]+/gi, "_") || "project";
14460
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14461
+ return `canonry-results-${safeName}-${date}.${format}`;
14462
+ }
14463
+ async function resultsExportRoutes(app) {
14464
+ app.get("/projects/:name/results/export", async (request, reply) => {
14465
+ const project = resolveProject(app.db, request.params.name);
14466
+ const format = parseFormat(request.query.format);
14467
+ const includeProbes = parseBoolean(request.query.includeProbes, "includeProbes");
14468
+ const { sinceIso, untilIso } = parseDateBounds(request.query.since, request.query.until);
14469
+ const conditions = [
14470
+ eq19(runs.projectId, project.id),
14471
+ eq19(runs.kind, RunKinds["answer-visibility"]),
14472
+ ...includeProbes ? [] : [notProbeRun()],
14473
+ ...sinceIso ? [gte4(runs.createdAt, sinceIso)] : [],
14474
+ ...untilIso ? [lte2(runs.createdAt, untilIso)] : []
14475
+ ];
14476
+ const rows = app.db.select({
14477
+ runId: runs.id,
14478
+ runStatus: runs.status,
14479
+ runTrigger: runs.trigger,
14480
+ runCreatedAt: runs.createdAt,
14481
+ runStartedAt: runs.startedAt,
14482
+ runFinishedAt: runs.finishedAt,
14483
+ snapshotId: querySnapshots.id,
14484
+ snapshotCreatedAt: querySnapshots.createdAt,
14485
+ queryId: querySnapshots.queryId,
14486
+ queryText: querySnapshots.queryText,
14487
+ currentQuery: queries.query,
14488
+ provider: querySnapshots.provider,
14489
+ model: querySnapshots.model,
14490
+ location: querySnapshots.location,
14491
+ citationState: querySnapshots.citationState,
14492
+ answerMentioned: querySnapshots.answerMentioned,
14493
+ citedDomains: querySnapshots.citedDomains,
14494
+ competitorOverlap: querySnapshots.competitorOverlap,
14495
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
14496
+ answerText: querySnapshots.answerText,
14497
+ rawResponse: querySnapshots.rawResponse
14498
+ }).from(querySnapshots).innerJoin(runs, eq19(querySnapshots.runId, runs.id)).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(and15(...conditions)).orderBy(asc4(runs.createdAt), asc4(runs.id), asc4(querySnapshots.createdAt), asc4(querySnapshots.id)).all();
14499
+ const records = rows.map((row) => {
14500
+ const evidence = readEvidence(row.rawResponse);
14501
+ const citationState = row.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"];
14502
+ return {
14503
+ runId: row.runId,
14504
+ runKind: RunKinds["answer-visibility"],
14505
+ runStatus: runStatusSchema.parse(row.runStatus),
14506
+ runTrigger: runTriggerSchema.parse(row.runTrigger),
14507
+ runCreatedAt: row.runCreatedAt,
14508
+ runStartedAt: row.runStartedAt,
14509
+ runFinishedAt: row.runFinishedAt,
14510
+ snapshotId: row.snapshotId,
14511
+ snapshotCreatedAt: row.snapshotCreatedAt,
14512
+ queryId: row.queryId,
14513
+ query: row.queryText ?? row.currentQuery,
14514
+ provider: row.provider,
14515
+ model: row.model,
14516
+ location: row.location,
14517
+ citationState,
14518
+ cited: citationState === CitationStates.cited,
14519
+ answerMentioned: row.answerMentioned,
14520
+ mentionState: recordMentionState(row.answerMentioned),
14521
+ citedDomains: row.citedDomains,
14522
+ competitorOverlap: row.competitorOverlap,
14523
+ recommendedCompetitors: row.recommendedCompetitors,
14524
+ answerText: row.answerText,
14525
+ groundingSources: evidence.groundingSources,
14526
+ searchQueries: evidence.searchQueries
14527
+ };
14528
+ });
14529
+ const dto = {
14530
+ schemaVersion: "canonry.results-export/v1",
14531
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14532
+ project: {
14533
+ id: project.id,
14534
+ name: project.name,
14535
+ displayName: project.displayName,
14536
+ canonicalDomain: project.canonicalDomain,
14537
+ country: project.country,
14538
+ language: project.language
14539
+ },
14540
+ filters: {
14541
+ since: request.query.since || null,
14542
+ until: request.query.until || null,
14543
+ includeProbes
14544
+ },
14545
+ recordCount: records.length,
14546
+ records
14547
+ };
14548
+ reply.header("Content-Disposition", `attachment; filename="${filenameFor(project.name, format)}"`);
14549
+ reply.header("Cache-Control", "private, no-store");
14550
+ reply.header("X-Content-Type-Options", "nosniff");
14551
+ if (format === "csv") {
14552
+ reply.type("text/csv; charset=utf-8");
14553
+ return reply.send(renderResultsExportCsv(dto));
14554
+ }
14555
+ reply.type("application/json; charset=utf-8");
14556
+ return reply.send(dto);
14557
+ });
14558
+ }
14559
+
14242
14560
  // ../api-routes/src/composites.ts
14243
- import { eq as eq19, and as and15, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
14561
+ import { eq as eq20, and as and16, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
14244
14562
  var TOP_INSIGHT_LIMIT = 5;
14245
14563
  var SEARCH_HIT_HARD_LIMIT = 50;
14246
14564
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -14258,7 +14576,7 @@ async function compositeRoutes(app) {
14258
14576
  const project = resolveProject(app.db, request.params.name);
14259
14577
  const filterLocation = (request.query.location ?? "").trim() || null;
14260
14578
  const sinceIso = parseSinceFilter(request.query.since);
14261
- const allRunsRaw = app.db.select().from(runs).where(and15(eq19(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
14579
+ const allRunsRaw = app.db.select().from(runs).where(and16(eq20(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
14262
14580
  const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
14263
14581
  const totalRuns = allRuns.length;
14264
14582
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
@@ -14271,15 +14589,15 @@ async function compositeRoutes(app) {
14271
14589
  const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
14272
14590
  const latestRunRow = allRuns[0] ?? null;
14273
14591
  const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
14274
- const healthRow = app.db.select().from(healthSnapshots).where(eq19(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
14592
+ const healthRow = app.db.select().from(healthSnapshots).where(eq20(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
14275
14593
  const health = healthRow ? mapHealthRow2(healthRow) : null;
14276
- const insightRows = app.db.select().from(insights).where(eq19(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
14594
+ const insightRows = app.db.select().from(insights).where(eq20(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
14277
14595
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
14278
14596
  const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
14279
14597
  const snapshotRunIds = new Set(sparklineRunIds);
14280
14598
  for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
14281
14599
  for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
14282
- const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq19(queries.projectId, project.id)).all();
14600
+ const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
14283
14601
  const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
14284
14602
  const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
14285
14603
  const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
@@ -14295,7 +14613,7 @@ async function compositeRoutes(app) {
14295
14613
  trackedPrevious,
14296
14614
  previousVisibilityRun?.createdAt ?? null
14297
14615
  );
14298
- const competitorRows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
14616
+ const competitorRows = app.db.select().from(competitors).where(eq20(competitors.projectId, project.id)).all();
14299
14617
  const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
14300
14618
  for (const snapshots of snapshotsByRun.values()) {
14301
14619
  for (const snapshot of snapshots) {
@@ -14413,9 +14731,9 @@ async function compositeRoutes(app) {
14413
14731
  citedDomains: querySnapshots.citedDomains,
14414
14732
  rawResponse: querySnapshots.rawResponse,
14415
14733
  createdAt: querySnapshots.createdAt
14416
- }).from(querySnapshots).innerJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(
14417
- and15(
14418
- eq19(queries.projectId, project.id),
14734
+ }).from(querySnapshots).innerJoin(queries, eq20(querySnapshots.queryId, queries.id)).where(
14735
+ and16(
14736
+ eq20(queries.projectId, project.id),
14419
14737
  or4(
14420
14738
  sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
14421
14739
  sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
@@ -14425,8 +14743,8 @@ async function compositeRoutes(app) {
14425
14743
  )
14426
14744
  ).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
14427
14745
  const insightMatches = app.db.select().from(insights).where(
14428
- and15(
14429
- eq19(insights.projectId, project.id),
14746
+ and16(
14747
+ eq20(insights.projectId, project.id),
14430
14748
  or4(
14431
14749
  like2(insights.title, pattern),
14432
14750
  like2(insights.query, pattern),
@@ -14642,8 +14960,8 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
14642
14960
  // NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
14643
14961
  // which the JS coerces to 0 — caught by the impression floor anyway).
14644
14962
  avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
14645
- }).from(gscSearchData).where(and15(
14646
- eq19(gscSearchData.projectId, projectId),
14963
+ }).from(gscSearchData).where(and16(
14964
+ eq20(gscSearchData.projectId, projectId),
14647
14965
  sql8`${gscSearchData.date} >= ${cutoff}`,
14648
14966
  sql8`${gscSearchData.impressions} > 0`
14649
14967
  )).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
@@ -14666,8 +14984,8 @@ function buildIndexCoverageScore(app, projectId) {
14666
14984
  tooltip,
14667
14985
  trend: []
14668
14986
  };
14669
- const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq19(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
14670
- const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq19(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
14987
+ const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq20(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
14988
+ const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq20(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
14671
14989
  const chosen = pickIndexCoverageRow(gscRow, bingRow);
14672
14990
  if (!chosen) return empty;
14673
14991
  const total = chosen.indexed + chosen.notIndexed;
@@ -14693,7 +15011,7 @@ function countGoogleDeindexedUrls(app, projectId) {
14693
15011
  url: gscUrlInspections.url,
14694
15012
  indexingState: gscUrlInspections.indexingState,
14695
15013
  inspectedAt: gscUrlInspections.inspectedAt
14696
- }).from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
15014
+ }).from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
14697
15015
  if (rows.length === 0) return 0;
14698
15016
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
14699
15017
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -14945,6 +15263,7 @@ var SCHEMA_TABLE = {
14945
15263
  AdsConnectionStatusDto: adsConnectionStatusDtoSchema,
14946
15264
  AdsDisconnectResponse: adsDisconnectResponseSchema,
14947
15265
  AdsInsightsResponse: adsInsightsResponseSchema,
15266
+ AdsOperationResponse: adsOperationResponseSchema,
14948
15267
  AdsSummaryDto: adsSummaryDtoSchema,
14949
15268
  AdsSyncResponse: adsSyncResponseSchema,
14950
15269
  ApiKeyDto: apiKeyDtoSchema,
@@ -15013,6 +15332,7 @@ var SCHEMA_TABLE = {
15013
15332
  GscSiteListResponseDto: gscSiteListResponseDtoSchema,
15014
15333
  GscSitemapListResponseDto: gscSitemapListResponseDtoSchema,
15015
15334
  GscUrlInspectionDto: gscUrlInspectionDtoSchema,
15335
+ HealthSnapshotDto: healthSnapshotDtoSchema,
15016
15336
  IndexingRequestResponseDto: indexingRequestResponseDtoSchema,
15017
15337
  KeywordDto: keywordDtoSchema,
15018
15338
  LatestProjectRunDto: latestProjectRunDtoSchema,
@@ -15022,6 +15342,7 @@ var SCHEMA_TABLE = {
15022
15342
  ProjectOverviewDto: projectOverviewDtoSchema,
15023
15343
  ProjectReportDto: projectReportDtoSchema,
15024
15344
  QueryDto: queryDtoSchema,
15345
+ ResultsExportDto: resultsExportDtoSchema,
15025
15346
  RunDetailDto: runDetailDtoSchema,
15026
15347
  RunDto: runDtoSchema,
15027
15348
  SchedulableRunKind: schedulableRunKindSchema,
@@ -15125,6 +15446,17 @@ var booleanSchema = { type: "boolean" };
15125
15446
  var integerSchema = { type: "integer" };
15126
15447
  var objectSchema = { type: "object", additionalProperties: true };
15127
15448
  var stringArraySchema = { type: "array", items: stringSchema };
15449
+ var adsOperationKeySchema = { type: "string", minLength: 8, maxLength: 128 };
15450
+ var adsCreativeRequestSchema = {
15451
+ type: "object",
15452
+ required: ["title", "body", "targetUrl", "fileId"],
15453
+ properties: {
15454
+ title: { type: "string", minLength: 3, maxLength: 50 },
15455
+ body: { type: "string", minLength: 1, maxLength: 100 },
15456
+ targetUrl: { type: "string", format: "uri" },
15457
+ fileId: stringSchema
15458
+ }
15459
+ };
15128
15460
  var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
15129
15461
  var locationSchema = {
15130
15462
  type: "object",
@@ -15526,6 +15858,62 @@ var routeCatalog = [
15526
15858
  404: errorResponse("Project not found.")
15527
15859
  }
15528
15860
  },
15861
+ {
15862
+ method: "get",
15863
+ path: "/api/v1/projects/{name}/results/export",
15864
+ summary: "Download historical answer-engine results",
15865
+ tags: ["exports"],
15866
+ description: "Downloads one record per persisted answer-visibility query \xD7 provider observation. Citation and mention stay independent: `citationState` describes source-list attribution, while nullable `answerMentioned` describes answer-text presence. Excludes probe runs by default. JSON is a versioned portable artifact; CSV is its flat spreadsheet representation. Raw provider payloads, credentials, and local paths are never included.",
15867
+ parameters: [
15868
+ nameParameter,
15869
+ {
15870
+ name: "format",
15871
+ in: "query",
15872
+ description: "Download format. Defaults to `json`.",
15873
+ schema: { type: "string", enum: ["json", "csv"], default: "json" }
15874
+ },
15875
+ {
15876
+ name: "since",
15877
+ in: "query",
15878
+ description: "Inclusive ISO 8601 lower bound on run creation time.",
15879
+ schema: stringSchema
15880
+ },
15881
+ {
15882
+ name: "until",
15883
+ in: "query",
15884
+ description: "Inclusive ISO 8601 upper bound on run creation time. A date-only value includes that whole UTC day.",
15885
+ schema: stringSchema
15886
+ },
15887
+ {
15888
+ name: "includeProbes",
15889
+ in: "query",
15890
+ description: "Include operator/agent probe runs. Defaults to `false`.",
15891
+ schema: booleanSchema
15892
+ }
15893
+ ],
15894
+ responses: {
15895
+ 200: {
15896
+ description: "Results attachment returned.",
15897
+ content: {
15898
+ // The query-selected attachment can be JSON or CSV. OpenAPI cannot
15899
+ // express that the `format` parameter selects a media type, so keep
15900
+ // the generated SDK honest with a union rather than promising JSON
15901
+ // to a caller that requested CSV.
15902
+ "application/json": {
15903
+ schema: {
15904
+ oneOf: [
15905
+ { $ref: "#/components/schemas/ResultsExportDto" },
15906
+ { type: "string" }
15907
+ ]
15908
+ }
15909
+ },
15910
+ "text/csv": { schema: { type: "string" } }
15911
+ }
15912
+ },
15913
+ 400: errorResponse("Invalid export filters."),
15914
+ 404: errorResponse("Project not found.")
15915
+ }
15916
+ },
15529
15917
  {
15530
15918
  method: "get",
15531
15919
  path: "/api/v1/projects/{name}/queries",
@@ -16013,7 +16401,15 @@ var routeCatalog = [
16013
16401
  path: "/api/v1/projects/{name}/history",
16014
16402
  summary: "Get project audit history",
16015
16403
  tags: ["history"],
16016
- parameters: [nameParameter],
16404
+ parameters: [
16405
+ nameParameter,
16406
+ limitQueryParameter,
16407
+ offsetQueryParameter,
16408
+ { name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
16409
+ { name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
16410
+ { name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
16411
+ { name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
16412
+ ],
16017
16413
  responses: {
16018
16414
  200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
16019
16415
  }
@@ -16023,6 +16419,14 @@ var routeCatalog = [
16023
16419
  path: "/api/v1/history",
16024
16420
  summary: "Get global audit history",
16025
16421
  tags: ["history"],
16422
+ parameters: [
16423
+ limitQueryParameter,
16424
+ offsetQueryParameter,
16425
+ { name: "since", in: "query", description: "ISO 8601 lower bound.", schema: stringSchema },
16426
+ { name: "action", in: "query", description: "Exact audit action filter.", schema: stringSchema },
16427
+ { name: "actor", in: "query", description: "Exact actor filter.", schema: stringSchema },
16428
+ { name: "entityType", in: "query", description: "Exact entity type filter.", schema: stringSchema }
16429
+ ],
16026
16430
  responses: {
16027
16431
  200: jsonArrayResponse("Audit history returned.", "AuditLogEntry")
16028
16432
  }
@@ -16047,7 +16451,11 @@ var routeCatalog = [
16047
16451
  path: "/api/v1/projects/{name}/timeline",
16048
16452
  summary: "Get query timeline",
16049
16453
  tags: ["history"],
16050
- parameters: [nameParameter, locationQueryParameter],
16454
+ parameters: [
16455
+ nameParameter,
16456
+ locationQueryParameter,
16457
+ { name: "limit", in: "query", description: "Restrict each query timeline to snapshots from the most recent N project runs. Omit for full history.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
16458
+ ],
16051
16459
  responses: {
16052
16460
  // TODO: Add `ProjectTimelineDto` Zod schema in contracts.
16053
16461
  200: rawJsonResponse("Timeline returned.", looseObjectSchema)
@@ -17168,6 +17576,7 @@ var routeCatalog = [
17168
17576
  responses: {
17169
17577
  200: jsonResponse("Connected; key validated against the upstream ad account.", "AdsConnectionStatusDto"),
17170
17578
  400: errorResponse("Missing/invalid key or credential storage unavailable."),
17579
+ 403: errorResponse("The key lacks ads.write."),
17171
17580
  404: errorResponse("Project not found.")
17172
17581
  }
17173
17582
  },
@@ -17179,6 +17588,7 @@ var routeCatalog = [
17179
17588
  parameters: [nameParameter],
17180
17589
  responses: {
17181
17590
  200: jsonResponse("Disconnected (idempotent).", "AdsDisconnectResponse"),
17591
+ 403: errorResponse("The key lacks ads.write."),
17182
17592
  404: errorResponse("Project not found.")
17183
17593
  }
17184
17594
  },
@@ -17193,6 +17603,288 @@ var routeCatalog = [
17193
17603
  404: errorResponse("Project not found.")
17194
17604
  }
17195
17605
  },
17606
+ {
17607
+ method: "get",
17608
+ path: "/api/v1/projects/{name}/ads/operations/{operationKey}",
17609
+ summary: "Read a durable OpenAI Ads mutation receipt by operation key",
17610
+ tags: ["ads"],
17611
+ parameters: [
17612
+ nameParameter,
17613
+ { name: "operationKey", in: "path", required: true, description: "Caller-supplied idempotency key.", schema: adsOperationKeySchema }
17614
+ ],
17615
+ responses: {
17616
+ 200: jsonResponse("Operation receipt.", "AdsOperationResponse"),
17617
+ 404: errorResponse("Project or operation not found.")
17618
+ }
17619
+ },
17620
+ {
17621
+ method: "post",
17622
+ path: "/api/v1/projects/{name}/ads/files",
17623
+ summary: "Upload an OpenAI Ads image from a public HTTPS URL",
17624
+ tags: ["ads"],
17625
+ parameters: [nameParameter],
17626
+ requestBody: {
17627
+ required: true,
17628
+ content: { "application/json": { schema: {
17629
+ type: "object",
17630
+ required: ["operationKey", "imageUrl"],
17631
+ properties: { operationKey: adsOperationKeySchema, imageUrl: { type: "string", format: "uri" } }
17632
+ } } }
17633
+ },
17634
+ responses: {
17635
+ 200: jsonResponse("Upload receipt.", "AdsOperationResponse"),
17636
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17637
+ 403: errorResponse("The key lacks ads.write."),
17638
+ 404: errorResponse("Project not found."),
17639
+ 409: errorResponse("Operation key was already used for a different request."),
17640
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17641
+ }
17642
+ },
17643
+ {
17644
+ method: "post",
17645
+ path: "/api/v1/projects/{name}/ads/campaigns",
17646
+ summary: "Create a paused OpenAI Ads campaign",
17647
+ description: "The server always creates the campaign paused. Status is not accepted from the caller.",
17648
+ tags: ["ads"],
17649
+ parameters: [nameParameter],
17650
+ requestBody: {
17651
+ required: true,
17652
+ content: { "application/json": { schema: {
17653
+ type: "object",
17654
+ required: ["operationKey", "name", "lifetimeSpendLimitMicros", "locationIds"],
17655
+ properties: {
17656
+ operationKey: adsOperationKeySchema,
17657
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17658
+ description: stringSchema,
17659
+ startTime: integerSchema,
17660
+ endTime: integerSchema,
17661
+ lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
17662
+ locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
17663
+ }
17664
+ } } }
17665
+ },
17666
+ responses: {
17667
+ 200: jsonResponse("Paused campaign creation receipt.", "AdsOperationResponse"),
17668
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17669
+ 403: errorResponse("The key lacks ads.write."),
17670
+ 404: errorResponse("Project not found."),
17671
+ 409: errorResponse("Operation key was already used for a different request."),
17672
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17673
+ }
17674
+ },
17675
+ {
17676
+ method: "post",
17677
+ path: "/api/v1/projects/{name}/ads/ad-groups",
17678
+ summary: "Create a paused OpenAI Ads ad group",
17679
+ description: "The server fixes billing to impressions and always creates the ad group paused.",
17680
+ tags: ["ads"],
17681
+ parameters: [nameParameter],
17682
+ requestBody: {
17683
+ required: true,
17684
+ content: { "application/json": { schema: {
17685
+ type: "object",
17686
+ required: ["operationKey", "campaignId", "name", "contextHints", "maxBidMicros"],
17687
+ properties: {
17688
+ operationKey: adsOperationKeySchema,
17689
+ campaignId: stringSchema,
17690
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17691
+ description: stringSchema,
17692
+ contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
17693
+ maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
17694
+ }
17695
+ } } }
17696
+ },
17697
+ responses: {
17698
+ 200: jsonResponse("Paused ad-group creation receipt.", "AdsOperationResponse"),
17699
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17700
+ 403: errorResponse("The key lacks ads.write."),
17701
+ 404: errorResponse("Project not found."),
17702
+ 409: errorResponse("Operation key was already used for a different request."),
17703
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17704
+ }
17705
+ },
17706
+ {
17707
+ method: "post",
17708
+ path: "/api/v1/projects/{name}/ads/ads",
17709
+ summary: "Create a paused OpenAI Ads chat-card ad",
17710
+ description: "The server always creates the ad paused and fixes creative.type to chat_card.",
17711
+ tags: ["ads"],
17712
+ parameters: [nameParameter],
17713
+ requestBody: {
17714
+ required: true,
17715
+ content: { "application/json": { schema: {
17716
+ type: "object",
17717
+ required: ["operationKey", "adGroupId", "name", "creative"],
17718
+ properties: {
17719
+ operationKey: adsOperationKeySchema,
17720
+ adGroupId: stringSchema,
17721
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17722
+ creative: adsCreativeRequestSchema
17723
+ }
17724
+ } } }
17725
+ },
17726
+ responses: {
17727
+ 200: jsonResponse("Paused ad creation receipt.", "AdsOperationResponse"),
17728
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17729
+ 403: errorResponse("The key lacks ads.write."),
17730
+ 404: errorResponse("Project not found."),
17731
+ 409: errorResponse("Operation key was already used for a different request."),
17732
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17733
+ }
17734
+ },
17735
+ {
17736
+ method: "post",
17737
+ path: "/api/v1/projects/{name}/ads/campaigns/{id}",
17738
+ summary: "Update an OpenAI Ads campaign with optimistic concurrency",
17739
+ description: "The campaign must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17740
+ tags: ["ads"],
17741
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
17742
+ requestBody: {
17743
+ required: true,
17744
+ content: { "application/json": { schema: {
17745
+ type: "object",
17746
+ required: ["operationKey", "expectedUpdatedAt"],
17747
+ properties: {
17748
+ operationKey: adsOperationKeySchema,
17749
+ expectedUpdatedAt: integerSchema,
17750
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17751
+ description: { type: "string", nullable: true },
17752
+ startTime: { type: "integer", nullable: true },
17753
+ endTime: { type: "integer", nullable: true },
17754
+ lifetimeSpendLimitMicros: { type: "integer", minimum: 1e6 },
17755
+ locationIds: { type: "array", minItems: 1, maxItems: 100, items: stringSchema }
17756
+ }
17757
+ } } }
17758
+ },
17759
+ responses: {
17760
+ 200: jsonResponse("Campaign update receipt.", "AdsOperationResponse"),
17761
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17762
+ 403: errorResponse("The key lacks ads.write."),
17763
+ 404: errorResponse("Project not found."),
17764
+ 409: errorResponse("Operation key was already used for a different request."),
17765
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17766
+ }
17767
+ },
17768
+ {
17769
+ method: "post",
17770
+ path: "/api/v1/projects/{name}/ads/ad-groups/{id}",
17771
+ summary: "Update an OpenAI Ads ad group with optimistic concurrency",
17772
+ description: "The ad group must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17773
+ tags: ["ads"],
17774
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
17775
+ requestBody: {
17776
+ required: true,
17777
+ content: { "application/json": { schema: {
17778
+ type: "object",
17779
+ required: ["operationKey", "expectedUpdatedAt"],
17780
+ properties: {
17781
+ operationKey: adsOperationKeySchema,
17782
+ expectedUpdatedAt: integerSchema,
17783
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17784
+ description: { type: "string", nullable: true },
17785
+ contextHints: { type: "array", minItems: 1, maxItems: 100, items: stringSchema },
17786
+ maxBidMicros: { type: "integer", minimum: 1, maximum: 1e8 }
17787
+ }
17788
+ } } }
17789
+ },
17790
+ responses: {
17791
+ 200: jsonResponse("Ad-group update receipt.", "AdsOperationResponse"),
17792
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17793
+ 403: errorResponse("The key lacks ads.write."),
17794
+ 404: errorResponse("Project not found."),
17795
+ 409: errorResponse("Operation key was already used for a different request."),
17796
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17797
+ }
17798
+ },
17799
+ {
17800
+ method: "post",
17801
+ path: "/api/v1/projects/{name}/ads/ads/{id}",
17802
+ summary: "Update an OpenAI Ads chat-card ad with optimistic concurrency",
17803
+ description: "The ad must already be paused. Pause and sync first, then pass the refreshed upstreamUpdatedAt. Activation remains human-only.",
17804
+ tags: ["ads"],
17805
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
17806
+ requestBody: {
17807
+ required: true,
17808
+ content: { "application/json": { schema: {
17809
+ type: "object",
17810
+ required: ["operationKey", "expectedUpdatedAt"],
17811
+ properties: {
17812
+ operationKey: adsOperationKeySchema,
17813
+ expectedUpdatedAt: integerSchema,
17814
+ name: { type: "string", minLength: 3, maxLength: 1e3 },
17815
+ creative: adsCreativeRequestSchema
17816
+ }
17817
+ } } }
17818
+ },
17819
+ responses: {
17820
+ 200: jsonResponse("Ad update receipt.", "AdsOperationResponse"),
17821
+ 400: errorResponse("Invalid request, active entity, or stale expectedUpdatedAt."),
17822
+ 403: errorResponse("The key lacks ads.write."),
17823
+ 404: errorResponse("Project not found."),
17824
+ 409: errorResponse("Operation key was already used for a different request."),
17825
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17826
+ }
17827
+ },
17828
+ {
17829
+ method: "post",
17830
+ path: "/api/v1/projects/{name}/ads/campaigns/{id}/pause",
17831
+ summary: "Pause an OpenAI Ads campaign",
17832
+ tags: ["ads"],
17833
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Campaign ID.", schema: stringSchema }],
17834
+ requestBody: { required: true, content: { "application/json": { schema: {
17835
+ type: "object",
17836
+ required: ["operationKey"],
17837
+ properties: { operationKey: adsOperationKeySchema }
17838
+ } } } },
17839
+ responses: {
17840
+ 200: jsonResponse("Campaign pause receipt.", "AdsOperationResponse"),
17841
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17842
+ 403: errorResponse("The key lacks ads.write."),
17843
+ 404: errorResponse("Project not found."),
17844
+ 409: errorResponse("Operation key was already used for a different request."),
17845
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17846
+ }
17847
+ },
17848
+ {
17849
+ method: "post",
17850
+ path: "/api/v1/projects/{name}/ads/ad-groups/{id}/pause",
17851
+ summary: "Pause an OpenAI Ads ad group",
17852
+ tags: ["ads"],
17853
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad group ID.", schema: stringSchema }],
17854
+ requestBody: { required: true, content: { "application/json": { schema: {
17855
+ type: "object",
17856
+ required: ["operationKey"],
17857
+ properties: { operationKey: adsOperationKeySchema }
17858
+ } } } },
17859
+ responses: {
17860
+ 200: jsonResponse("Ad-group pause receipt.", "AdsOperationResponse"),
17861
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17862
+ 403: errorResponse("The key lacks ads.write."),
17863
+ 404: errorResponse("Project not found."),
17864
+ 409: errorResponse("Operation key was already used for a different request."),
17865
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17866
+ }
17867
+ },
17868
+ {
17869
+ method: "post",
17870
+ path: "/api/v1/projects/{name}/ads/ads/{id}/pause",
17871
+ summary: "Pause an OpenAI Ads ad",
17872
+ tags: ["ads"],
17873
+ parameters: [nameParameter, { name: "id", in: "path", required: true, description: "Ad ID.", schema: stringSchema }],
17874
+ requestBody: { required: true, content: { "application/json": { schema: {
17875
+ type: "object",
17876
+ required: ["operationKey"],
17877
+ properties: { operationKey: adsOperationKeySchema }
17878
+ } } } },
17879
+ responses: {
17880
+ 200: jsonResponse("Ad pause receipt.", "AdsOperationResponse"),
17881
+ 400: errorResponse("Invalid request or ads connection unavailable."),
17882
+ 403: errorResponse("The key lacks ads.write."),
17883
+ 404: errorResponse("Project not found."),
17884
+ 409: errorResponse("Operation key was already used for a different request."),
17885
+ 502: errorResponse("Upstream outcome failed or is unknown.")
17886
+ }
17887
+ },
17196
17888
  {
17197
17889
  method: "post",
17198
17890
  path: "/api/v1/projects/{name}/ads/sync",
@@ -17202,13 +17894,14 @@ var routeCatalog = [
17202
17894
  responses: {
17203
17895
  200: jsonResponse("Sync run queued.", "AdsSyncResponse"),
17204
17896
  400: errorResponse("No ads connection for this project."),
17897
+ 403: errorResponse("The key lacks ads.write."),
17205
17898
  404: errorResponse("Project not found.")
17206
17899
  }
17207
17900
  },
17208
17901
  {
17209
17902
  method: "get",
17210
17903
  path: "/api/v1/projects/{name}/ads/campaigns",
17211
- summary: "Synced campaign snapshots with nested ad groups (context hints) and ads",
17904
+ summary: "Synced campaign snapshots with lifecycle timestamps, targeting, nested ad groups, and ads",
17212
17905
  tags: ["ads"],
17213
17906
  parameters: [nameParameter],
17214
17907
  responses: {
@@ -18118,8 +18811,7 @@ var routeCatalog = [
18118
18811
  tags: ["intelligence"],
18119
18812
  parameters: [nameParameter],
18120
18813
  responses: {
18121
- // TODO: Add `HealthSnapshotDto` Zod schema in contracts.
18122
- 200: rawJsonResponse("Health snapshot or no-data sentinel returned.", looseObjectSchema),
18814
+ 200: jsonResponse("Health snapshot or no-data sentinel returned.", "HealthSnapshotDto"),
18123
18815
  404: errorResponse("Project not found.")
18124
18816
  }
18125
18817
  },
@@ -18130,11 +18822,10 @@ var routeCatalog = [
18130
18822
  tags: ["intelligence"],
18131
18823
  parameters: [
18132
18824
  nameParameter,
18133
- { name: "limit", in: "query", description: "Max results.", schema: stringSchema }
18825
+ { name: "limit", in: "query", description: "Max results.", schema: { ...integerSchema, minimum: 1, maximum: 100 } }
18134
18826
  ],
18135
18827
  responses: {
18136
- // TODO: Add `HealthSnapshotDto` Zod schema in contracts.
18137
- 200: rawJsonResponse("Health history returned.", { type: "array", items: looseObjectSchema }),
18828
+ 200: jsonArrayResponse("Health history returned.", "HealthSnapshotDto"),
18138
18829
  404: errorResponse("Project not found.")
18139
18830
  }
18140
18831
  },
@@ -19035,22 +19726,26 @@ var routeCatalog = [
19035
19726
  method: "get",
19036
19727
  path: "/api/v1/projects/{name}/technical-aeo",
19037
19728
  summary: "Get the Technical AEO scorecard for a project",
19038
- description: "Returns the latest completed/partial site-audit: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
19729
+ description: "Returns the latest completed/partial site-audit, or the historical audit selected by `runId`: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the audit immediately before it. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
19039
19730
  tags: ["technical-aeo"],
19040
- parameters: [nameParameter],
19731
+ parameters: [
19732
+ nameParameter,
19733
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema }
19734
+ ],
19041
19735
  responses: {
19042
19736
  200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
19043
- 404: errorResponse("Project not found.")
19737
+ 404: errorResponse("Project or site-audit run not found.")
19044
19738
  }
19045
19739
  },
19046
19740
  {
19047
19741
  method: "get",
19048
19742
  path: "/api/v1/projects/{name}/technical-aeo/pages",
19049
- summary: "List audited pages from the latest site-audit run",
19050
- description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
19743
+ summary: "List audited pages from a site-audit run",
19744
+ description: "Returns the per-page breakdown of the latest completed/partial site-audit run, or the historical audit selected by `runId` (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
19051
19745
  tags: ["technical-aeo"],
19052
19746
  parameters: [
19053
19747
  nameParameter,
19748
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest audit.", schema: stringSchema },
19054
19749
  { name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
19055
19750
  { name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
19056
19751
  limitQueryParameter,
@@ -19058,7 +19753,7 @@ var routeCatalog = [
19058
19753
  ],
19059
19754
  responses: {
19060
19755
  200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
19061
- 404: errorResponse("Project not found.")
19756
+ 404: errorResponse("Project or site-audit run not found.")
19062
19757
  }
19063
19758
  },
19064
19759
  {
@@ -19432,7 +20127,7 @@ async function settingsRoutes(app, opts) {
19432
20127
 
19433
20128
  // ../api-routes/src/keys.ts
19434
20129
  import crypto13 from "crypto";
19435
- import { desc as desc10, eq as eq20 } from "drizzle-orm";
20130
+ import { desc as desc10, eq as eq21 } from "drizzle-orm";
19436
20131
  var KEYS_WRITE_SCOPE = "keys.write";
19437
20132
  function toApiKeyDto(row) {
19438
20133
  const scopes = Array.isArray(row.scopes) ? row.scopes : [];
@@ -19451,7 +20146,7 @@ function toApiKeyDto(row) {
19451
20146
  async function keysRoutes(app) {
19452
20147
  app.get("/keys", async (request) => {
19453
20148
  const scopedProjectId = request.apiKey?.projectId;
19454
- const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq20(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
20149
+ const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq21(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
19455
20150
  return { keys: rows.map(toApiKeyDto) };
19456
20151
  });
19457
20152
  app.get("/keys/self", async (request) => {
@@ -19459,7 +20154,7 @@ async function keysRoutes(app) {
19459
20154
  if (!id) {
19460
20155
  throw notFound("API key", "self");
19461
20156
  }
19462
- const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
20157
+ const row = app.db.select().from(apiKeys).where(eq21(apiKeys.id, id)).get();
19463
20158
  if (!row) {
19464
20159
  throw notFound("API key", id);
19465
20160
  }
@@ -19477,7 +20172,7 @@ async function keysRoutes(app) {
19477
20172
  throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
19478
20173
  }
19479
20174
  if (projectId) {
19480
- const proj = app.db.select({ id: projects.id }).from(projects).where(eq20(projects.id, projectId)).get();
20175
+ const proj = app.db.select({ id: projects.id }).from(projects).where(eq21(projects.id, projectId)).get();
19481
20176
  if (!proj) {
19482
20177
  throw notFound("Project", projectId);
19483
20178
  }
@@ -19523,7 +20218,7 @@ async function keysRoutes(app) {
19523
20218
  app.post("/keys/:id/revoke", async (request) => {
19524
20219
  requireScope(request, KEYS_WRITE_SCOPE);
19525
20220
  const { id } = request.params;
19526
- const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
20221
+ const row = app.db.select().from(apiKeys).where(eq21(apiKeys.id, id)).get();
19527
20222
  if (!row) {
19528
20223
  throw notFound("API key", id);
19529
20224
  }
@@ -19539,7 +20234,7 @@ async function keysRoutes(app) {
19539
20234
  }
19540
20235
  const now = (/* @__PURE__ */ new Date()).toISOString();
19541
20236
  app.db.transaction((tx) => {
19542
- tx.update(apiKeys).set({ revokedAt: now }).where(eq20(apiKeys.id, id)).run();
20237
+ tx.update(apiKeys).set({ revokedAt: now }).where(eq21(apiKeys.id, id)).run();
19543
20238
  writeAuditLog(tx, auditFromRequest(request, {
19544
20239
  actor: "api",
19545
20240
  action: "api-key.revoked",
@@ -19612,7 +20307,7 @@ async function telemetryRoutes(app, opts) {
19612
20307
 
19613
20308
  // ../api-routes/src/schedules.ts
19614
20309
  import crypto14 from "crypto";
19615
- import { and as and16, eq as eq21 } from "drizzle-orm";
20310
+ import { and as and17, eq as eq22 } from "drizzle-orm";
19616
20311
  function parseKindParam(raw) {
19617
20312
  if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
19618
20313
  const parsed = schedulableRunKindSchema.safeParse(raw);
@@ -19639,7 +20334,7 @@ async function scheduleRoutes(app, opts) {
19639
20334
  if (!sourceId) {
19640
20335
  throw validationError('"sourceId" is required when kind is "traffic-sync"');
19641
20336
  }
19642
- const sourceRow = app.db.select().from(trafficSources).where(eq21(trafficSources.id, sourceId)).get();
20337
+ const sourceRow = app.db.select().from(trafficSources).where(eq22(trafficSources.id, sourceId)).get();
19643
20338
  if (!sourceRow || sourceRow.projectId !== project.id) {
19644
20339
  throw notFound("Traffic source", sourceId);
19645
20340
  }
@@ -19684,7 +20379,7 @@ async function scheduleRoutes(app, opts) {
19684
20379
  }
19685
20380
  const now = (/* @__PURE__ */ new Date()).toISOString();
19686
20381
  const enabledBool = enabled !== false;
19687
- const existing = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
20382
+ const existing = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
19688
20383
  if (existing) {
19689
20384
  app.db.update(schedules).set({
19690
20385
  cronExpr,
@@ -19694,7 +20389,7 @@ async function scheduleRoutes(app, opts) {
19694
20389
  sourceId: sourceId ?? null,
19695
20390
  enabled: enabledBool,
19696
20391
  updatedAt: now
19697
- }).where(eq21(schedules.id, existing.id)).run();
20392
+ }).where(eq22(schedules.id, existing.id)).run();
19698
20393
  } else {
19699
20394
  app.db.insert(schedules).values({
19700
20395
  id: crypto14.randomUUID(),
@@ -19718,13 +20413,13 @@ async function scheduleRoutes(app, opts) {
19718
20413
  diff: { kind, cronExpr, preset, timezone, providers, sourceId }
19719
20414
  });
19720
20415
  opts.onScheduleUpdated?.("upsert", project.id, kind);
19721
- const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
20416
+ const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
19722
20417
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
19723
20418
  });
19724
20419
  app.get("/projects/:name/schedule", async (request, reply) => {
19725
20420
  const project = resolveProject(app.db, request.params.name);
19726
20421
  const kind = parseKindParam(request.query?.kind);
19727
- const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
20422
+ const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
19728
20423
  if (!schedule) {
19729
20424
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
19730
20425
  }
@@ -19733,11 +20428,11 @@ async function scheduleRoutes(app, opts) {
19733
20428
  app.delete("/projects/:name/schedule", async (request, reply) => {
19734
20429
  const project = resolveProject(app.db, request.params.name);
19735
20430
  const kind = parseKindParam(request.query?.kind);
19736
- const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
20431
+ const schedule = app.db.select().from(schedules).where(and17(eq22(schedules.projectId, project.id), eq22(schedules.kind, kind))).get();
19737
20432
  if (!schedule) {
19738
20433
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
19739
20434
  }
19740
- app.db.delete(schedules).where(eq21(schedules.id, schedule.id)).run();
20435
+ app.db.delete(schedules).where(eq22(schedules.id, schedule.id)).run();
19741
20436
  writeAuditLog(app.db, {
19742
20437
  projectId: project.id,
19743
20438
  actor: "api",
@@ -19770,7 +20465,7 @@ function formatSchedule(row) {
19770
20465
 
19771
20466
  // ../api-routes/src/notifications.ts
19772
20467
  import crypto15 from "crypto";
19773
- import { eq as eq22 } from "drizzle-orm";
20468
+ import { eq as eq23 } from "drizzle-orm";
19774
20469
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
19775
20470
  async function notificationRoutes(app, opts = {}) {
19776
20471
  const allowLoopback = opts.allowLoopbackWebhooks === true;
@@ -19810,22 +20505,22 @@ async function notificationRoutes(app, opts = {}) {
19810
20505
  diff: { channel, ...redactNotificationUrl(url), events }
19811
20506
  });
19812
20507
  return reply.status(201).send({
19813
- ...formatNotification(app.db.select().from(notifications).where(eq22(notifications.id, id)).get()),
20508
+ ...formatNotification(app.db.select().from(notifications).where(eq23(notifications.id, id)).get()),
19814
20509
  webhookSecret
19815
20510
  });
19816
20511
  });
19817
20512
  app.get("/projects/:name/notifications", async (request, reply) => {
19818
20513
  const project = resolveProject(app.db, request.params.name);
19819
- const rows = app.db.select().from(notifications).where(eq22(notifications.projectId, project.id)).all();
20514
+ const rows = app.db.select().from(notifications).where(eq23(notifications.projectId, project.id)).all();
19820
20515
  return reply.send(rows.map(formatNotification));
19821
20516
  });
19822
20517
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
19823
20518
  const project = resolveProject(app.db, request.params.name);
19824
- const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
20519
+ const notification = app.db.select().from(notifications).where(eq23(notifications.id, request.params.id)).get();
19825
20520
  if (!notification || notification.projectId !== project.id) {
19826
20521
  throw notFound("Notification", request.params.id);
19827
20522
  }
19828
- app.db.delete(notifications).where(eq22(notifications.id, notification.id)).run();
20523
+ app.db.delete(notifications).where(eq23(notifications.id, notification.id)).run();
19829
20524
  writeAuditLog(app.db, {
19830
20525
  projectId: project.id,
19831
20526
  actor: "api",
@@ -19837,7 +20532,7 @@ async function notificationRoutes(app, opts = {}) {
19837
20532
  });
19838
20533
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
19839
20534
  const project = resolveProject(app.db, request.params.name);
19840
- const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
20535
+ const notification = app.db.select().from(notifications).where(eq23(notifications.id, request.params.id)).get();
19841
20536
  if (!notification || notification.projectId !== project.id) {
19842
20537
  throw notFound("Notification", request.params.id);
19843
20538
  }
@@ -19890,7 +20585,7 @@ function formatNotification(row) {
19890
20585
 
19891
20586
  // ../api-routes/src/google.ts
19892
20587
  import crypto19 from "crypto";
19893
- import { eq as eq23, and as and17, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
20588
+ import { eq as eq24, and as and18, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
19894
20589
 
19895
20590
  // ../api-routes/src/gbp-summary.ts
19896
20591
  function computeMetricTotals(rows) {
@@ -21730,7 +22425,7 @@ async function googleRoutes(app, opts) {
21730
22425
  if (!projectId) {
21731
22426
  return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
21732
22427
  }
21733
- const project = app.db.select().from(projects).where(eq23(projects.id, projectId)).get();
22428
+ const project = app.db.select().from(projects).where(eq24(projects.id, projectId)).get();
21734
22429
  if (!project) {
21735
22430
  return reply.status(400).send("Project no longer exists. Restart the connect flow.");
21736
22431
  }
@@ -21862,14 +22557,14 @@ async function googleRoutes(app, opts) {
21862
22557
  if (opts.onGscSyncRequested) {
21863
22558
  opts.onGscSyncRequested(runId, project.id, { days, full });
21864
22559
  }
21865
- const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
22560
+ const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
21866
22561
  return run;
21867
22562
  });
21868
22563
  app.get("/projects/:name/google/gsc/performance", async (request) => {
21869
22564
  const project = resolveProject(app.db, request.params.name);
21870
22565
  const { startDate, endDate, query, page, limit, offset } = request.query;
21871
22566
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
21872
- const conditions = [eq23(gscSearchData.projectId, project.id)];
22567
+ const conditions = [eq24(gscSearchData.projectId, project.id)];
21873
22568
  if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
21874
22569
  else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
21875
22570
  if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
@@ -21877,7 +22572,7 @@ async function googleRoutes(app, opts) {
21877
22572
  if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
21878
22573
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
21879
22574
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
21880
- const rows = app.db.select().from(gscSearchData).where(and17(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
22575
+ const rows = app.db.select().from(gscSearchData).where(and18(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
21881
22576
  return rows.map((r) => ({
21882
22577
  date: r.date,
21883
22578
  query: r.query,
@@ -21897,7 +22592,7 @@ async function googleRoutes(app, opts) {
21897
22592
  const windowStart = startDate ?? cutoffDate ?? "";
21898
22593
  const windowEnd = endDate ?? "9999-12-31";
21899
22594
  const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
21900
- const conditions = [eq23(gscSearchData.projectId, project.id)];
22595
+ const conditions = [eq24(gscSearchData.projectId, project.id)];
21901
22596
  if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
21902
22597
  else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
21903
22598
  if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
@@ -21905,7 +22600,7 @@ async function googleRoutes(app, opts) {
21905
22600
  date: gscSearchData.date,
21906
22601
  clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
21907
22602
  impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
21908
- }).from(gscSearchData).where(and17(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
22603
+ }).from(gscSearchData).where(and18(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
21909
22604
  const daily = mergeGscDailyTotalsWithFallback(
21910
22605
  dailyTotals,
21911
22606
  dimensionedRows.map((r) => ({
@@ -21996,9 +22691,9 @@ async function googleRoutes(app, opts) {
21996
22691
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
21997
22692
  const project = resolveProject(app.db, request.params.name);
21998
22693
  const { url, limit } = request.query;
21999
- const conditions = [eq23(gscUrlInspections.projectId, project.id)];
22000
- if (url) conditions.push(eq23(gscUrlInspections.url, url));
22001
- const rows = app.db.select().from(gscUrlInspections).where(and17(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
22694
+ const conditions = [eq24(gscUrlInspections.projectId, project.id)];
22695
+ if (url) conditions.push(eq24(gscUrlInspections.url, url));
22696
+ const rows = app.db.select().from(gscUrlInspections).where(and18(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
22002
22697
  return rows.map((r) => ({
22003
22698
  id: r.id,
22004
22699
  url: r.url,
@@ -22017,7 +22712,7 @@ async function googleRoutes(app, opts) {
22017
22712
  });
22018
22713
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
22019
22714
  const project = resolveProject(app.db, request.params.name);
22020
- const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22715
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22021
22716
  const byUrl = /* @__PURE__ */ new Map();
22022
22717
  for (const row of allInspections) {
22023
22718
  const existing = byUrl.get(row.url);
@@ -22045,7 +22740,7 @@ async function googleRoutes(app, opts) {
22045
22740
  });
22046
22741
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
22047
22742
  const project = resolveProject(app.db, request.params.name);
22048
- const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22743
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22049
22744
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
22050
22745
  const latestByUrl = /* @__PURE__ */ new Map();
22051
22746
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -22094,7 +22789,7 @@ async function googleRoutes(app, opts) {
22094
22789
  const total = latestByUrl.size;
22095
22790
  const indexed = indexedUrls.length;
22096
22791
  const notIndexed = notIndexedUrls.length;
22097
- const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
22792
+ const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq24(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
22098
22793
  const lastSyncedAt = latestSnapshot?.createdAt ?? null;
22099
22794
  const formatRow = (r) => ({
22100
22795
  id: r.id,
@@ -22145,7 +22840,7 @@ async function googleRoutes(app, opts) {
22145
22840
  const project = resolveProject(app.db, request.params.name);
22146
22841
  const parsed = parseInt(request.query.limit ?? "90", 10);
22147
22842
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
22148
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
22843
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq24(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
22149
22844
  return rows.map((r) => ({
22150
22845
  date: r.date,
22151
22846
  indexed: r.indexed,
@@ -22214,7 +22909,7 @@ async function googleRoutes(app, opts) {
22214
22909
  if (opts.onInspectSitemapRequested) {
22215
22910
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
22216
22911
  }
22217
- const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
22912
+ const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
22218
22913
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
22219
22914
  });
22220
22915
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -22241,7 +22936,7 @@ async function googleRoutes(app, opts) {
22241
22936
  if (opts.onInspectSitemapRequested) {
22242
22937
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
22243
22938
  }
22244
- const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
22939
+ const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
22245
22940
  return run;
22246
22941
  });
22247
22942
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -22288,7 +22983,7 @@ async function googleRoutes(app, opts) {
22288
22983
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
22289
22984
  let urlsToNotify = request.body?.urls ?? [];
22290
22985
  if (request.body?.allUnindexed) {
22291
- const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22986
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
22292
22987
  const latestByUrl = /* @__PURE__ */ new Map();
22293
22988
  for (const row of allInspections) {
22294
22989
  if (!latestByUrl.has(row.url)) {
@@ -22406,7 +23101,7 @@ async function googleRoutes(app, opts) {
22406
23101
  };
22407
23102
  }
22408
23103
  function listSelectionResponse(projectId) {
22409
- const rows = app.db.select().from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).all();
23104
+ const rows = app.db.select().from(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).all();
22410
23105
  const dtos = rows.map(rowToDto2);
22411
23106
  return {
22412
23107
  locations: dtos,
@@ -22415,15 +23110,15 @@ async function googleRoutes(app, opts) {
22415
23110
  };
22416
23111
  }
22417
23112
  function clearGbpProjectData(tx, projectId) {
22418
- tx.delete(gbpDailyMetrics).where(eq23(gbpDailyMetrics.projectId, projectId)).run();
22419
- tx.delete(gbpKeywordImpressions).where(eq23(gbpKeywordImpressions.projectId, projectId)).run();
22420
- tx.delete(gbpKeywordMonthly).where(eq23(gbpKeywordMonthly.projectId, projectId)).run();
22421
- tx.delete(gbpPlaceActions).where(eq23(gbpPlaceActions.projectId, projectId)).run();
22422
- tx.delete(gbpLodgingSnapshots).where(eq23(gbpLodgingSnapshots.projectId, projectId)).run();
22423
- tx.delete(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).run();
23113
+ tx.delete(gbpDailyMetrics).where(eq24(gbpDailyMetrics.projectId, projectId)).run();
23114
+ tx.delete(gbpKeywordImpressions).where(eq24(gbpKeywordImpressions.projectId, projectId)).run();
23115
+ tx.delete(gbpKeywordMonthly).where(eq24(gbpKeywordMonthly.projectId, projectId)).run();
23116
+ tx.delete(gbpPlaceActions).where(eq24(gbpPlaceActions.projectId, projectId)).run();
23117
+ tx.delete(gbpLodgingSnapshots).where(eq24(gbpLodgingSnapshots.projectId, projectId)).run();
23118
+ tx.delete(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).run();
22424
23119
  }
22425
23120
  function currentProjectAccount(projectId) {
22426
- const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).limit(1).get();
23121
+ const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq24(gbpLocations.projectId, projectId)).limit(1).get();
22427
23122
  return row?.accountName ?? null;
22428
23123
  }
22429
23124
  app.post("/projects/:name/gbp/locations/discover", async (request) => {
@@ -22493,7 +23188,7 @@ async function googleRoutes(app, opts) {
22493
23188
  app.db.transaction((tx) => {
22494
23189
  if (switching) clearGbpProjectData(tx, project.id);
22495
23190
  for (const remote of remoteLocations) {
22496
- const existing = tx.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, remote.name))).get();
23191
+ const existing = tx.select().from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.locationName, remote.name))).get();
22497
23192
  const profile = buildLocationProfileFields(remote);
22498
23193
  if (existing) {
22499
23194
  tx.update(gbpLocations).set({
@@ -22506,7 +23201,7 @@ async function googleRoutes(app, opts) {
22506
23201
  mapsUri: remote.metadata?.mapsUri ?? null,
22507
23202
  ...profile,
22508
23203
  updatedAt: now
22509
- }).where(eq23(gbpLocations.id, existing.id)).run();
23204
+ }).where(eq24(gbpLocations.id, existing.id)).run();
22510
23205
  } else {
22511
23206
  tx.insert(gbpLocations).values({
22512
23207
  id: crypto19.randomUUID(),
@@ -22590,11 +23285,11 @@ async function googleRoutes(app, opts) {
22590
23285
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
22591
23286
  }
22592
23287
  const { selected } = parsed.data;
22593
- const existing = app.db.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, locationName))).get();
23288
+ const existing = app.db.select().from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.locationName, locationName))).get();
22594
23289
  if (!existing) throw notFound("GBP location", locationName);
22595
23290
  const now = (/* @__PURE__ */ new Date()).toISOString();
22596
23291
  app.db.transaction((tx) => {
22597
- tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq23(gbpLocations.id, existing.id)).run();
23292
+ tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq24(gbpLocations.id, existing.id)).run();
22598
23293
  writeAuditLog(tx, {
22599
23294
  projectId: project.id,
22600
23295
  actor: "api",
@@ -22603,7 +23298,7 @@ async function googleRoutes(app, opts) {
22603
23298
  entityId: locationName
22604
23299
  });
22605
23300
  });
22606
- const refreshed = app.db.select().from(gbpLocations).where(eq23(gbpLocations.id, existing.id)).get();
23301
+ const refreshed = app.db.select().from(gbpLocations).where(eq24(gbpLocations.id, existing.id)).get();
22607
23302
  return rowToDto2(refreshed);
22608
23303
  });
22609
23304
  app.delete("/projects/:name/gbp/connection", async (request, reply) => {
@@ -22647,10 +23342,10 @@ async function googleRoutes(app, opts) {
22647
23342
  });
22648
23343
  app.get("/projects/:name/gbp/metrics", async (request) => {
22649
23344
  const project = resolveProject(app.db, request.params.name);
22650
- const conditions = [eq23(gbpDailyMetrics.projectId, project.id)];
22651
- if (request.query.locationName) conditions.push(eq23(gbpDailyMetrics.locationName, request.query.locationName));
22652
- if (request.query.metric) conditions.push(eq23(gbpDailyMetrics.metric, request.query.metric));
22653
- const rows = app.db.select().from(gbpDailyMetrics).where(and17(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
23345
+ const conditions = [eq24(gbpDailyMetrics.projectId, project.id)];
23346
+ if (request.query.locationName) conditions.push(eq24(gbpDailyMetrics.locationName, request.query.locationName));
23347
+ if (request.query.metric) conditions.push(eq24(gbpDailyMetrics.metric, request.query.metric));
23348
+ const rows = app.db.select().from(gbpDailyMetrics).where(and18(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
22654
23349
  return {
22655
23350
  metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
22656
23351
  total: rows.length
@@ -22658,9 +23353,9 @@ async function googleRoutes(app, opts) {
22658
23353
  });
22659
23354
  app.get("/projects/:name/gbp/keywords", async (request) => {
22660
23355
  const project = resolveProject(app.db, request.params.name);
22661
- const conditions = [eq23(gbpKeywordImpressions.projectId, project.id)];
22662
- if (request.query.locationName) conditions.push(eq23(gbpKeywordImpressions.locationName, request.query.locationName));
22663
- const rows = app.db.select().from(gbpKeywordImpressions).where(and17(...conditions)).all();
23356
+ const conditions = [eq24(gbpKeywordImpressions.projectId, project.id)];
23357
+ if (request.query.locationName) conditions.push(eq24(gbpKeywordImpressions.locationName, request.query.locationName));
23358
+ const rows = app.db.select().from(gbpKeywordImpressions).where(and18(...conditions)).all();
22664
23359
  rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
22665
23360
  const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
22666
23361
  return {
@@ -22678,9 +23373,9 @@ async function googleRoutes(app, opts) {
22678
23373
  });
22679
23374
  app.get("/projects/:name/gbp/place-actions", async (request) => {
22680
23375
  const project = resolveProject(app.db, request.params.name);
22681
- const conditions = [eq23(gbpPlaceActions.projectId, project.id)];
22682
- if (request.query.locationName) conditions.push(eq23(gbpPlaceActions.locationName, request.query.locationName));
22683
- const rows = app.db.select().from(gbpPlaceActions).where(and17(...conditions)).all();
23376
+ const conditions = [eq24(gbpPlaceActions.projectId, project.id)];
23377
+ if (request.query.locationName) conditions.push(eq24(gbpPlaceActions.locationName, request.query.locationName));
23378
+ const rows = app.db.select().from(gbpPlaceActions).where(and18(...conditions)).all();
22684
23379
  return {
22685
23380
  placeActions: rows.map((r) => ({
22686
23381
  locationName: r.locationName,
@@ -22695,9 +23390,9 @@ async function googleRoutes(app, opts) {
22695
23390
  });
22696
23391
  app.get("/projects/:name/gbp/lodging", async (request) => {
22697
23392
  const project = resolveProject(app.db, request.params.name);
22698
- const conditions = [eq23(gbpLodgingSnapshots.projectId, project.id)];
22699
- if (request.query.locationName) conditions.push(eq23(gbpLodgingSnapshots.locationName, request.query.locationName));
22700
- const rows = app.db.select().from(gbpLodgingSnapshots).where(and17(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
23393
+ const conditions = [eq24(gbpLodgingSnapshots.projectId, project.id)];
23394
+ if (request.query.locationName) conditions.push(eq24(gbpLodgingSnapshots.locationName, request.query.locationName));
23395
+ const rows = app.db.select().from(gbpLodgingSnapshots).where(and18(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22701
23396
  const latestByLocation = /* @__PURE__ */ new Map();
22702
23397
  for (const row of rows) {
22703
23398
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22712,9 +23407,9 @@ async function googleRoutes(app, opts) {
22712
23407
  });
22713
23408
  app.get("/projects/:name/gbp/attributes", async (request) => {
22714
23409
  const project = resolveProject(app.db, request.params.name);
22715
- const conditions = [eq23(gbpAttributesSnapshots.projectId, project.id)];
22716
- if (request.query.locationName) conditions.push(eq23(gbpAttributesSnapshots.locationName, request.query.locationName));
22717
- const rows = app.db.select().from(gbpAttributesSnapshots).where(and17(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
23410
+ const conditions = [eq24(gbpAttributesSnapshots.projectId, project.id)];
23411
+ if (request.query.locationName) conditions.push(eq24(gbpAttributesSnapshots.locationName, request.query.locationName));
23412
+ const rows = app.db.select().from(gbpAttributesSnapshots).where(and18(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
22718
23413
  const latestByLocation = /* @__PURE__ */ new Map();
22719
23414
  for (const row of rows) {
22720
23415
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22732,9 +23427,9 @@ async function googleRoutes(app, opts) {
22732
23427
  });
22733
23428
  app.get("/projects/:name/gbp/places", async (request) => {
22734
23429
  const project = resolveProject(app.db, request.params.name);
22735
- const conditions = [eq23(gbpPlaceDetails.projectId, project.id)];
22736
- if (request.query.locationName) conditions.push(eq23(gbpPlaceDetails.locationName, request.query.locationName));
22737
- const rows = app.db.select().from(gbpPlaceDetails).where(and17(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
23430
+ const conditions = [eq24(gbpPlaceDetails.projectId, project.id)];
23431
+ if (request.query.locationName) conditions.push(eq24(gbpPlaceDetails.locationName, request.query.locationName));
23432
+ const rows = app.db.select().from(gbpPlaceDetails).where(and18(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
22738
23433
  const latestByLocation = /* @__PURE__ */ new Map();
22739
23434
  for (const row of rows) {
22740
23435
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22753,7 +23448,7 @@ async function googleRoutes(app, opts) {
22753
23448
  app.get("/projects/:name/gbp/summary", async (request) => {
22754
23449
  const project = resolveProject(app.db, request.params.name);
22755
23450
  const locationName = request.query.locationName ?? null;
22756
- const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.selected, true))).all().map((r) => r.n);
23451
+ const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), eq24(gbpLocations.selected, true))).all().map((r) => r.n);
22757
23452
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22758
23453
  if (locationNames.length === 0) {
22759
23454
  return buildGbpSummary({
@@ -22767,10 +23462,10 @@ async function googleRoutes(app, opts) {
22767
23462
  locationProfiles: []
22768
23463
  });
22769
23464
  }
22770
- const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(eq23(gbpDailyMetrics.projectId, project.id), inArray11(gbpDailyMetrics.locationName, locationNames))).all();
22771
- const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(eq23(gbpKeywordImpressions.projectId, project.id), inArray11(gbpKeywordImpressions.locationName, locationNames))).all();
22772
- const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(eq23(gbpPlaceActions.projectId, project.id), inArray11(gbpPlaceActions.locationName, locationNames))).all();
22773
- const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(eq23(gbpLodgingSnapshots.projectId, project.id), inArray11(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
23465
+ const metricRows = app.db.select().from(gbpDailyMetrics).where(and18(eq24(gbpDailyMetrics.projectId, project.id), inArray11(gbpDailyMetrics.locationName, locationNames))).all();
23466
+ const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and18(eq24(gbpKeywordImpressions.projectId, project.id), inArray11(gbpKeywordImpressions.locationName, locationNames))).all();
23467
+ const placeActionRows = app.db.select().from(gbpPlaceActions).where(and18(eq24(gbpPlaceActions.projectId, project.id), inArray11(gbpPlaceActions.locationName, locationNames))).all();
23468
+ const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and18(eq24(gbpLodgingSnapshots.projectId, project.id), inArray11(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22774
23469
  const latestLodgingByLocation = /* @__PURE__ */ new Map();
22775
23470
  for (const row of lodgingRows) {
22776
23471
  if (!latestLodgingByLocation.has(row.locationName)) {
@@ -22784,7 +23479,7 @@ async function googleRoutes(app, opts) {
22784
23479
  regularHours: gbpLocations.regularHours,
22785
23480
  primaryPhone: gbpLocations.primaryPhone,
22786
23481
  openStatus: gbpLocations.openStatus
22787
- }).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), inArray11(gbpLocations.locationName, locationNames))).all();
23482
+ }).from(gbpLocations).where(and18(eq24(gbpLocations.projectId, project.id), inArray11(gbpLocations.locationName, locationNames))).all();
22788
23483
  return buildGbpSummary({
22789
23484
  locationName,
22790
23485
  locationCount: locationNames.length,
@@ -22800,7 +23495,17 @@ async function googleRoutes(app, opts) {
22800
23495
 
22801
23496
  // ../api-routes/src/ads.ts
22802
23497
  import crypto20 from "crypto";
22803
- import { eq as eq24, and as and18, asc as asc3, gte as gte3, lte as lte2, inArray as inArray12 } from "drizzle-orm";
23498
+ import { eq as eq25, and as and19, asc as asc5, gte as gte5, lte as lte3, inArray as inArray12 } from "drizzle-orm";
23499
+ var AdsPausedPostconditionError = class extends Error {
23500
+ constructor(entityId) {
23501
+ super("OpenAI Ads API did not confirm the required paused state");
23502
+ this.entityId = entityId;
23503
+ this.name = "AdsPausedPostconditionError";
23504
+ }
23505
+ entityId;
23506
+ code = "ADS_PAUSED_POSTCONDITION_FAILED";
23507
+ status = 502;
23508
+ };
22804
23509
  function statusDto(row) {
22805
23510
  if (!row) return { connected: false };
22806
23511
  return {
@@ -22821,15 +23526,185 @@ function creativeDto(raw) {
22821
23526
  type: typeof c.type === "string" ? c.type : null,
22822
23527
  title: typeof c.title === "string" ? c.title : null,
22823
23528
  body: typeof c.body === "string" ? c.body : null,
22824
- targetUrl: typeof c.target_url === "string" ? c.target_url : null
23529
+ targetUrl: typeof c.target_url === "string" ? c.target_url : null,
23530
+ fileId: typeof c.file_id === "string" ? c.file_id : null
23531
+ };
23532
+ }
23533
+ function locationIdsDto(raw) {
23534
+ if (!raw || typeof raw !== "object") return [];
23535
+ const locations = raw.locations;
23536
+ if (!locations || typeof locations !== "object") return [];
23537
+ const include = locations.include;
23538
+ if (!Array.isArray(include)) return [];
23539
+ return include.flatMap((entry) => {
23540
+ if (!entry || typeof entry !== "object") return [];
23541
+ const id = entry.id;
23542
+ return typeof id === "string" && id.length > 0 ? [id] : [];
23543
+ });
23544
+ }
23545
+ function operationDto(row) {
23546
+ return {
23547
+ id: row.id,
23548
+ operationKey: row.operationKey,
23549
+ kind: row.kind,
23550
+ state: row.state,
23551
+ entityType: row.entityType,
23552
+ entityId: row.entityId,
23553
+ upstreamUpdatedAt: row.upstreamUpdatedAt,
23554
+ errorCode: row.errorCode,
23555
+ errorMessage: row.errorMessage,
23556
+ createdAt: row.createdAt,
23557
+ updatedAt: row.updatedAt
23558
+ };
23559
+ }
23560
+ function canonicalize(value) {
23561
+ if (Array.isArray(value)) return value.map(canonicalize);
23562
+ if (!value || typeof value !== "object") return value;
23563
+ const out = {};
23564
+ for (const key of Object.keys(value).sort()) {
23565
+ const child = value[key];
23566
+ if (child !== void 0) out[key] = canonicalize(child);
23567
+ }
23568
+ return out;
23569
+ }
23570
+ function requestHash(value) {
23571
+ return crypto20.createHash("sha256").update(JSON.stringify(canonicalize(value))).digest("hex");
23572
+ }
23573
+ function errorDetails(err) {
23574
+ const candidate = err;
23575
+ const status = typeof candidate.status === "number" ? candidate.status : void 0;
23576
+ const knownClientFailure = err instanceof AppError || status !== void 0 && status >= 400 && status < 500 && status !== 408 && status !== 429;
23577
+ const rawCode = typeof candidate.code === "string" ? candidate.code : err instanceof AppError ? err.code : "upstream_error";
23578
+ const code = rawCode.replace(/[^\w.:-]/g, "_").slice(0, 100) || "upstream_error";
23579
+ const message = err instanceof AppError ? err.message.slice(0, 500) : knownClientFailure ? "OpenAI Ads API rejected the operation" : "OpenAI Ads API outcome could not be confirmed";
23580
+ return {
23581
+ state: knownClientFailure ? AdsOperationStates.failed : AdsOperationStates.unknown,
23582
+ code,
23583
+ message,
23584
+ entityId: typeof candidate.entityId === "string" ? candidate.entityId.slice(0, 200) : void 0
22825
23585
  };
22826
23586
  }
23587
+ function resolveAdsOperator(opts, projectName) {
23588
+ const apiKey = opts.adsCredentialStore?.getConnection(projectName)?.apiKey;
23589
+ if (!apiKey) {
23590
+ throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
23591
+ }
23592
+ if (!opts.adsOperator) {
23593
+ throw validationError("Ads lifecycle operations are not configured for this deployment");
23594
+ }
23595
+ return { apiKey, operator: opts.adsOperator };
23596
+ }
23597
+ async function executeAdsOperation(app, request, input) {
23598
+ const hash = requestHash({ kind: input.kind, entityType: input.entityType, payload: input.payload });
23599
+ const existing = app.db.select().from(adsOperations).where(and19(
23600
+ eq25(adsOperations.projectId, input.projectId),
23601
+ eq25(adsOperations.operationKey, input.operationKey)
23602
+ )).get();
23603
+ if (existing) {
23604
+ if (existing.requestHash !== hash) {
23605
+ throw alreadyExists("Ads operation key", input.operationKey);
23606
+ }
23607
+ return { operation: operationDto(existing), replayed: true };
23608
+ }
23609
+ const id = crypto20.randomUUID();
23610
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
23611
+ app.db.insert(adsOperations).values({
23612
+ id,
23613
+ projectId: input.projectId,
23614
+ operationKey: input.operationKey,
23615
+ requestHash: hash,
23616
+ kind: input.kind,
23617
+ state: AdsOperationStates.pending,
23618
+ entityType: input.entityType,
23619
+ createdAt,
23620
+ updatedAt: createdAt
23621
+ }).run();
23622
+ let result;
23623
+ try {
23624
+ result = await input.run();
23625
+ if (input.expectedStatus !== void 0 && result.status !== input.expectedStatus) {
23626
+ if (input.remediateStatus) result = await input.remediateStatus(result);
23627
+ if (result.status !== input.expectedStatus) {
23628
+ throw new AdsPausedPostconditionError(result.id);
23629
+ }
23630
+ }
23631
+ const succeededResult = result;
23632
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23633
+ app.db.transaction((tx) => {
23634
+ tx.update(adsOperations).set({
23635
+ state: AdsOperationStates.succeeded,
23636
+ entityId: succeededResult.id,
23637
+ upstreamUpdatedAt: succeededResult.updatedAt ?? null,
23638
+ updatedAt
23639
+ }).where(eq25(adsOperations.id, id)).run();
23640
+ writeAuditLog(tx, auditFromRequest(request, {
23641
+ projectId: input.projectId,
23642
+ actor: "api",
23643
+ action: `ads.${input.kind}.succeeded`,
23644
+ entityType: input.entityType,
23645
+ entityId: succeededResult.id,
23646
+ diff: { operationId: id, operationKey: input.operationKey }
23647
+ }));
23648
+ });
23649
+ const row = app.db.select().from(adsOperations).where(eq25(adsOperations.id, id)).get();
23650
+ return { operation: operationDto(row), replayed: false };
23651
+ } catch (err) {
23652
+ const failure = errorDetails(err);
23653
+ const entityId = failure.entityId ?? result?.id;
23654
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23655
+ app.db.transaction((tx) => {
23656
+ tx.update(adsOperations).set({
23657
+ state: failure.state,
23658
+ entityId,
23659
+ errorCode: failure.code,
23660
+ errorMessage: failure.message,
23661
+ updatedAt
23662
+ }).where(eq25(adsOperations.id, id)).run();
23663
+ writeAuditLog(tx, auditFromRequest(request, {
23664
+ projectId: input.projectId,
23665
+ actor: "api",
23666
+ action: `ads.${input.kind}.${failure.state}`,
23667
+ entityType: input.entityType,
23668
+ entityId: entityId ?? id,
23669
+ diff: { operationId: id, operationKey: input.operationKey, errorCode: failure.code }
23670
+ }));
23671
+ });
23672
+ if (err instanceof AppError) throw err;
23673
+ throw providerError("OpenAI Ads API mutation failed", {
23674
+ operationId: id,
23675
+ operationKey: input.operationKey,
23676
+ state: failure.state,
23677
+ code: failure.code
23678
+ });
23679
+ }
23680
+ }
23681
+ function parseBody(schema, value) {
23682
+ const parsed = schema.safeParse(value);
23683
+ if (!parsed.success) throw validationError("Invalid ads operation request", { issues: parsed.error.issues });
23684
+ return parsed.data;
23685
+ }
23686
+ function assertExpectedUpdatedAt(entity, expected) {
23687
+ if (entity.updatedAt !== expected) {
23688
+ throw validationError("The upstream ad entity changed since it was reviewed", {
23689
+ expectedUpdatedAt: expected,
23690
+ actualUpdatedAt: entity.updatedAt
23691
+ });
23692
+ }
23693
+ }
23694
+ function assertPausedForUpdate(entity) {
23695
+ if (entity.status !== AdsEntityStatuses.paused) {
23696
+ throw validationError("Pause the upstream ad entity before updating it", {
23697
+ actualStatus: entity.status
23698
+ });
23699
+ }
23700
+ }
22827
23701
  async function adsRoutes(app, opts) {
22828
23702
  app.post(
22829
23703
  "/projects/:name/ads/connect",
22830
23704
  async (request) => {
23705
+ requireScope(request, ADS_WRITE_SCOPE);
22831
23706
  const project = resolveProject(app.db, request.params.name);
22832
- const parsed = adsConnectRequestSchema.safeParse(request.body ?? {});
23707
+ const parsed = adsConnectRequestSchema.safeParse(request.body);
22833
23708
  if (!parsed.success) throw validationError('"apiKey" is required');
22834
23709
  if (!opts.adsCredentialStore || !opts.verifyAdsAccount) {
22835
23710
  throw validationError("Ads credential storage is not configured for this deployment");
@@ -22850,7 +23725,7 @@ async function adsRoutes(app, opts) {
22850
23725
  createdAt: existingCfg?.createdAt ?? now,
22851
23726
  updatedAt: now
22852
23727
  });
22853
- const existingRow = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
23728
+ const existingRow = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
22854
23729
  app.db.transaction((tx) => {
22855
23730
  if (existingRow) {
22856
23731
  tx.update(adsConnections).set({
@@ -22860,7 +23735,7 @@ async function adsRoutes(app, opts) {
22860
23735
  timezone: account.timezone,
22861
23736
  status: account.status,
22862
23737
  updatedAt: now
22863
- }).where(eq24(adsConnections.id, existingRow.id)).run();
23738
+ }).where(eq25(adsConnections.id, existingRow.id)).run();
22864
23739
  } else {
22865
23740
  tx.insert(adsConnections).values({
22866
23741
  id: crypto20.randomUUID(),
@@ -22882,16 +23757,17 @@ async function adsRoutes(app, opts) {
22882
23757
  entityId: account.id
22883
23758
  }));
22884
23759
  });
22885
- const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
23760
+ const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
22886
23761
  return statusDto(row);
22887
23762
  }
22888
23763
  );
22889
23764
  app.delete("/projects/:name/ads/connection", async (request) => {
23765
+ requireScope(request, ADS_WRITE_SCOPE);
22890
23766
  const project = resolveProject(app.db, request.params.name);
22891
- const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
23767
+ const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
22892
23768
  if (row) {
22893
23769
  app.db.transaction((tx) => {
22894
- tx.delete(adsConnections).where(eq24(adsConnections.id, row.id)).run();
23770
+ tx.delete(adsConnections).where(eq25(adsConnections.id, row.id)).run();
22895
23771
  writeAuditLog(tx, auditFromRequest(request, {
22896
23772
  projectId: project.id,
22897
23773
  actor: "api",
@@ -22907,18 +23783,232 @@ async function adsRoutes(app, opts) {
22907
23783
  });
22908
23784
  app.get("/projects/:name/ads/status", async (request) => {
22909
23785
  const project = resolveProject(app.db, request.params.name);
22910
- const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
23786
+ const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
22911
23787
  return statusDto(row);
22912
23788
  });
23789
+ app.get(
23790
+ "/projects/:name/ads/operations/:operationKey",
23791
+ async (request) => {
23792
+ const project = resolveProject(app.db, request.params.name);
23793
+ const row = app.db.select().from(adsOperations).where(and19(
23794
+ eq25(adsOperations.projectId, project.id),
23795
+ eq25(adsOperations.operationKey, request.params.operationKey)
23796
+ )).get();
23797
+ if (!row) throw notFound("Ads operation", request.params.operationKey);
23798
+ const response = { operation: operationDto(row), replayed: true };
23799
+ return response;
23800
+ }
23801
+ );
23802
+ app.post("/projects/:name/ads/files", async (request) => {
23803
+ requireScope(request, ADS_WRITE_SCOPE);
23804
+ const project = resolveProject(app.db, request.params.name);
23805
+ const body = parseBody(adsImageUploadRequestSchema, request.body);
23806
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23807
+ return executeAdsOperation(app, request, {
23808
+ projectId: project.id,
23809
+ operationKey: body.operationKey,
23810
+ kind: AdsOperationKinds.image_upload,
23811
+ entityType: AdsEntityTypes.file,
23812
+ payload: { imageUrl: body.imageUrl },
23813
+ run: async () => {
23814
+ const result = await operator.uploadImage(apiKey, body.imageUrl);
23815
+ return { id: result.fileId };
23816
+ }
23817
+ });
23818
+ });
23819
+ app.post("/projects/:name/ads/campaigns", async (request) => {
23820
+ requireScope(request, ADS_WRITE_SCOPE);
23821
+ const project = resolveProject(app.db, request.params.name);
23822
+ const body = parseBody(adsCampaignCreateRequestSchema, request.body);
23823
+ const { operationKey, ...providerInput } = body;
23824
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23825
+ return executeAdsOperation(app, request, {
23826
+ projectId: project.id,
23827
+ operationKey,
23828
+ kind: AdsOperationKinds.campaign_create,
23829
+ entityType: AdsEntityTypes.campaign,
23830
+ payload: providerInput,
23831
+ expectedStatus: AdsEntityStatuses.paused,
23832
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23833
+ run: async () => operator.createCampaign(apiKey, providerInput)
23834
+ });
23835
+ });
23836
+ app.post("/projects/:name/ads/ad-groups", async (request) => {
23837
+ requireScope(request, ADS_WRITE_SCOPE);
23838
+ const project = resolveProject(app.db, request.params.name);
23839
+ const body = parseBody(adsAdGroupCreateRequestSchema, request.body);
23840
+ const { operationKey, ...providerInput } = body;
23841
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23842
+ return executeAdsOperation(app, request, {
23843
+ projectId: project.id,
23844
+ operationKey,
23845
+ kind: AdsOperationKinds.ad_group_create,
23846
+ entityType: AdsEntityTypes.ad_group,
23847
+ payload: providerInput,
23848
+ expectedStatus: AdsEntityStatuses.paused,
23849
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23850
+ run: async () => operator.createAdGroup(apiKey, providerInput)
23851
+ });
23852
+ });
23853
+ app.post("/projects/:name/ads/ads", async (request) => {
23854
+ requireScope(request, ADS_WRITE_SCOPE);
23855
+ const project = resolveProject(app.db, request.params.name);
23856
+ const body = parseBody(adsAdCreateRequestSchema, request.body);
23857
+ const { operationKey, ...providerInput } = body;
23858
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23859
+ return executeAdsOperation(app, request, {
23860
+ projectId: project.id,
23861
+ operationKey,
23862
+ kind: AdsOperationKinds.ad_create,
23863
+ entityType: AdsEntityTypes.ad,
23864
+ payload: providerInput,
23865
+ expectedStatus: AdsEntityStatuses.paused,
23866
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23867
+ run: async () => operator.createAd(apiKey, providerInput)
23868
+ });
23869
+ });
23870
+ app.post(
23871
+ "/projects/:name/ads/campaigns/:id",
23872
+ async (request) => {
23873
+ requireScope(request, ADS_WRITE_SCOPE);
23874
+ const project = resolveProject(app.db, request.params.name);
23875
+ const body = parseBody(adsCampaignUpdateRequestSchema, request.body);
23876
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23877
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23878
+ return executeAdsOperation(app, request, {
23879
+ projectId: project.id,
23880
+ operationKey,
23881
+ kind: AdsOperationKinds.campaign_update,
23882
+ entityType: AdsEntityTypes.campaign,
23883
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23884
+ expectedStatus: AdsEntityStatuses.paused,
23885
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23886
+ run: async () => {
23887
+ const current = await operator.getCampaign(apiKey, request.params.id);
23888
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23889
+ assertPausedForUpdate(current);
23890
+ return operator.updateCampaign(apiKey, request.params.id, update);
23891
+ }
23892
+ });
23893
+ }
23894
+ );
23895
+ app.post(
23896
+ "/projects/:name/ads/ad-groups/:id",
23897
+ async (request) => {
23898
+ requireScope(request, ADS_WRITE_SCOPE);
23899
+ const project = resolveProject(app.db, request.params.name);
23900
+ const body = parseBody(adsAdGroupUpdateRequestSchema, request.body);
23901
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23902
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23903
+ return executeAdsOperation(app, request, {
23904
+ projectId: project.id,
23905
+ operationKey,
23906
+ kind: AdsOperationKinds.ad_group_update,
23907
+ entityType: AdsEntityTypes.ad_group,
23908
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23909
+ expectedStatus: AdsEntityStatuses.paused,
23910
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23911
+ run: async () => {
23912
+ const current = await operator.getAdGroup(apiKey, request.params.id);
23913
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23914
+ assertPausedForUpdate(current);
23915
+ return operator.updateAdGroup(apiKey, request.params.id, update);
23916
+ }
23917
+ });
23918
+ }
23919
+ );
23920
+ app.post(
23921
+ "/projects/:name/ads/ads/:id",
23922
+ async (request) => {
23923
+ requireScope(request, ADS_WRITE_SCOPE);
23924
+ const project = resolveProject(app.db, request.params.name);
23925
+ const body = parseBody(adsAdUpdateRequestSchema, request.body);
23926
+ const { operationKey, expectedUpdatedAt, ...update } = body;
23927
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23928
+ return executeAdsOperation(app, request, {
23929
+ projectId: project.id,
23930
+ operationKey,
23931
+ kind: AdsOperationKinds.ad_update,
23932
+ entityType: AdsEntityTypes.ad,
23933
+ payload: { id: request.params.id, expectedUpdatedAt, update },
23934
+ expectedStatus: AdsEntityStatuses.paused,
23935
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23936
+ run: async () => {
23937
+ const current = await operator.getAd(apiKey, request.params.id);
23938
+ assertExpectedUpdatedAt(current, expectedUpdatedAt);
23939
+ assertPausedForUpdate(current);
23940
+ return operator.updateAd(apiKey, request.params.id, update);
23941
+ }
23942
+ });
23943
+ }
23944
+ );
23945
+ app.post(
23946
+ "/projects/:name/ads/campaigns/:id/pause",
23947
+ async (request) => {
23948
+ requireScope(request, ADS_WRITE_SCOPE);
23949
+ const project = resolveProject(app.db, request.params.name);
23950
+ const body = parseBody(adsPauseRequestSchema, request.body);
23951
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23952
+ return executeAdsOperation(app, request, {
23953
+ projectId: project.id,
23954
+ operationKey: body.operationKey,
23955
+ kind: AdsOperationKinds.campaign_pause,
23956
+ entityType: AdsEntityTypes.campaign,
23957
+ payload: { id: request.params.id },
23958
+ expectedStatus: AdsEntityStatuses.paused,
23959
+ remediateStatus: (result) => operator.pauseCampaign(apiKey, result.id),
23960
+ run: async () => operator.pauseCampaign(apiKey, request.params.id)
23961
+ });
23962
+ }
23963
+ );
23964
+ app.post(
23965
+ "/projects/:name/ads/ad-groups/:id/pause",
23966
+ async (request) => {
23967
+ requireScope(request, ADS_WRITE_SCOPE);
23968
+ const project = resolveProject(app.db, request.params.name);
23969
+ const body = parseBody(adsPauseRequestSchema, request.body);
23970
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23971
+ return executeAdsOperation(app, request, {
23972
+ projectId: project.id,
23973
+ operationKey: body.operationKey,
23974
+ kind: AdsOperationKinds.ad_group_pause,
23975
+ entityType: AdsEntityTypes.ad_group,
23976
+ payload: { id: request.params.id },
23977
+ expectedStatus: AdsEntityStatuses.paused,
23978
+ remediateStatus: (result) => operator.pauseAdGroup(apiKey, result.id),
23979
+ run: async () => operator.pauseAdGroup(apiKey, request.params.id)
23980
+ });
23981
+ }
23982
+ );
23983
+ app.post(
23984
+ "/projects/:name/ads/ads/:id/pause",
23985
+ async (request) => {
23986
+ requireScope(request, ADS_WRITE_SCOPE);
23987
+ const project = resolveProject(app.db, request.params.name);
23988
+ const body = parseBody(adsPauseRequestSchema, request.body);
23989
+ const { apiKey, operator } = resolveAdsOperator(opts, project.name);
23990
+ return executeAdsOperation(app, request, {
23991
+ projectId: project.id,
23992
+ operationKey: body.operationKey,
23993
+ kind: AdsOperationKinds.ad_pause,
23994
+ entityType: AdsEntityTypes.ad,
23995
+ payload: { id: request.params.id },
23996
+ expectedStatus: AdsEntityStatuses.paused,
23997
+ remediateStatus: (result) => operator.pauseAd(apiKey, result.id),
23998
+ run: async () => operator.pauseAd(apiKey, request.params.id)
23999
+ });
24000
+ }
24001
+ );
22913
24002
  app.post("/projects/:name/ads/sync", async (request) => {
24003
+ requireScope(request, ADS_WRITE_SCOPE);
22914
24004
  const project = resolveProject(app.db, request.params.name);
22915
- const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
24005
+ const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
22916
24006
  if (!row) {
22917
24007
  throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
22918
24008
  }
22919
- const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and18(
22920
- eq24(runs.projectId, project.id),
22921
- eq24(runs.kind, RunKinds["ads-sync"]),
24009
+ const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and19(
24010
+ eq25(runs.projectId, project.id),
24011
+ eq25(runs.kind, RunKinds["ads-sync"]),
22922
24012
  inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
22923
24013
  )).get();
22924
24014
  if (inFlight) {
@@ -22940,9 +24030,9 @@ async function adsRoutes(app, opts) {
22940
24030
  });
22941
24031
  app.get("/projects/:name/ads/campaigns", async (request) => {
22942
24032
  const project = resolveProject(app.db, request.params.name);
22943
- const campaignRows = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all();
22944
- const groupRows = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all();
22945
- const adRows = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all();
24033
+ const campaignRows = app.db.select().from(adsCampaigns).where(eq25(adsCampaigns.projectId, project.id)).all();
24034
+ const groupRows = app.db.select().from(adsAdGroups).where(eq25(adsAdGroups.projectId, project.id)).all();
24035
+ const adRows = app.db.select().from(adsAds).where(eq25(adsAds.projectId, project.id)).all();
22946
24036
  const adsByGroup = /* @__PURE__ */ new Map();
22947
24037
  for (const ad of adRows) {
22948
24038
  const dto = {
@@ -22951,7 +24041,9 @@ async function adsRoutes(app, opts) {
22951
24041
  name: ad.name,
22952
24042
  status: ad.status,
22953
24043
  reviewStatus: ad.reviewStatus,
22954
- creative: creativeDto(ad.creative)
24044
+ creative: creativeDto(ad.creative),
24045
+ upstreamUpdatedAt: ad.upstreamUpdatedAt,
24046
+ syncedAt: ad.syncedAt
22955
24047
  };
22956
24048
  const list = adsByGroup.get(ad.adGroupId) ?? [];
22957
24049
  list.push(dto);
@@ -22963,11 +24055,14 @@ async function adsRoutes(app, opts) {
22963
24055
  id: group.id,
22964
24056
  campaignId: group.campaignId,
22965
24057
  name: group.name,
24058
+ description: group.description,
22966
24059
  status: group.status,
22967
24060
  billingEventType: group.billingEventType,
22968
24061
  maxBidMicros: group.maxBidMicros,
22969
24062
  contextHints: group.contextHints,
22970
- ads: adsByGroup.get(group.id) ?? []
24063
+ ads: adsByGroup.get(group.id) ?? [],
24064
+ upstreamUpdatedAt: group.upstreamUpdatedAt,
24065
+ syncedAt: group.syncedAt
22971
24066
  };
22972
24067
  const list = groupsByCampaign.get(group.campaignId) ?? [];
22973
24068
  list.push(dto);
@@ -22976,11 +24071,17 @@ async function adsRoutes(app, opts) {
22976
24071
  const campaigns = campaignRows.map((campaign) => ({
22977
24072
  id: campaign.id,
22978
24073
  name: campaign.name,
24074
+ description: campaign.description,
22979
24075
  status: campaign.status,
24076
+ startTime: campaign.startTime,
24077
+ endTime: campaign.endTime,
22980
24078
  biddingType: campaign.biddingType,
22981
24079
  dailySpendLimitMicros: campaign.dailySpendLimitMicros,
22982
24080
  lifetimeSpendLimitMicros: campaign.lifetimeSpendLimitMicros,
22983
- adGroups: groupsByCampaign.get(campaign.id) ?? []
24081
+ locationIds: locationIdsDto(campaign.targeting),
24082
+ adGroups: groupsByCampaign.get(campaign.id) ?? [],
24083
+ upstreamUpdatedAt: campaign.upstreamUpdatedAt,
24084
+ syncedAt: campaign.syncedAt
22984
24085
  }));
22985
24086
  const response = { campaigns };
22986
24087
  return response;
@@ -22996,12 +24097,12 @@ async function adsRoutes(app, opts) {
22996
24097
  }
22997
24098
  parsedLevel = result.data;
22998
24099
  }
22999
- const conditions = [eq24(adsInsightsDaily.projectId, project.id)];
23000
- if (parsedLevel) conditions.push(eq24(adsInsightsDaily.level, parsedLevel));
23001
- if (entityId) conditions.push(eq24(adsInsightsDaily.entityId, entityId));
23002
- if (from) conditions.push(gte3(adsInsightsDaily.date, from));
23003
- if (to) conditions.push(lte2(adsInsightsDaily.date, to));
23004
- const rows = app.db.select().from(adsInsightsDaily).where(and18(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
24100
+ const conditions = [eq25(adsInsightsDaily.projectId, project.id)];
24101
+ if (parsedLevel) conditions.push(eq25(adsInsightsDaily.level, parsedLevel));
24102
+ if (entityId) conditions.push(eq25(adsInsightsDaily.entityId, entityId));
24103
+ if (from) conditions.push(gte5(adsInsightsDaily.date, from));
24104
+ if (to) conditions.push(lte3(adsInsightsDaily.date, to));
24105
+ const rows = app.db.select().from(adsInsightsDaily).where(and19(...conditions)).orderBy(asc5(adsInsightsDaily.date)).all();
23005
24106
  const dtoRows = rows.map((row) => ({
23006
24107
  level: row.level,
23007
24108
  entityId: row.entityId,
@@ -23013,19 +24114,19 @@ async function adsRoutes(app, opts) {
23013
24114
  ctr: adsCtr(row.clicks, row.impressions),
23014
24115
  cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
23015
24116
  }));
23016
- const conn = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
24117
+ const conn = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
23017
24118
  const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
23018
24119
  return response;
23019
24120
  });
23020
24121
  app.get("/projects/:name/ads/summary", async (request) => {
23021
24122
  const project = resolveProject(app.db, request.params.name);
23022
- const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
23023
- const campaignCount = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all().length;
23024
- const adGroupCount = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all().length;
23025
- const adCount = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all().length;
23026
- const campaignInsights = app.db.select().from(adsInsightsDaily).where(and18(
23027
- eq24(adsInsightsDaily.projectId, project.id),
23028
- eq24(adsInsightsDaily.level, "campaign")
24123
+ const row = app.db.select().from(adsConnections).where(eq25(adsConnections.projectId, project.id)).get();
24124
+ const campaignCount = app.db.select().from(adsCampaigns).where(eq25(adsCampaigns.projectId, project.id)).all().length;
24125
+ const adGroupCount = app.db.select().from(adsAdGroups).where(eq25(adsAdGroups.projectId, project.id)).all().length;
24126
+ const adCount = app.db.select().from(adsAds).where(eq25(adsAds.projectId, project.id)).all().length;
24127
+ const campaignInsights = app.db.select().from(adsInsightsDaily).where(and19(
24128
+ eq25(adsInsightsDaily.projectId, project.id),
24129
+ eq25(adsInsightsDaily.level, "campaign")
23029
24130
  )).all();
23030
24131
  let impressions = 0;
23031
24132
  let clicks = 0;
@@ -23065,7 +24166,7 @@ async function adsRoutes(app, opts) {
23065
24166
 
23066
24167
  // ../api-routes/src/bing.ts
23067
24168
  import crypto21 from "crypto";
23068
- import { eq as eq25, and as and19, desc as desc12 } from "drizzle-orm";
24169
+ import { eq as eq26, and as and20, desc as desc12 } from "drizzle-orm";
23069
24170
 
23070
24171
  // ../integration-bing/src/constants.ts
23071
24172
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -23439,7 +24540,7 @@ async function bingRoutes(app, opts) {
23439
24540
  const store = requireConnectionStore();
23440
24541
  const project = resolveProject(app.db, request.params.name);
23441
24542
  requireConnection(store, project.canonicalDomain);
23442
- const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
24543
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
23443
24544
  const latestByUrl = /* @__PURE__ */ new Map();
23444
24545
  const definitiveByUrl = /* @__PURE__ */ new Map();
23445
24546
  for (const row of allInspections) {
@@ -23528,7 +24629,7 @@ async function bingRoutes(app, opts) {
23528
24629
  const project = resolveProject(app.db, request.params.name);
23529
24630
  const parsed = parseInt(request.query.limit ?? "90", 10);
23530
24631
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
23531
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq25(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
24632
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq26(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
23532
24633
  return rows.map((r) => ({
23533
24634
  date: r.date,
23534
24635
  indexed: r.indexed,
@@ -23540,7 +24641,7 @@ async function bingRoutes(app, opts) {
23540
24641
  requireConnectionStore();
23541
24642
  const project = resolveProject(app.db, request.params.name);
23542
24643
  const { url, limit } = request.query;
23543
- const whereClause = url ? and19(eq25(bingUrlInspections.projectId, project.id), eq25(bingUrlInspections.url, url)) : eq25(bingUrlInspections.projectId, project.id);
24644
+ const whereClause = url ? and20(eq26(bingUrlInspections.projectId, project.id), eq26(bingUrlInspections.url, url)) : eq26(bingUrlInspections.projectId, project.id);
23544
24645
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
23545
24646
  return filtered.map((r) => ({
23546
24647
  id: r.id,
@@ -23630,7 +24731,7 @@ async function bingRoutes(app, opts) {
23630
24731
  anchorCount: result.AnchorCount ?? null,
23631
24732
  discoveryDate
23632
24733
  }).run();
23633
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq25(runs.id, runId)).run();
24734
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq26(runs.id, runId)).run();
23634
24735
  return {
23635
24736
  id,
23636
24737
  url,
@@ -23646,7 +24747,7 @@ async function bingRoutes(app, opts) {
23646
24747
  } catch (e) {
23647
24748
  const msg = e instanceof Error ? e.message : String(e);
23648
24749
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
23649
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
24750
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
23650
24751
  throw e;
23651
24752
  }
23652
24753
  });
@@ -23673,7 +24774,7 @@ async function bingRoutes(app, opts) {
23673
24774
  } else {
23674
24775
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
23675
24776
  }
23676
- const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
24777
+ const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
23677
24778
  return run;
23678
24779
  });
23679
24780
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -23685,7 +24786,7 @@ async function bingRoutes(app, opts) {
23685
24786
  }
23686
24787
  let urlsToSubmit = request.body?.urls ?? [];
23687
24788
  if (request.body?.allUnindexed) {
23688
- const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
24789
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
23689
24790
  const latestByUrl = /* @__PURE__ */ new Map();
23690
24791
  for (const row of allInspections) {
23691
24792
  if (!latestByUrl.has(row.url)) {
@@ -23772,14 +24873,14 @@ async function bingRoutes(app, opts) {
23772
24873
  import fs from "fs";
23773
24874
  import path from "path";
23774
24875
  import os from "os";
23775
- import { eq as eq26, and as and20 } from "drizzle-orm";
24876
+ import { eq as eq27, and as and21 } from "drizzle-orm";
23776
24877
  function getScreenshotDir() {
23777
24878
  return path.join(os.homedir(), ".canonry", "screenshots");
23778
24879
  }
23779
24880
  async function cdpRoutes(app, opts) {
23780
24881
  app.get("/screenshots/:snapshotId", async (request, reply) => {
23781
24882
  const { snapshotId } = request.params;
23782
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq26(querySnapshots.runId, runs.id)).where(eq26(querySnapshots.id, snapshotId)).get();
24883
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq27(querySnapshots.runId, runs.id)).where(eq27(querySnapshots.id, snapshotId)).get();
23783
24884
  if (!snapshot?.screenshotPath) {
23784
24885
  const err = notFound("Screenshot", snapshotId);
23785
24886
  return reply.code(err.statusCode).send(err.toJSON());
@@ -23847,7 +24948,7 @@ async function cdpRoutes(app, opts) {
23847
24948
  async (request, reply) => {
23848
24949
  const project = resolveProject(app.db, request.params.name);
23849
24950
  const { runId } = request.params;
23850
- const run = app.db.select().from(runs).where(and20(eq26(runs.id, runId), eq26(runs.projectId, project.id))).get();
24951
+ const run = app.db.select().from(runs).where(and21(eq27(runs.id, runId), eq27(runs.projectId, project.id))).get();
23851
24952
  if (!run) {
23852
24953
  const err = notFound("Run", runId);
23853
24954
  return reply.code(err.statusCode).send(err.toJSON());
@@ -23860,8 +24961,8 @@ async function cdpRoutes(app, opts) {
23860
24961
  citedDomains: querySnapshots.citedDomains,
23861
24962
  screenshotPath: querySnapshots.screenshotPath,
23862
24963
  rawResponse: querySnapshots.rawResponse
23863
- }).from(querySnapshots).where(eq26(querySnapshots.runId, runId)).all());
23864
- const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq26(queries.projectId, project.id)).all();
24964
+ }).from(querySnapshots).where(eq27(querySnapshots.runId, runId)).all());
24965
+ const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq27(queries.projectId, project.id)).all();
23865
24966
  const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
23866
24967
  const byQuery = /* @__PURE__ */ new Map();
23867
24968
  for (const snap of snapshots) {
@@ -23944,7 +25045,7 @@ async function cdpRoutes(app, opts) {
23944
25045
 
23945
25046
  // ../api-routes/src/ga.ts
23946
25047
  import crypto22 from "crypto";
23947
- import { eq as eq27, desc as desc13, and as and21, sql as sql10 } from "drizzle-orm";
25048
+ import { eq as eq28, desc as desc13, and as and22, sql as sql10 } from "drizzle-orm";
23948
25049
  function gaLog(level, action, ctx) {
23949
25050
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
23950
25051
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -24221,10 +25322,10 @@ async function ga4Routes(app, opts) {
24221
25322
  if (!saConn && !oauthConn) {
24222
25323
  throw notFound("GA4 connection", project.name);
24223
25324
  }
24224
- app.db.delete(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).run();
24225
- app.db.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
24226
- app.db.delete(gaAiReferrals).where(eq27(gaAiReferrals.projectId, project.id)).run();
24227
- app.db.delete(gaSocialReferrals).where(eq27(gaSocialReferrals.projectId, project.id)).run();
25325
+ app.db.delete(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, project.id)).run();
25326
+ app.db.delete(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).run();
25327
+ app.db.delete(gaAiReferrals).where(eq28(gaAiReferrals.projectId, project.id)).run();
25328
+ app.db.delete(gaSocialReferrals).where(eq28(gaSocialReferrals.projectId, project.id)).run();
24228
25329
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
24229
25330
  opts.ga4CredentialStore?.deleteConnection(project.name);
24230
25331
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -24245,7 +25346,7 @@ async function ga4Routes(app, opts) {
24245
25346
  if (!connected) {
24246
25347
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
24247
25348
  }
24248
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
25349
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
24249
25350
  return {
24250
25351
  connected: true,
24251
25352
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -24309,8 +25410,8 @@ async function ga4Routes(app, opts) {
24309
25410
  app.db.transaction((tx) => {
24310
25411
  if (syncTraffic) {
24311
25412
  tx.delete(gaTrafficSnapshots).where(
24312
- and21(
24313
- eq27(gaTrafficSnapshots.projectId, project.id),
25413
+ and22(
25414
+ eq28(gaTrafficSnapshots.projectId, project.id),
24314
25415
  sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
24315
25416
  sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
24316
25417
  )
@@ -24333,8 +25434,8 @@ async function ga4Routes(app, opts) {
24333
25434
  }
24334
25435
  if (syncAi) {
24335
25436
  tx.delete(gaAiReferrals).where(
24336
- and21(
24337
- eq27(gaAiReferrals.projectId, project.id),
25437
+ and22(
25438
+ eq28(gaAiReferrals.projectId, project.id),
24338
25439
  sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
24339
25440
  sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
24340
25441
  )
@@ -24365,8 +25466,8 @@ async function ga4Routes(app, opts) {
24365
25466
  }
24366
25467
  if (syncSocial) {
24367
25468
  tx.delete(gaSocialReferrals).where(
24368
- and21(
24369
- eq27(gaSocialReferrals.projectId, project.id),
25469
+ and22(
25470
+ eq28(gaSocialReferrals.projectId, project.id),
24370
25471
  sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
24371
25472
  sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
24372
25473
  )
@@ -24387,7 +25488,7 @@ async function ga4Routes(app, opts) {
24387
25488
  }
24388
25489
  }
24389
25490
  if (syncSummary) {
24390
- tx.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
25491
+ tx.delete(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).run();
24391
25492
  tx.insert(gaTrafficSummaries).values({
24392
25493
  id: crypto22.randomUUID(),
24393
25494
  projectId: project.id,
@@ -24399,7 +25500,7 @@ async function ga4Routes(app, opts) {
24399
25500
  syncedAt: now,
24400
25501
  syncRunId: runId
24401
25502
  }).run();
24402
- tx.delete(gaTrafficWindowSummaries).where(eq27(gaTrafficWindowSummaries.projectId, project.id)).run();
25503
+ tx.delete(gaTrafficWindowSummaries).where(eq28(gaTrafficWindowSummaries.projectId, project.id)).run();
24403
25504
  for (const ws of windowSummaries) {
24404
25505
  tx.insert(gaTrafficWindowSummaries).values({
24405
25506
  id: crypto22.randomUUID(),
@@ -24417,7 +25518,7 @@ async function ga4Routes(app, opts) {
24417
25518
  }
24418
25519
  }
24419
25520
  });
24420
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq27(runs.id, runId)).run();
25521
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq28(runs.id, runId)).run();
24421
25522
  const syncedComponents = only ? [
24422
25523
  ...syncTraffic ? ["traffic"] : [],
24423
25524
  ...syncSummary ? ["summary"] : [],
@@ -24446,7 +25547,7 @@ async function ga4Routes(app, opts) {
24446
25547
  } catch (e) {
24447
25548
  const msg = e instanceof Error ? e.message : String(e);
24448
25549
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
24449
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
25550
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq28(runs.id, runId)).run();
24450
25551
  throw e;
24451
25552
  }
24452
25553
  });
@@ -24457,11 +25558,11 @@ async function ga4Routes(app, opts) {
24457
25558
  const window = parseWindow(request.query.window);
24458
25559
  const cutoff = windowCutoff(window);
24459
25560
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
24460
- const snapshotConditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
25561
+ const snapshotConditions = [eq28(gaTrafficSnapshots.projectId, project.id)];
24461
25562
  if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
24462
- const aiConditions = [eq27(gaAiReferrals.projectId, project.id)];
25563
+ const aiConditions = [eq28(gaAiReferrals.projectId, project.id)];
24463
25564
  if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
24464
- const socialConditions = [eq27(gaSocialReferrals.projectId, project.id)];
25565
+ const socialConditions = [eq28(gaSocialReferrals.projectId, project.id)];
24465
25566
  if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
24466
25567
  const windowSummaryRow = cutoffDate ? app.db.select({
24467
25568
  totalSessions: gaTrafficWindowSummaries.totalSessions,
@@ -24469,35 +25570,35 @@ async function ga4Routes(app, opts) {
24469
25570
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
24470
25571
  totalUsers: gaTrafficWindowSummaries.totalUsers
24471
25572
  }).from(gaTrafficWindowSummaries).where(
24472
- and21(
24473
- eq27(gaTrafficWindowSummaries.projectId, project.id),
24474
- eq27(gaTrafficWindowSummaries.windowKey, window)
25573
+ and22(
25574
+ eq28(gaTrafficWindowSummaries.projectId, project.id),
25575
+ eq28(gaTrafficWindowSummaries.windowKey, window)
24475
25576
  )
24476
25577
  ).get() : null;
24477
25578
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
24478
25579
  totalSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
24479
25580
  totalOrganicSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
24480
25581
  totalUsers: sql10`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
24481
- }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get() : null;
25582
+ }).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).get() : null;
24482
25583
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
24483
25584
  totalSessions: gaTrafficSummaries.totalSessions,
24484
25585
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
24485
25586
  totalUsers: gaTrafficSummaries.totalUsers
24486
- }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
25587
+ }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).get();
24487
25588
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
24488
25589
  totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
24489
- }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get();
25590
+ }).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).get();
24490
25591
  const summaryMeta = app.db.select({
24491
25592
  periodStart: gaTrafficSummaries.periodStart,
24492
25593
  periodEnd: gaTrafficSummaries.periodEnd
24493
- }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
25594
+ }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).get();
24494
25595
  const rows = app.db.select({
24495
25596
  landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
24496
25597
  sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
24497
25598
  organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
24498
25599
  directSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
24499
25600
  users: sql10`SUM(${gaTrafficSnapshots.users})`
24500
- }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
25601
+ }).from(gaTrafficSnapshots).where(and22(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
24501
25602
  const aiReferralRows = app.db.select({
24502
25603
  source: gaAiReferrals.source,
24503
25604
  medium: gaAiReferrals.medium,
@@ -24505,7 +25606,7 @@ async function ga4Routes(app, opts) {
24505
25606
  sourceDimension: gaAiReferrals.sourceDimension,
24506
25607
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
24507
25608
  users: sql10`SUM(${gaAiReferrals.users})`
24508
- }).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
25609
+ }).from(gaAiReferrals).where(and22(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
24509
25610
  const aiReferralLandingPageRows = app.db.select({
24510
25611
  source: gaAiReferrals.source,
24511
25612
  medium: gaAiReferrals.medium,
@@ -24514,7 +25615,7 @@ async function ga4Routes(app, opts) {
24514
25615
  landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
24515
25616
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
24516
25617
  users: sql10`SUM(${gaAiReferrals.users})`
24517
- }).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(
25618
+ }).from(gaAiReferrals).where(and22(...aiConditions)).groupBy(
24518
25619
  gaAiReferrals.source,
24519
25620
  gaAiReferrals.medium,
24520
25621
  gaAiReferrals.trafficClass,
@@ -24530,7 +25631,7 @@ async function ga4Routes(app, opts) {
24530
25631
  channelGroup: gaAiReferrals.channelGroup,
24531
25632
  sessions: gaAiReferrals.sessions,
24532
25633
  users: gaAiReferrals.users
24533
- }).from(gaAiReferrals).where(and21(...aiConditions)).all();
25634
+ }).from(gaAiReferrals).where(and22(...aiConditions)).all();
24534
25635
  const aiReferrals = pickWinningDimension(
24535
25636
  aiReferralRows,
24536
25637
  (r) => `${r.source}\0${r.medium}\0${r.trafficClass}`
@@ -24546,12 +25647,12 @@ async function ga4Routes(app, opts) {
24546
25647
  channelGroup: gaSocialReferrals.channelGroup,
24547
25648
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
24548
25649
  users: sql10`SUM(${gaSocialReferrals.users})`
24549
- }).from(gaSocialReferrals).where(and21(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
25650
+ }).from(gaSocialReferrals).where(and22(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
24550
25651
  const socialTotals = app.db.select({
24551
25652
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
24552
25653
  users: sql10`SUM(${gaSocialReferrals.users})`
24553
- }).from(gaSocialReferrals).where(and21(...socialConditions)).get();
24554
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
25654
+ }).from(gaSocialReferrals).where(and22(...socialConditions)).get();
25655
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
24555
25656
  const total = summaryRow?.totalSessions ?? 0;
24556
25657
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
24557
25658
  const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
@@ -24649,7 +25750,7 @@ async function ga4Routes(app, opts) {
24649
25750
  const project = resolveProject(app.db, request.params.name);
24650
25751
  requireGa4Connection(opts, project.name, project.canonicalDomain);
24651
25752
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
24652
- const conditions = [eq27(gaAiReferrals.projectId, project.id)];
25753
+ const conditions = [eq28(gaAiReferrals.projectId, project.id)];
24653
25754
  if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
24654
25755
  const rows = app.db.select({
24655
25756
  date: gaAiReferrals.date,
@@ -24660,7 +25761,7 @@ async function ga4Routes(app, opts) {
24660
25761
  sourceDimension: gaAiReferrals.sourceDimension,
24661
25762
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
24662
25763
  users: sql10`SUM(${gaAiReferrals.users})`
24663
- }).from(gaAiReferrals).where(and21(...conditions)).groupBy(
25764
+ }).from(gaAiReferrals).where(and22(...conditions)).groupBy(
24664
25765
  gaAiReferrals.date,
24665
25766
  gaAiReferrals.source,
24666
25767
  gaAiReferrals.medium,
@@ -24674,7 +25775,7 @@ async function ga4Routes(app, opts) {
24674
25775
  const project = resolveProject(app.db, request.params.name);
24675
25776
  requireGa4Connection(opts, project.name, project.canonicalDomain);
24676
25777
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
24677
- const conditions = [eq27(gaSocialReferrals.projectId, project.id)];
25778
+ const conditions = [eq28(gaSocialReferrals.projectId, project.id)];
24678
25779
  if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
24679
25780
  const rows = app.db.select({
24680
25781
  date: gaSocialReferrals.date,
@@ -24683,7 +25784,7 @@ async function ga4Routes(app, opts) {
24683
25784
  channelGroup: gaSocialReferrals.channelGroup,
24684
25785
  sessions: gaSocialReferrals.sessions,
24685
25786
  users: gaSocialReferrals.users
24686
- }).from(gaSocialReferrals).where(and21(...conditions)).orderBy(gaSocialReferrals.date).all();
25787
+ }).from(gaSocialReferrals).where(and22(...conditions)).orderBy(gaSocialReferrals.date).all();
24687
25788
  return rows;
24688
25789
  });
24689
25790
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -24696,8 +25797,8 @@ async function ga4Routes(app, opts) {
24696
25797
  d.setDate(d.getDate() - n);
24697
25798
  return fmt(d);
24698
25799
  };
24699
- const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
24700
- eq27(gaSocialReferrals.projectId, project.id),
25800
+ const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and22(
25801
+ eq28(gaSocialReferrals.projectId, project.id),
24701
25802
  sql10`${gaSocialReferrals.date} >= ${from}`,
24702
25803
  sql10`${gaSocialReferrals.date} < ${to}`
24703
25804
  )).get();
@@ -24709,16 +25810,16 @@ async function ga4Routes(app, opts) {
24709
25810
  const sourceCurrent = app.db.select({
24710
25811
  source: gaSocialReferrals.source,
24711
25812
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`
24712
- }).from(gaSocialReferrals).where(and21(
24713
- eq27(gaSocialReferrals.projectId, project.id),
25813
+ }).from(gaSocialReferrals).where(and22(
25814
+ eq28(gaSocialReferrals.projectId, project.id),
24714
25815
  sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
24715
25816
  sql10`${gaSocialReferrals.date} < ${fmt(today)}`
24716
25817
  )).groupBy(gaSocialReferrals.source).all();
24717
25818
  const sourcePrev = app.db.select({
24718
25819
  source: gaSocialReferrals.source,
24719
25820
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`
24720
- }).from(gaSocialReferrals).where(and21(
24721
- eq27(gaSocialReferrals.projectId, project.id),
25821
+ }).from(gaSocialReferrals).where(and22(
25822
+ eq28(gaSocialReferrals.projectId, project.id),
24722
25823
  sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
24723
25824
  sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
24724
25825
  )).groupBy(gaSocialReferrals.source).all();
@@ -24759,16 +25860,16 @@ async function ga4Routes(app, opts) {
24759
25860
  return fmt(d);
24760
25861
  };
24761
25862
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
24762
- const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24763
- const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24764
- const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24765
- const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24766
- eq27(gaAiReferrals.projectId, project.id),
25863
+ const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
25864
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
25865
+ const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and22(eq28(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
25866
+ const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
25867
+ eq28(gaAiReferrals.projectId, project.id),
24767
25868
  sql10`${gaAiReferrals.date} >= ${from}`,
24768
25869
  sql10`${gaAiReferrals.date} < ${to}`,
24769
- eq27(gaAiReferrals.sourceDimension, "session")
25870
+ eq28(gaAiReferrals.sourceDimension, "session")
24770
25871
  )).get();
24771
- const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
25872
+ const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
24772
25873
  const todayStr = fmt(today);
24773
25874
  const buildTrend = (sum) => {
24774
25875
  const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
@@ -24777,17 +25878,17 @@ async function ga4Routes(app, opts) {
24777
25878
  const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
24778
25879
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
24779
25880
  };
24780
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24781
- eq27(gaAiReferrals.projectId, project.id),
25881
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
25882
+ eq28(gaAiReferrals.projectId, project.id),
24782
25883
  sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
24783
25884
  sql10`${gaAiReferrals.date} < ${todayStr}`,
24784
- eq27(gaAiReferrals.sourceDimension, "session")
25885
+ eq28(gaAiReferrals.sourceDimension, "session")
24785
25886
  )).groupBy(gaAiReferrals.source).all();
24786
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24787
- eq27(gaAiReferrals.projectId, project.id),
25887
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and22(
25888
+ eq28(gaAiReferrals.projectId, project.id),
24788
25889
  sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
24789
25890
  sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
24790
- eq27(gaAiReferrals.sourceDimension, "session")
25891
+ eq28(gaAiReferrals.sourceDimension, "session")
24791
25892
  )).groupBy(gaAiReferrals.source).all();
24792
25893
  const findBiggestMover = (current, prev) => {
24793
25894
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
@@ -24803,8 +25904,8 @@ async function ga4Routes(app, opts) {
24803
25904
  }
24804
25905
  return mover;
24805
25906
  };
24806
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
24807
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
25907
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
25908
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and22(eq28(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
24808
25909
  return {
24809
25910
  total: buildTrend(sumTotal),
24810
25911
  organic: buildTrend(sumOrganic),
@@ -24819,14 +25920,14 @@ async function ga4Routes(app, opts) {
24819
25920
  const project = resolveProject(app.db, request.params.name);
24820
25921
  requireGa4Connection(opts, project.name, project.canonicalDomain);
24821
25922
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
24822
- const conditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
25923
+ const conditions = [eq28(gaTrafficSnapshots.projectId, project.id)];
24823
25924
  if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
24824
25925
  const rows = app.db.select({
24825
25926
  date: gaTrafficSnapshots.date,
24826
25927
  sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
24827
25928
  organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
24828
25929
  users: sql10`SUM(${gaTrafficSnapshots.users})`
24829
- }).from(gaTrafficSnapshots).where(and21(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
25930
+ }).from(gaTrafficSnapshots).where(and22(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
24830
25931
  return rows.map((r) => ({
24831
25932
  date: r.date,
24832
25933
  sessions: r.sessions ?? 0,
@@ -24842,7 +25943,7 @@ async function ga4Routes(app, opts) {
24842
25943
  sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
24843
25944
  organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
24844
25945
  users: sql10`SUM(${gaTrafficSnapshots.users})`
24845
- }).from(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
25946
+ }).from(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
24846
25947
  return {
24847
25948
  pages: trafficPages.map((r) => ({
24848
25949
  landingPage: r.landingPage,
@@ -26490,7 +27591,7 @@ async function wordpressRoutes(app, opts) {
26490
27591
 
26491
27592
  // ../api-routes/src/backlinks.ts
26492
27593
  import crypto24 from "crypto";
26493
- import { and as and23, asc as asc4, desc as desc14, eq as eq28, sql as sql11 } from "drizzle-orm";
27594
+ import { and as and24, asc as asc6, desc as desc14, eq as eq29, sql as sql11 } from "drizzle-orm";
26494
27595
 
26495
27596
  // ../integration-commoncrawl/src/constants.ts
26496
27597
  import os2 from "os";
@@ -26893,7 +27994,7 @@ function pruneCachedRelease(release, opts = {}) {
26893
27994
  }
26894
27995
 
26895
27996
  // ../api-routes/src/backlinks-filter.ts
26896
- import { and as and22, ne as ne3, notLike } from "drizzle-orm";
27997
+ import { and as and23, ne as ne3, notLike } from "drizzle-orm";
26897
27998
  var BACKLINK_FILTER_PATTERNS = [
26898
27999
  "*.google.com",
26899
28000
  "*.googleusercontent.com",
@@ -26916,7 +28017,7 @@ function backlinkCrawlerExclusionClause() {
26916
28017
  conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
26917
28018
  }
26918
28019
  }
26919
- const combined = and22(...conditions);
28020
+ const combined = and23(...conditions);
26920
28021
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
26921
28022
  return combined;
26922
28023
  }
@@ -26987,11 +28088,11 @@ function mapRunRow(row) {
26987
28088
  }
26988
28089
  function latestSummaryForProject(db, projectId, source, release) {
26989
28090
  const conditions = [
26990
- eq28(backlinkSummaries.projectId, projectId),
26991
- eq28(backlinkSummaries.source, source)
28091
+ eq29(backlinkSummaries.projectId, projectId),
28092
+ eq29(backlinkSummaries.source, source)
26992
28093
  ];
26993
- if (release) conditions.push(eq28(backlinkSummaries.release, release));
26994
- return db.select().from(backlinkSummaries).where(and23(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
28094
+ if (release) conditions.push(eq29(backlinkSummaries.release, release));
28095
+ return db.select().from(backlinkSummaries).where(and24(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
26995
28096
  }
26996
28097
  function parseExcludeCrawlers(value) {
26997
28098
  if (!value) return false;
@@ -26999,12 +28100,12 @@ function parseExcludeCrawlers(value) {
26999
28100
  return lower === "1" || lower === "true" || lower === "yes";
27000
28101
  }
27001
28102
  function computeFilteredSummary(db, base) {
27002
- const baseDomainCondition = and23(
27003
- eq28(backlinkDomains.projectId, base.projectId),
27004
- eq28(backlinkDomains.source, base.source),
27005
- eq28(backlinkDomains.release, base.release)
28103
+ const baseDomainCondition = and24(
28104
+ eq29(backlinkDomains.projectId, base.projectId),
28105
+ eq29(backlinkDomains.source, base.source),
28106
+ eq29(backlinkDomains.release, base.release)
27006
28107
  );
27007
- const filteredCondition = and23(baseDomainCondition, backlinkCrawlerExclusionClause());
28108
+ const filteredCondition = and24(baseDomainCondition, backlinkCrawlerExclusionClause());
27008
28109
  const unfilteredAgg = db.select({
27009
28110
  count: sql11`count(*)`,
27010
28111
  total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -27034,13 +28135,13 @@ function computeFilteredSummary(db, base) {
27034
28135
  };
27035
28136
  }
27036
28137
  function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
27037
- const summary = db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, projectId), eq28(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
28138
+ const summary = db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, projectId), eq29(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
27038
28139
  let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
27039
28140
  if (summary && excludeCrawlers) {
27040
- const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and23(
27041
- eq28(backlinkDomains.projectId, projectId),
27042
- eq28(backlinkDomains.source, source),
27043
- eq28(backlinkDomains.release, summary.release),
28141
+ const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and24(
28142
+ eq29(backlinkDomains.projectId, projectId),
28143
+ eq29(backlinkDomains.source, source),
28144
+ eq29(backlinkDomains.release, summary.release),
27044
28145
  backlinkCrawlerExclusionClause()
27045
28146
  )).get();
27046
28147
  totalLinkingDomains = Number(filtered?.count ?? 0);
@@ -27055,7 +28156,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
27055
28156
  };
27056
28157
  }
27057
28158
  function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
27058
- const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
28159
+ const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
27059
28160
  const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
27060
28161
  const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
27061
28162
  const sources = [
@@ -27111,7 +28212,7 @@ async function backlinksRoutes(app, opts) {
27111
28212
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
27112
28213
  );
27113
28214
  }
27114
- const existing = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.release, release)).get();
28215
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, release)).get();
27115
28216
  const now = (/* @__PURE__ */ new Date()).toISOString();
27116
28217
  if (existing) {
27117
28218
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -27122,9 +28223,9 @@ async function backlinksRoutes(app, opts) {
27122
28223
  phaseDetail: null,
27123
28224
  error: null,
27124
28225
  updatedAt: now
27125
- }).where(eq28(ccReleaseSyncs.id, existing.id)).run();
28226
+ }).where(eq29(ccReleaseSyncs.id, existing.id)).run();
27126
28227
  opts.onReleaseSyncRequested(existing.id, release);
27127
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, existing.id)).get();
28228
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.id, existing.id)).get();
27128
28229
  return reply.status(200).send(mapSyncRow(refreshed));
27129
28230
  }
27130
28231
  const id = crypto24.randomUUID();
@@ -27136,7 +28237,7 @@ async function backlinksRoutes(app, opts) {
27136
28237
  updatedAt: now
27137
28238
  }).run();
27138
28239
  opts.onReleaseSyncRequested(id, release);
27139
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, id)).get();
28240
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.id, id)).get();
27140
28241
  return reply.status(201).send(mapSyncRow(inserted));
27141
28242
  });
27142
28243
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
@@ -27194,7 +28295,7 @@ async function backlinksRoutes(app, opts) {
27194
28295
  createdAt: now
27195
28296
  }).run();
27196
28297
  opts.onBacklinkExtractRequested(runId, project.id, release);
27197
- const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
28298
+ const run = app.db.select().from(runs).where(eq29(runs.id, runId)).get();
27198
28299
  return reply.status(201).send(mapRunRow(run));
27199
28300
  });
27200
28301
  app.get(
@@ -27220,12 +28321,12 @@ async function backlinksRoutes(app, opts) {
27220
28321
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
27221
28322
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
27222
28323
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
27223
- const baseDomainCondition = and23(
27224
- eq28(backlinkDomains.projectId, project.id),
27225
- eq28(backlinkDomains.source, source),
27226
- eq28(backlinkDomains.release, targetRelease)
28324
+ const baseDomainCondition = and24(
28325
+ eq29(backlinkDomains.projectId, project.id),
28326
+ eq29(backlinkDomains.source, source),
28327
+ eq29(backlinkDomains.release, targetRelease)
27227
28328
  );
27228
- const domainCondition = excludeCrawlers ? and23(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
28329
+ const domainCondition = excludeCrawlers ? and24(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
27229
28330
  const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
27230
28331
  const rows = app.db.select({
27231
28332
  linkingDomain: backlinkDomains.linkingDomain,
@@ -27249,7 +28350,7 @@ async function backlinksRoutes(app, opts) {
27249
28350
  async (request, reply) => {
27250
28351
  const project = resolveProject(app.db, request.params.name);
27251
28352
  const source = parseSourceParam(request.query.source);
27252
- const rows = app.db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, project.id), eq28(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
28353
+ const rows = app.db.select().from(backlinkSummaries).where(and24(eq29(backlinkSummaries.projectId, project.id), eq29(backlinkSummaries.source, source))).orderBy(asc6(backlinkSummaries.queriedAt)).all();
27253
28354
  const response = rows.map((r) => ({
27254
28355
  release: r.release,
27255
28356
  totalLinkingDomains: r.totalLinkingDomains,
@@ -27296,7 +28397,7 @@ async function backlinksRoutes(app, opts) {
27296
28397
  createdAt: now
27297
28398
  }).run();
27298
28399
  opts.onBingBacklinkSyncRequested(runId, project.id);
27299
- const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
28400
+ const run = app.db.select().from(runs).where(eq29(runs.id, runId)).get();
27300
28401
  return reply.status(201).send(mapRunRow(run));
27301
28402
  }
27302
28403
  );
@@ -27305,7 +28406,7 @@ async function backlinksRoutes(app, opts) {
27305
28406
  // ../api-routes/src/traffic.ts
27306
28407
  import crypto26 from "crypto";
27307
28408
  import { Agent as UndiciAgent } from "undici";
27308
- import { and as and24, desc as desc15, eq as eq29, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
28409
+ import { and as and25, desc as desc15, eq as eq30, gte as gte6, lte as lte4, sql as sql12 } from "drizzle-orm";
27309
28410
 
27310
28411
  // ../integration-cloud-run/src/auth.ts
27311
28412
  import crypto25 from "crypto";
@@ -31172,8 +32273,8 @@ async function runBackfillTask(options) {
31172
32273
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
31173
32274
  try {
31174
32275
  app.db.transaction((tx) => {
31175
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
31176
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
32276
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq30(runs.id, runId)).run();
32277
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq30(trafficSources.id, sourceRow.id)).run();
31177
32278
  });
31178
32279
  } catch {
31179
32280
  }
@@ -31188,7 +32289,7 @@ async function runBackfillTask(options) {
31188
32289
  if (allEvents.length === 0) {
31189
32290
  const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
31190
32291
  try {
31191
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq29(runs.id, runId)).run();
32292
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq30(runs.id, runId)).run();
31192
32293
  } catch {
31193
32294
  }
31194
32295
  return;
@@ -31210,31 +32311,31 @@ async function runBackfillTask(options) {
31210
32311
  try {
31211
32312
  app.db.transaction((tx) => {
31212
32313
  tx.delete(crawlerEventsHourly).where(
31213
- and24(
31214
- eq29(crawlerEventsHourly.sourceId, sourceRow.id),
31215
- gte4(crawlerEventsHourly.tsHour, windowStartIso),
31216
- lte3(crawlerEventsHourly.tsHour, windowEndIso)
32314
+ and25(
32315
+ eq30(crawlerEventsHourly.sourceId, sourceRow.id),
32316
+ gte6(crawlerEventsHourly.tsHour, windowStartIso),
32317
+ lte4(crawlerEventsHourly.tsHour, windowEndIso)
31217
32318
  )
31218
32319
  ).run();
31219
32320
  tx.delete(aiUserFetchEventsHourly).where(
31220
- and24(
31221
- eq29(aiUserFetchEventsHourly.sourceId, sourceRow.id),
31222
- gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
31223
- lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
32321
+ and25(
32322
+ eq30(aiUserFetchEventsHourly.sourceId, sourceRow.id),
32323
+ gte6(aiUserFetchEventsHourly.tsHour, windowStartIso),
32324
+ lte4(aiUserFetchEventsHourly.tsHour, windowEndIso)
31224
32325
  )
31225
32326
  ).run();
31226
32327
  tx.delete(aiReferralEventsHourly).where(
31227
- and24(
31228
- eq29(aiReferralEventsHourly.sourceId, sourceRow.id),
31229
- gte4(aiReferralEventsHourly.tsHour, windowStartIso),
31230
- lte3(aiReferralEventsHourly.tsHour, windowEndIso)
32328
+ and25(
32329
+ eq30(aiReferralEventsHourly.sourceId, sourceRow.id),
32330
+ gte6(aiReferralEventsHourly.tsHour, windowStartIso),
32331
+ lte4(aiReferralEventsHourly.tsHour, windowEndIso)
31231
32332
  )
31232
32333
  ).run();
31233
32334
  tx.delete(rawEventSamples).where(
31234
- and24(
31235
- eq29(rawEventSamples.sourceId, sourceRow.id),
31236
- gte4(rawEventSamples.ts, windowStartIso),
31237
- lte3(rawEventSamples.ts, windowEndIso)
32335
+ and25(
32336
+ eq30(rawEventSamples.sourceId, sourceRow.id),
32337
+ gte6(rawEventSamples.ts, windowStartIso),
32338
+ lte4(rawEventSamples.ts, windowEndIso)
31238
32339
  )
31239
32340
  ).run();
31240
32341
  for (const bucket of report.crawlerEventsHourly) {
@@ -31323,8 +32424,8 @@ async function runBackfillTask(options) {
31323
32424
  lastError: null,
31324
32425
  lastEventIds: newRingBuffer,
31325
32426
  updatedAt: finishedAt
31326
- }).where(eq29(trafficSources.id, sourceRow.id)).run();
31327
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
32427
+ }).where(eq30(trafficSources.id, sourceRow.id)).run();
32428
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq30(runs.id, runId)).run();
31328
32429
  });
31329
32430
  } catch (e) {
31330
32431
  markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -31406,7 +32507,7 @@ async function trafficRoutes(app, opts) {
31406
32507
  createdAt: existing?.createdAt ?? now,
31407
32508
  updatedAt: now
31408
32509
  });
31409
- const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
32510
+ const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
31410
32511
  const config = {
31411
32512
  gcpProjectId,
31412
32513
  serviceName: serviceName ?? null,
@@ -31422,8 +32523,8 @@ async function trafficRoutes(app, opts) {
31422
32523
  lastError: null,
31423
32524
  configJson: config,
31424
32525
  updatedAt: now
31425
- }).where(eq29(trafficSources.id, activeSource.id)).run();
31426
- sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
32526
+ }).where(eq30(trafficSources.id, activeSource.id)).run();
32527
+ sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
31427
32528
  } else {
31428
32529
  const newId = crypto26.randomUUID();
31429
32530
  app.db.insert(trafficSources).values({
@@ -31440,7 +32541,7 @@ async function trafficRoutes(app, opts) {
31440
32541
  createdAt: now,
31441
32542
  updatedAt: now
31442
32543
  }).run();
31443
- sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
32544
+ sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
31444
32545
  }
31445
32546
  writeAuditLog(app.db, {
31446
32547
  projectId: project.id,
@@ -31492,7 +32593,7 @@ async function trafficRoutes(app, opts) {
31492
32593
  createdAt: existing?.createdAt ?? now,
31493
32594
  updatedAt: now
31494
32595
  });
31495
- const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
32596
+ const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
31496
32597
  const config = { baseUrl, username };
31497
32598
  const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
31498
32599
  let sourceRow;
@@ -31503,8 +32604,8 @@ async function trafficRoutes(app, opts) {
31503
32604
  lastError: null,
31504
32605
  configJson: config,
31505
32606
  updatedAt: now
31506
- }).where(eq29(trafficSources.id, activeSource.id)).run();
31507
- sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
32607
+ }).where(eq30(trafficSources.id, activeSource.id)).run();
32608
+ sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
31508
32609
  } else {
31509
32610
  const newId = crypto26.randomUUID();
31510
32611
  app.db.insert(trafficSources).values({
@@ -31521,7 +32622,7 @@ async function trafficRoutes(app, opts) {
31521
32622
  createdAt: now,
31522
32623
  updatedAt: now
31523
32624
  }).run();
31524
- sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
32625
+ sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
31525
32626
  }
31526
32627
  writeAuditLog(app.db, {
31527
32628
  projectId: project.id,
@@ -31575,7 +32676,7 @@ async function trafficRoutes(app, opts) {
31575
32676
  createdAt: existing?.createdAt ?? now,
31576
32677
  updatedAt: now
31577
32678
  });
31578
- const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
32679
+ const activeSource = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
31579
32680
  const config = { projectId, teamId, environment };
31580
32681
  const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
31581
32682
  const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
@@ -31587,8 +32688,8 @@ async function trafficRoutes(app, opts) {
31587
32688
  lastError: null,
31588
32689
  configJson: config,
31589
32690
  updatedAt: now
31590
- }).where(eq29(trafficSources.id, activeSource.id)).run();
31591
- row = tx.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
32691
+ }).where(eq30(trafficSources.id, activeSource.id)).run();
32692
+ row = tx.select().from(trafficSources).where(eq30(trafficSources.id, activeSource.id)).get();
31592
32693
  } else {
31593
32694
  const newId = crypto26.randomUUID();
31594
32695
  tx.insert(trafficSources).values({
@@ -31613,12 +32714,12 @@ async function trafficRoutes(app, opts) {
31613
32714
  createdAt: now,
31614
32715
  updatedAt: now
31615
32716
  }).run();
31616
- row = tx.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
32717
+ row = tx.select().from(trafficSources).where(eq30(trafficSources.id, newId)).get();
31617
32718
  }
31618
32719
  const existingSchedule = tx.select().from(schedules).where(
31619
- and24(
31620
- eq29(schedules.projectId, project.id),
31621
- eq29(schedules.kind, SchedulableRunKinds["traffic-sync"])
32720
+ and25(
32721
+ eq30(schedules.projectId, project.id),
32722
+ eq30(schedules.kind, SchedulableRunKinds["traffic-sync"])
31622
32723
  )
31623
32724
  ).get();
31624
32725
  let created = false;
@@ -31667,7 +32768,7 @@ async function trafficRoutes(app, opts) {
31667
32768
  });
31668
32769
  app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
31669
32770
  const project = resolveProject(app.db, request.params.name);
31670
- const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
32771
+ const sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
31671
32772
  if (!sourceRow || sourceRow.projectId !== project.id) {
31672
32773
  throw notFound("Traffic source", request.params.id);
31673
32774
  }
@@ -31693,8 +32794,8 @@ async function trafficRoutes(app, opts) {
31693
32794
  const markFailed = (msg, errorCode) => {
31694
32795
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
31695
32796
  app.db.transaction((tx) => {
31696
- tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
31697
- tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
32797
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq30(runs.id, runId)).run();
32798
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq30(trafficSources.id, sourceRow.id)).run();
31698
32799
  });
31699
32800
  try {
31700
32801
  opts.onTrafficSynced?.({
@@ -31775,7 +32876,7 @@ async function trafficRoutes(app, opts) {
31775
32876
  }
31776
32877
  const credential = credentialStore.getConnection(project.name);
31777
32878
  if (!credential) {
31778
- app.db.delete(runs).where(eq29(runs.id, runId)).run();
32879
+ app.db.delete(runs).where(eq30(runs.id, runId)).run();
31779
32880
  throw validationError(
31780
32881
  `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
31781
32882
  );
@@ -31824,12 +32925,12 @@ async function trafficRoutes(app, opts) {
31824
32925
  auditAction = "traffic.vercel.synced";
31825
32926
  const credentialStore = opts.vercelTrafficCredentialStore;
31826
32927
  if (!credentialStore) {
31827
- app.db.delete(runs).where(eq29(runs.id, runId)).run();
32928
+ app.db.delete(runs).where(eq30(runs.id, runId)).run();
31828
32929
  throw validationError("Vercel traffic credential storage is not configured for this deployment");
31829
32930
  }
31830
32931
  const credential = credentialStore.getConnection(project.name);
31831
32932
  if (!credential) {
31832
- app.db.delete(runs).where(eq29(runs.id, runId)).run();
32933
+ app.db.delete(runs).where(eq30(runs.id, runId)).run();
31833
32934
  throw validationError(
31834
32935
  `No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
31835
32936
  );
@@ -31929,7 +33030,7 @@ async function trafficRoutes(app, opts) {
31929
33030
  let aiReferralHitsCount = 0;
31930
33031
  let unknownHitsCount = 0;
31931
33032
  app.db.transaction((tx) => {
31932
- const latestRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
33033
+ const latestRow = tx.select().from(trafficSources).where(eq30(trafficSources.id, sourceRow.id)).get();
31933
33034
  const previousIds = latestRow.lastEventIds ?? [];
31934
33035
  const seenEventIds = new Set(previousIds);
31935
33036
  const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
@@ -32104,8 +33205,8 @@ async function trafficRoutes(app, opts) {
32104
33205
  if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
32105
33206
  sourceUpdate.lastCursor = nextCursor ?? null;
32106
33207
  }
32107
- tx.update(trafficSources).set(sourceUpdate).where(eq29(trafficSources.id, sourceRow.id)).run();
32108
- tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
33208
+ tx.update(trafficSources).set(sourceUpdate).where(eq30(trafficSources.id, sourceRow.id)).run();
33209
+ tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq30(runs.id, runId)).run();
32109
33210
  });
32110
33211
  writeAuditLog(app.db, {
32111
33212
  projectId: project.id,
@@ -32157,7 +33258,7 @@ async function trafficRoutes(app, opts) {
32157
33258
  });
32158
33259
  app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
32159
33260
  const project = resolveProject(app.db, request.params.name);
32160
- const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
33261
+ const sourceRow = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
32161
33262
  if (!sourceRow || sourceRow.projectId !== project.id) {
32162
33263
  throw notFound("Traffic source", request.params.id);
32163
33264
  }
@@ -32345,9 +33446,9 @@ async function trafficRoutes(app, opts) {
32345
33446
  pathNormalized: crawlerEventsHourly.pathNormalized,
32346
33447
  hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
32347
33448
  }).from(crawlerEventsHourly).where(
32348
- and24(
32349
- eq29(crawlerEventsHourly.sourceId, row.id),
32350
- gte4(crawlerEventsHourly.tsHour, since)
33449
+ and25(
33450
+ eq30(crawlerEventsHourly.sourceId, row.id),
33451
+ gte6(crawlerEventsHourly.tsHour, since)
32351
33452
  )
32352
33453
  ).groupBy(crawlerEventsHourly.pathNormalized).all();
32353
33454
  const crawlerSegments = segmentCrawlerHits(
@@ -32355,28 +33456,28 @@ async function trafficRoutes(app, opts) {
32355
33456
  );
32356
33457
  const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
32357
33458
  const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
32358
- and24(
32359
- eq29(aiUserFetchEventsHourly.sourceId, row.id),
32360
- gte4(aiUserFetchEventsHourly.tsHour, since)
33459
+ and25(
33460
+ eq30(aiUserFetchEventsHourly.sourceId, row.id),
33461
+ gte6(aiUserFetchEventsHourly.tsHour, since)
32361
33462
  )
32362
33463
  ).get();
32363
33464
  const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
32364
- and24(
32365
- eq29(aiReferralEventsHourly.sourceId, row.id),
32366
- gte4(aiReferralEventsHourly.tsHour, since)
33465
+ and25(
33466
+ eq30(aiReferralEventsHourly.sourceId, row.id),
33467
+ gte6(aiReferralEventsHourly.tsHour, since)
32367
33468
  )
32368
33469
  ).get();
32369
33470
  const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
32370
- and24(
32371
- eq29(rawEventSamples.sourceId, row.id),
32372
- gte4(rawEventSamples.ts, since)
33471
+ and25(
33472
+ eq30(rawEventSamples.sourceId, row.id),
33473
+ gte6(rawEventSamples.ts, since)
32373
33474
  )
32374
33475
  ).get();
32375
33476
  const latestRun = app.db.select().from(runs).where(
32376
- and24(
32377
- eq29(runs.projectId, projectId),
32378
- eq29(runs.kind, RunKinds["traffic-sync"]),
32379
- eq29(runs.sourceId, row.id)
33477
+ and25(
33478
+ eq30(runs.projectId, projectId),
33479
+ eq30(runs.kind, RunKinds["traffic-sync"]),
33480
+ eq30(runs.sourceId, row.id)
32380
33481
  )
32381
33482
  ).orderBy(desc15(runs.startedAt)).limit(1).get();
32382
33483
  return {
@@ -32407,7 +33508,7 @@ async function trafficRoutes(app, opts) {
32407
33508
  "`advanceToNow` must be `true`. There is no implicit reset."
32408
33509
  );
32409
33510
  }
32410
- const sourceRow = app.db.select().from(trafficSources).where(and24(eq29(trafficSources.projectId, project.id), eq29(trafficSources.id, request.params.id))).get();
33511
+ const sourceRow = app.db.select().from(trafficSources).where(and25(eq30(trafficSources.projectId, project.id), eq30(trafficSources.id, request.params.id))).get();
32411
33512
  if (!sourceRow) {
32412
33513
  throw notFound("traffic source", request.params.id);
32413
33514
  }
@@ -32424,7 +33525,7 @@ async function trafficRoutes(app, opts) {
32424
33525
  status: TrafficSourceStatuses.connected,
32425
33526
  lastError: null,
32426
33527
  updatedAt: now
32427
- }).where(eq29(trafficSources.id, sourceRow.id)).run();
33528
+ }).where(eq30(trafficSources.id, sourceRow.id)).run();
32428
33529
  writeAuditLog(tx, auditFromRequest(request, {
32429
33530
  projectId: project.id,
32430
33531
  actor: "api",
@@ -32432,20 +33533,20 @@ async function trafficRoutes(app, opts) {
32432
33533
  entityType: "traffic_source",
32433
33534
  entityId: sourceRow.id
32434
33535
  }));
32435
- updatedRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
33536
+ updatedRow = tx.select().from(trafficSources).where(eq30(trafficSources.id, sourceRow.id)).get();
32436
33537
  });
32437
33538
  return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
32438
33539
  });
32439
33540
  app.get("/projects/:name/traffic/sources", async (request) => {
32440
33541
  const project = resolveProject(app.db, request.params.name);
32441
- const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
33542
+ const rows = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
32442
33543
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
32443
33544
  const response = { sources };
32444
33545
  return response;
32445
33546
  });
32446
33547
  app.get("/projects/:name/traffic/status", async (request) => {
32447
33548
  const project = resolveProject(app.db, request.params.name);
32448
- const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
33549
+ const rows = app.db.select().from(trafficSources).where(eq30(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
32449
33550
  const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
32450
33551
  const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
32451
33552
  const response = { sources };
@@ -32455,7 +33556,7 @@ async function trafficRoutes(app, opts) {
32455
33556
  "/projects/:name/traffic/sources/:id",
32456
33557
  async (request) => {
32457
33558
  const project = resolveProject(app.db, request.params.name);
32458
- const row = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
33559
+ const row = app.db.select().from(trafficSources).where(eq30(trafficSources.id, request.params.id)).get();
32459
33560
  if (!row || row.projectId !== project.id) {
32460
33561
  throw notFound("Traffic source", request.params.id);
32461
33562
  }
@@ -32507,12 +33608,12 @@ async function trafficRoutes(app, opts) {
32507
33608
  let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
32508
33609
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
32509
33610
  const crawlerFilters = [
32510
- eq29(crawlerEventsHourly.projectId, project.id),
32511
- gte4(crawlerEventsHourly.tsHour, sinceIso),
32512
- lte3(crawlerEventsHourly.tsHour, untilIso)
33611
+ eq30(crawlerEventsHourly.projectId, project.id),
33612
+ gte6(crawlerEventsHourly.tsHour, sinceIso),
33613
+ lte4(crawlerEventsHourly.tsHour, untilIso)
32513
33614
  ];
32514
- if (sourceIdParam) crawlerFilters.push(eq29(crawlerEventsHourly.sourceId, sourceIdParam));
32515
- const crawlerWhere = and24(...crawlerFilters);
33615
+ if (sourceIdParam) crawlerFilters.push(eq30(crawlerEventsHourly.sourceId, sourceIdParam));
33616
+ const crawlerWhere = and25(...crawlerFilters);
32516
33617
  const pathTotals = app.db.select({
32517
33618
  pathNormalized: crawlerEventsHourly.pathNormalized,
32518
33619
  hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
@@ -32539,12 +33640,12 @@ async function trafficRoutes(app, opts) {
32539
33640
  }
32540
33641
  if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
32541
33642
  const userFetchFilters = [
32542
- eq29(aiUserFetchEventsHourly.projectId, project.id),
32543
- gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
32544
- lte3(aiUserFetchEventsHourly.tsHour, untilIso)
33643
+ eq30(aiUserFetchEventsHourly.projectId, project.id),
33644
+ gte6(aiUserFetchEventsHourly.tsHour, sinceIso),
33645
+ lte4(aiUserFetchEventsHourly.tsHour, untilIso)
32545
33646
  ];
32546
- if (sourceIdParam) userFetchFilters.push(eq29(aiUserFetchEventsHourly.sourceId, sourceIdParam));
32547
- const userFetchWhere = and24(...userFetchFilters);
33647
+ if (sourceIdParam) userFetchFilters.push(eq30(aiUserFetchEventsHourly.sourceId, sourceIdParam));
33648
+ const userFetchWhere = and25(...userFetchFilters);
32548
33649
  const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
32549
33650
  aiUserFetchTotal = Number(total?.total ?? 0);
32550
33651
  const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
@@ -32564,12 +33665,12 @@ async function trafficRoutes(app, opts) {
32564
33665
  }
32565
33666
  if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
32566
33667
  const aiFilters = [
32567
- eq29(aiReferralEventsHourly.projectId, project.id),
32568
- gte4(aiReferralEventsHourly.tsHour, sinceIso),
32569
- lte3(aiReferralEventsHourly.tsHour, untilIso)
33668
+ eq30(aiReferralEventsHourly.projectId, project.id),
33669
+ gte6(aiReferralEventsHourly.tsHour, sinceIso),
33670
+ lte4(aiReferralEventsHourly.tsHour, untilIso)
32570
33671
  ];
32571
- if (sourceIdParam) aiFilters.push(eq29(aiReferralEventsHourly.sourceId, sourceIdParam));
32572
- const aiWhere = and24(...aiFilters);
33672
+ if (sourceIdParam) aiFilters.push(eq30(aiReferralEventsHourly.sourceId, sourceIdParam));
33673
+ const aiWhere = and25(...aiFilters);
32573
33674
  const total = app.db.select({
32574
33675
  total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
32575
33676
  paid: sql12`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
@@ -32791,7 +33892,7 @@ function readInstalledManifest(skillDir) {
32791
33892
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
32792
33893
 
32793
33894
  // ../api-routes/src/doctor/checks/backlinks.ts
32794
- import { and as and25, eq as eq30 } from "drizzle-orm";
33895
+ import { and as and26, eq as eq31 } from "drizzle-orm";
32795
33896
  function skippedNoProject() {
32796
33897
  return {
32797
33898
  status: CheckStatuses.skipped,
@@ -32807,8 +33908,8 @@ var BACKLINKS_CHECKS = [
32807
33908
  title: "Backlinks source connected",
32808
33909
  run: (ctx) => {
32809
33910
  if (!ctx.project) return skippedNoProject();
32810
- const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq30(projects.id, ctx.project.id)).get();
32811
- const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
33911
+ const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq31(projects.id, ctx.project.id)).get();
33912
+ const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq31(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
32812
33913
  const ccConnected = projectRow?.autoExtract === true && !!readySync;
32813
33914
  const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
32814
33915
  const connected = [];
@@ -32823,9 +33924,9 @@ var BACKLINKS_CHECKS = [
32823
33924
  details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
32824
33925
  };
32825
33926
  }
32826
- const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and25(
32827
- eq30(backlinkSummaries.projectId, ctx.project.id),
32828
- eq30(backlinkSummaries.source, BacklinkSources.commoncrawl)
33927
+ const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and26(
33928
+ eq31(backlinkSummaries.projectId, ctx.project.id),
33929
+ eq31(backlinkSummaries.source, BacklinkSources.commoncrawl)
32829
33930
  )).limit(1).get() : false;
32830
33931
  return {
32831
33932
  status: CheckStatuses.ok,
@@ -32985,7 +34086,7 @@ var BING_AUTH_CHECKS = [
32985
34086
  ];
32986
34087
 
32987
34088
  // ../api-routes/src/doctor/checks/content.ts
32988
- import { eq as eq31 } from "drizzle-orm";
34089
+ import { eq as eq32 } from "drizzle-orm";
32989
34090
  var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
32990
34091
  var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
32991
34092
  function skippedNoProject2() {
@@ -32998,7 +34099,7 @@ function skippedNoProject2() {
32998
34099
  }
32999
34100
  function loadProject(ctx) {
33000
34101
  if (!ctx.project) return null;
33001
- return ctx.db.select().from(projects).where(eq31(projects.id, ctx.project.id)).get() ?? null;
34102
+ return ctx.db.select().from(projects).where(eq32(projects.id, ctx.project.id)).get() ?? null;
33002
34103
  }
33003
34104
  function percent(value) {
33004
34105
  return Math.round(value * 100);
@@ -33090,7 +34191,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
33090
34191
  );
33091
34192
 
33092
34193
  // ../api-routes/src/doctor/checks/ads.ts
33093
- import { eq as eq32 } from "drizzle-orm";
34194
+ import { eq as eq33 } from "drizzle-orm";
33094
34195
  var RECENT_SYNC_WARN_DAYS = 7;
33095
34196
  var RECENT_SYNC_FAIL_DAYS = 30;
33096
34197
  var adsConnectionCheck = {
@@ -33107,7 +34208,7 @@ var adsConnectionCheck = {
33107
34208
  remediation: null
33108
34209
  };
33109
34210
  }
33110
- const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
34211
+ const row = ctx.db.select().from(adsConnections).where(eq33(adsConnections.projectId, ctx.project.id)).get();
33111
34212
  if (!row) {
33112
34213
  return {
33113
34214
  status: CheckStatuses.skipped,
@@ -33157,7 +34258,7 @@ var adsRecentSyncCheck = {
33157
34258
  remediation: null
33158
34259
  };
33159
34260
  }
33160
- const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
34261
+ const row = ctx.db.select().from(adsConnections).where(eq33(adsConnections.projectId, ctx.project.id)).get();
33161
34262
  if (!row) {
33162
34263
  return {
33163
34264
  status: CheckStatuses.skipped,
@@ -33348,7 +34449,7 @@ var ga4ConnectionCheck = {
33348
34449
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
33349
34450
 
33350
34451
  // ../api-routes/src/doctor/checks/gbp-auth.ts
33351
- import { and as and26, eq as eq33 } from "drizzle-orm";
34452
+ import { and as and27, eq as eq34 } from "drizzle-orm";
33352
34453
  var RECENT_SYNC_WARN_DAYS2 = 7;
33353
34454
  var RECENT_SYNC_FAIL_DAYS2 = 30;
33354
34455
  function skippedNoProject3() {
@@ -33581,7 +34682,7 @@ var recentSyncCheck = {
33581
34682
  title: "GBP recent sync",
33582
34683
  run: (ctx) => {
33583
34684
  if (!ctx.project) return skippedNoProject3();
33584
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(eq33(gbpLocations.projectId, ctx.project.id), eq33(gbpLocations.selected, true))).all();
34685
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and27(eq34(gbpLocations.projectId, ctx.project.id), eq34(gbpLocations.selected, true))).all();
33585
34686
  if (selected.length === 0) {
33586
34687
  return {
33587
34688
  status: CheckStatuses.skipped,
@@ -33641,7 +34742,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
33641
34742
  );
33642
34743
 
33643
34744
  // ../api-routes/src/doctor/checks/places.ts
33644
- import { eq as eq34 } from "drizzle-orm";
34745
+ import { eq as eq35 } from "drizzle-orm";
33645
34746
  var apiKeyCheck = {
33646
34747
  id: "gbp.places.api-key",
33647
34748
  category: CheckCategories.auth,
@@ -33686,7 +34787,7 @@ var apiKeyCheck = {
33686
34787
  details: { tier: cfg.tier }
33687
34788
  };
33688
34789
  }
33689
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq34(gbpLocations.projectId, ctx.project.id)).all();
34790
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq35(gbpLocations.projectId, ctx.project.id)).all();
33690
34791
  const selected = rows.filter((r) => r.selected);
33691
34792
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
33692
34793
  const details = {
@@ -34183,7 +35284,7 @@ var RUNTIME_STATE_CHECKS = [
34183
35284
  ];
34184
35285
 
34185
35286
  // ../api-routes/src/doctor/checks/traffic-source.ts
34186
- import { and as and27, eq as eq35, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
35287
+ import { and as and28, eq as eq36, gte as gte7, ne as ne4, sql as sql13 } from "drizzle-orm";
34187
35288
  var RECENT_DATA_WARN_DAYS = 7;
34188
35289
  var RECENT_DATA_FAIL_DAYS = 30;
34189
35290
  function skippedNoProject5() {
@@ -34197,8 +35298,8 @@ function skippedNoProject5() {
34197
35298
  function loadProbes(ctx) {
34198
35299
  if (!ctx.project) return [];
34199
35300
  const rows = ctx.db.select().from(trafficSources).where(
34200
- and27(
34201
- eq35(trafficSources.projectId, ctx.project.id),
35301
+ and28(
35302
+ eq36(trafficSources.projectId, ctx.project.id),
34202
35303
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
34203
35304
  )
34204
35305
  ).all();
@@ -34278,17 +35379,17 @@ var recentDataCheck = {
34278
35379
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
34279
35380
  const recentCrawlers = Number(
34280
35381
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
34281
- and27(
34282
- eq35(crawlerEventsHourly.projectId, ctx.project.id),
34283
- gte5(crawlerEventsHourly.tsHour, warnCutoff)
35382
+ and28(
35383
+ eq36(crawlerEventsHourly.projectId, ctx.project.id),
35384
+ gte7(crawlerEventsHourly.tsHour, warnCutoff)
34284
35385
  )
34285
35386
  ).get()?.total ?? 0
34286
35387
  );
34287
35388
  const recentReferrals = Number(
34288
35389
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
34289
- and27(
34290
- eq35(aiReferralEventsHourly.projectId, ctx.project.id),
34291
- gte5(aiReferralEventsHourly.tsHour, warnCutoff)
35390
+ and28(
35391
+ eq36(aiReferralEventsHourly.projectId, ctx.project.id),
35392
+ gte7(aiReferralEventsHourly.tsHour, warnCutoff)
34292
35393
  )
34293
35394
  ).get()?.total ?? 0
34294
35395
  );
@@ -34302,17 +35403,17 @@ var recentDataCheck = {
34302
35403
  }
34303
35404
  const olderCrawlers = Number(
34304
35405
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
34305
- and27(
34306
- eq35(crawlerEventsHourly.projectId, ctx.project.id),
34307
- gte5(crawlerEventsHourly.tsHour, failCutoff)
35406
+ and28(
35407
+ eq36(crawlerEventsHourly.projectId, ctx.project.id),
35408
+ gte7(crawlerEventsHourly.tsHour, failCutoff)
34308
35409
  )
34309
35410
  ).get()?.total ?? 0
34310
35411
  );
34311
35412
  const olderReferrals = Number(
34312
35413
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
34313
- and27(
34314
- eq35(aiReferralEventsHourly.projectId, ctx.project.id),
34315
- gte5(aiReferralEventsHourly.tsHour, failCutoff)
35414
+ and28(
35415
+ eq36(aiReferralEventsHourly.projectId, ctx.project.id),
35416
+ gte7(aiReferralEventsHourly.tsHour, failCutoff)
34316
35417
  )
34317
35418
  ).get()?.total ?? 0
34318
35419
  );
@@ -34727,7 +35828,7 @@ async function doctorRoutes(app, opts) {
34727
35828
 
34728
35829
  // ../api-routes/src/discovery/routes.ts
34729
35830
  import crypto28 from "crypto";
34730
- import { and as and28, desc as desc16, eq as eq36, gte as gte6, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
35831
+ import { and as and29, desc as desc16, eq as eq37, gte as gte8, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
34731
35832
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
34732
35833
  async function discoveryRoutes(app, opts) {
34733
35834
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -34761,13 +35862,13 @@ async function discoveryRoutes(app, opts) {
34761
35862
  const now = (/* @__PURE__ */ new Date()).toISOString();
34762
35863
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
34763
35864
  const decision = app.db.transaction((tx) => {
34764
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and28(
34765
- eq36(discoverySessions.projectId, project.id),
34766
- eq36(discoverySessions.icpDescription, icpDescription),
35865
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and29(
35866
+ eq37(discoverySessions.projectId, project.id),
35867
+ eq37(discoverySessions.icpDescription, icpDescription),
34767
35868
  // Buyer is part of session identity: it changes the seed prompt's
34768
35869
  // semantics, so a request with a different (or no) buyer must start
34769
35870
  // its own session, never adopt another buyer's probes.
34770
- parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) : eq36(discoverySessions.buyerDescription, parsed.data.buyerDescription),
35871
+ parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) : eq37(discoverySessions.buyerDescription, parsed.data.buyerDescription),
34771
35872
  // Locations are identity too: a different service-area subset seeds
34772
35873
  // and probes a different geo, so it must never reuse another geo's
34773
35874
  // session. resolveLocations is deterministic (project-config order),
@@ -34777,17 +35878,17 @@ async function discoveryRoutes(app, opts) {
34777
35878
  // locations a NULL row's subset is unknowable, so it conservatively
34778
35879
  // never reuses — a one-time, bounded (2h window) non-reuse after
34779
35880
  // upgrade, never a wrong reuse.
34780
- locations.length === 0 ? or5(isNull(discoverySessions.locations), eq36(discoverySessions.locations, locations)) : eq36(discoverySessions.locations, locations),
35881
+ locations.length === 0 ? or5(isNull(discoverySessions.locations), eq37(discoverySessions.locations, locations)) : eq37(discoverySessions.locations, locations),
34781
35882
  // Seed provider set is identity: a different phrasing distribution
34782
35883
  // must never reuse another set's session. Null (= Gemini-only
34783
35884
  // default, including explicit ['gemini']) matches legacy rows.
34784
- seedProviders == null ? isNull(discoverySessions.seedProviders) : eq36(discoverySessions.seedProviders, seedProviders),
35885
+ seedProviders == null ? isNull(discoverySessions.seedProviders) : eq37(discoverySessions.seedProviders, seedProviders),
34785
35886
  inArray13(discoverySessions.status, [
34786
35887
  DiscoverySessionStatuses.queued,
34787
35888
  DiscoverySessionStatuses.seeding,
34788
35889
  DiscoverySessionStatuses.probing
34789
35890
  ]),
34790
- gte6(discoverySessions.createdAt, ageFloorIso)
35891
+ gte8(discoverySessions.createdAt, ageFloorIso)
34791
35892
  )).orderBy(desc16(discoverySessions.createdAt)).get();
34792
35893
  if (existing && existing.runId) {
34793
35894
  return { reused: true, sessionId: existing.id, runId: existing.runId };
@@ -34857,7 +35958,7 @@ async function discoveryRoutes(app, opts) {
34857
35958
  const project = resolveProject(app.db, request.params.name);
34858
35959
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
34859
35960
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
34860
- const rows = app.db.select().from(discoverySessions).where(eq36(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
35961
+ const rows = app.db.select().from(discoverySessions).where(eq37(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
34861
35962
  return reply.send(rows.map(serializeSession));
34862
35963
  }
34863
35964
  );
@@ -34865,11 +35966,11 @@ async function discoveryRoutes(app, opts) {
34865
35966
  "/projects/:name/discover/sessions/:id",
34866
35967
  async (request, reply) => {
34867
35968
  const project = resolveProject(app.db, request.params.name);
34868
- const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
35969
+ const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
34869
35970
  if (!session || session.projectId !== project.id) {
34870
35971
  throw notFound("Discovery session", request.params.id);
34871
35972
  }
34872
- const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
35973
+ const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
34873
35974
  const detail = {
34874
35975
  ...serializeSession(session),
34875
35976
  probes: probeRows.map(serializeProbe)
@@ -34881,7 +35982,7 @@ async function discoveryRoutes(app, opts) {
34881
35982
  "/projects/:name/discover/sessions/:id/harvest",
34882
35983
  async (request, reply) => {
34883
35984
  const project = resolveProject(app.db, request.params.name);
34884
- const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
35985
+ const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
34885
35986
  if (!session || session.projectId !== project.id) {
34886
35987
  throw notFound("Discovery session", request.params.id);
34887
35988
  }
@@ -34889,7 +35990,7 @@ async function discoveryRoutes(app, opts) {
34889
35990
  const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
34890
35991
  const applyAnchor = request.query.anchor !== "false";
34891
35992
  const provider = session.seedProvider ?? "gemini";
34892
- const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
35993
+ const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
34893
35994
  const extract = opts.harvestSearchQueries;
34894
35995
  const probesWithQueries = probeRows.map((row) => {
34895
35996
  if (!extract || !row.rawResponse) return { searchQueries: [] };
@@ -34900,7 +36001,7 @@ async function discoveryRoutes(app, opts) {
34900
36001
  return { searchQueries: [] };
34901
36002
  }
34902
36003
  });
34903
- const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query);
36004
+ const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq37(queries.projectId, project.id)).all().map((r) => r.query);
34904
36005
  const anchorTerms = buildHarvestAnchorTerms(
34905
36006
  [session.icpDescription ?? "", ...trackedQueries],
34906
36007
  effectiveDomains(project)
@@ -34947,12 +36048,12 @@ async function discoveryRoutes(app, opts) {
34947
36048
  "/projects/:name/discover/sessions/:id/promote",
34948
36049
  async (request, reply) => {
34949
36050
  const project = resolveProject(app.db, request.params.name);
34950
- const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
36051
+ const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
34951
36052
  if (!session || session.projectId !== project.id) {
34952
36053
  throw notFound("Discovery session", request.params.id);
34953
36054
  }
34954
- const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
34955
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
36055
+ const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
36056
+ const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
34956
36057
  const seenCompetitors = new Set(existingCompetitors);
34957
36058
  const cited = /* @__PURE__ */ new Set();
34958
36059
  const aspirational = /* @__PURE__ */ new Set();
@@ -34981,7 +36082,7 @@ async function discoveryRoutes(app, opts) {
34981
36082
  );
34982
36083
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
34983
36084
  const project = resolveProject(app.db, request.params.name);
34984
- const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
36085
+ const session = app.db.select().from(discoverySessions).where(eq37(discoverySessions.id, request.params.id)).get();
34985
36086
  if (!session || session.projectId !== project.id) {
34986
36087
  throw notFound("Discovery session", request.params.id);
34987
36088
  }
@@ -35004,7 +36105,7 @@ async function discoveryRoutes(app, opts) {
35004
36105
  const bucketSet = new Set(buckets);
35005
36106
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
35006
36107
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
35007
- const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
36108
+ const probeRows = app.db.select().from(discoveryProbes).where(eq37(discoveryProbes.sessionId, session.id)).all();
35008
36109
  const candidateQueries = /* @__PURE__ */ new Set();
35009
36110
  for (const probe of probeRows) {
35010
36111
  if (!probe.bucket) continue;
@@ -35012,7 +36113,7 @@ async function discoveryRoutes(app, opts) {
35012
36113
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
35013
36114
  }
35014
36115
  const existingQueries = new Set(
35015
- app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
36116
+ app.db.select({ query: queries.query }).from(queries).where(eq37(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
35016
36117
  );
35017
36118
  const promotedQueries = [];
35018
36119
  const skippedQueries = [];
@@ -35028,7 +36129,7 @@ async function discoveryRoutes(app, opts) {
35028
36129
  const skippedCompetitors = [];
35029
36130
  if (includeCompetitors) {
35030
36131
  const existingCompetitors = new Set(
35031
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
36132
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
35032
36133
  );
35033
36134
  const competitorMap = parseCompetitorMap(session.competitorMap);
35034
36135
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -35147,7 +36248,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
35147
36248
 
35148
36249
  // ../api-routes/src/discovery/orchestrate.ts
35149
36250
  import crypto29 from "crypto";
35150
- import { eq as eq37 } from "drizzle-orm";
36251
+ import { eq as eq38 } from "drizzle-orm";
35151
36252
  var DEFAULT_MAX_PROBES = 100;
35152
36253
  var ABSOLUTE_MAX_PROBES = 500;
35153
36254
  function classifyProbeBucket(input) {
@@ -35234,7 +36335,7 @@ async function executeDiscovery(opts) {
35234
36335
  status: DiscoverySessionStatuses.seeding,
35235
36336
  dedupThreshold,
35236
36337
  startedAt
35237
- }).where(eq37(discoverySessions.id, opts.sessionId)).run();
36338
+ }).where(eq38(discoverySessions.id, opts.sessionId)).run();
35238
36339
  const seedResult = await opts.deps.seed({
35239
36340
  project: opts.project,
35240
36341
  icpDescription: opts.icpDescription,
@@ -35295,7 +36396,7 @@ async function executeDiscovery(opts) {
35295
36396
  dedupBandPairFraction: dedupStats.bandPairFraction,
35296
36397
  dedupPairsTotal: dedupStats.pairsTotal,
35297
36398
  warning
35298
- }).where(eq37(discoverySessions.id, opts.sessionId)).run();
36399
+ }).where(eq38(discoverySessions.id, opts.sessionId)).run();
35299
36400
  const probeLocation = opts.locations?.[0];
35300
36401
  const probeResults = await mapWithConcurrency(
35301
36402
  probedCanonicals,
@@ -35349,7 +36450,7 @@ async function executeDiscovery(opts) {
35349
36450
  wastedCount: buckets["wasted-surface"],
35350
36451
  competitorMap,
35351
36452
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
35352
- }).where(eq37(discoverySessions.id, opts.sessionId)).run();
36453
+ }).where(eq38(discoverySessions.id, opts.sessionId)).run();
35353
36454
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
35354
36455
  return {
35355
36456
  buckets,
@@ -35389,7 +36490,7 @@ function markSessionFailed(db, sessionId, error) {
35389
36490
  status: DiscoverySessionStatuses.failed,
35390
36491
  error,
35391
36492
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
35392
- }).where(eq37(discoverySessions.id, sessionId)).run();
36493
+ }).where(eq38(discoverySessions.id, sessionId)).run();
35393
36494
  }
35394
36495
  function dedupeStrings(input) {
35395
36496
  const seen = /* @__PURE__ */ new Set();
@@ -35407,7 +36508,7 @@ function dedupeStrings(input) {
35407
36508
 
35408
36509
  // ../api-routes/src/technical-aeo.ts
35409
36510
  import crypto30 from "crypto";
35410
- import { and as and29, asc as asc5, count, desc as desc17, eq as eq38, inArray as inArray14 } from "drizzle-orm";
36511
+ import { and as and30, asc as asc7, count, desc as desc17, eq as eq39, inArray as inArray14, lt as lt2 } from "drizzle-orm";
35411
36512
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
35412
36513
  function emptyScore(projectName) {
35413
36514
  return {
@@ -35439,16 +36540,20 @@ function parsePositiveInt(value, fallback, max) {
35439
36540
  async function technicalAeoRoutes(app, opts) {
35440
36541
  app.get("/projects/:name/technical-aeo", async (request) => {
35441
36542
  const project = resolveProject(app.db, request.params.name);
35442
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
35443
- eq38(siteAuditSnapshots.projectId, project.id),
35444
- eq38(runs.kind, RunKinds["site-audit"]),
36543
+ const baseFilters = [
36544
+ eq39(siteAuditSnapshots.projectId, project.id),
36545
+ eq39(runs.kind, RunKinds["site-audit"]),
35445
36546
  inArray14(runs.status, SURFACEABLE_STATUSES),
35446
36547
  notProbeRun()
35447
- )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
35448
- const latest = rows[0];
35449
- if (!latest) return emptyScore(project.name);
36548
+ ];
36549
+ const targetFilters = request.query.runId ? [...baseFilters, eq39(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
36550
+ const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36551
+ if (!latest) {
36552
+ if (request.query.runId) throw notFound("Site audit run", request.query.runId);
36553
+ return emptyScore(project.name);
36554
+ }
35450
36555
  const snap = latest.snap;
35451
- const previous = rows[1]?.snap ?? null;
36556
+ const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...baseFilters, lt2(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
35452
36557
  const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
35453
36558
  const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
35454
36559
  return {
@@ -35474,24 +36579,29 @@ async function technicalAeoRoutes(app, opts) {
35474
36579
  });
35475
36580
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
35476
36581
  const project = resolveProject(app.db, request.params.name);
35477
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
35478
- eq38(siteAuditSnapshots.projectId, project.id),
35479
- eq38(runs.kind, RunKinds["site-audit"]),
36582
+ const targetFilters = [
36583
+ eq39(siteAuditSnapshots.projectId, project.id),
36584
+ eq39(runs.kind, RunKinds["site-audit"]),
35480
36585
  inArray14(runs.status, SURFACEABLE_STATUSES),
35481
36586
  notProbeRun()
35482
- )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36587
+ ];
36588
+ if (request.query.runId) targetFilters.push(eq39(siteAuditSnapshots.runId, request.query.runId));
36589
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(...targetFilters)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
36590
+ if (!latest && request.query.runId) {
36591
+ throw notFound("Site audit run", request.query.runId);
36592
+ }
35483
36593
  if (!latest) {
35484
36594
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
35485
36595
  }
35486
36596
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
35487
- const conds = [eq38(siteAuditPages.runId, latest.runId)];
35488
- if (statusFilter) conds.push(eq38(siteAuditPages.status, statusFilter));
35489
- const where = and29(...conds);
36597
+ const conds = [eq39(siteAuditPages.runId, latest.runId)];
36598
+ if (statusFilter) conds.push(eq39(siteAuditPages.status, statusFilter));
36599
+ const where = and30(...conds);
35490
36600
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
35491
36601
  const total = totalRow?.value ?? 0;
35492
36602
  const limit = parsePositiveInt(request.query.limit, 100, 500);
35493
36603
  const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
35494
- const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc5(siteAuditPages.url) : asc5(siteAuditPages.overallScore);
36604
+ const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc7(siteAuditPages.url) : asc7(siteAuditPages.overallScore);
35495
36605
  const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
35496
36606
  const pages = rows.map((row) => ({
35497
36607
  url: row.url,
@@ -35510,9 +36620,9 @@ async function technicalAeoRoutes(app, opts) {
35510
36620
  auditedAt: siteAuditSnapshots.auditedAt,
35511
36621
  aggregateScore: siteAuditSnapshots.aggregateScore,
35512
36622
  pagesAudited: siteAuditSnapshots.pagesAudited
35513
- }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
35514
- eq38(siteAuditSnapshots.projectId, project.id),
35515
- eq38(runs.kind, RunKinds["site-audit"]),
36623
+ }).from(siteAuditSnapshots).innerJoin(runs, eq39(siteAuditSnapshots.runId, runs.id)).where(and30(
36624
+ eq39(siteAuditSnapshots.projectId, project.id),
36625
+ eq39(runs.kind, RunKinds["site-audit"]),
35516
36626
  inArray14(runs.status, SURFACEABLE_STATUSES),
35517
36627
  notProbeRun()
35518
36628
  )).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
@@ -35524,9 +36634,9 @@ async function technicalAeoRoutes(app, opts) {
35524
36634
  if (!parsed.success) {
35525
36635
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
35526
36636
  }
35527
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and29(
35528
- eq38(runs.projectId, project.id),
35529
- eq38(runs.kind, RunKinds["site-audit"]),
36637
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and30(
36638
+ eq39(runs.projectId, project.id),
36639
+ eq39(runs.kind, RunKinds["site-audit"]),
35530
36640
  inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
35531
36641
  )).get();
35532
36642
  if (existing) {
@@ -35632,6 +36742,7 @@ async function apiRoutes(app, opts) {
35632
36742
  await api.register(reportRoutes);
35633
36743
  await api.register(citationRoutes);
35634
36744
  await api.register(visibilityStatsRoutes);
36745
+ await api.register(resultsExportRoutes);
35635
36746
  await api.register(compositeRoutes);
35636
36747
  await api.register(contentRoutes, {
35637
36748
  explainContentRecommendation: opts.explainContentRecommendation,
@@ -35665,7 +36776,8 @@ async function apiRoutes(app, opts) {
35665
36776
  await api.register(adsRoutes, {
35666
36777
  adsCredentialStore: opts.adsCredentialStore,
35667
36778
  verifyAdsAccount: opts.verifyAdsAccount,
35668
- onAdsSyncRequested: opts.onAdsSyncRequested
36779
+ onAdsSyncRequested: opts.onAdsSyncRequested,
36780
+ adsOperator: opts.adsOperator
35669
36781
  });
35670
36782
  await api.register(bingRoutes, {
35671
36783
  bingConnectionStore: opts.bingConnectionStore,
@@ -36118,9 +37230,9 @@ var IntelligenceService = class {
36118
37230
  */
36119
37231
  analyzeAndPersist(runId, projectId) {
36120
37232
  const recentRuns = this.db.select().from(runs).where(
36121
- and30(
36122
- eq39(runs.projectId, projectId),
36123
- or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
37233
+ and31(
37234
+ eq40(runs.projectId, projectId),
37235
+ or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
36124
37236
  // Defensive: RunCoordinator already skips probes before this is
36125
37237
  // called, but if a future call site invokes analyzeAndPersist
36126
37238
  // directly for a probe, probes still must not pollute the
@@ -36202,7 +37314,7 @@ var IntelligenceService = class {
36202
37314
  * Returns the persisted insights so the coordinator can count critical/high.
36203
37315
  */
36204
37316
  analyzeAndPersistGbp(runId, projectId) {
36205
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq39(runs.id, runId)).get();
37317
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq40(runs.id, runId)).get();
36206
37318
  if (!runRow) {
36207
37319
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
36208
37320
  this.persistGbpInsights(runId, projectId, [], []);
@@ -36210,11 +37322,11 @@ var IntelligenceService = class {
36210
37322
  }
36211
37323
  const windowStart = runRow.startedAt ?? runRow.createdAt;
36212
37324
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
36213
- const selected = this.db.select().from(gbpLocations).where(and30(
36214
- eq39(gbpLocations.projectId, projectId),
36215
- eq39(gbpLocations.selected, true),
36216
- gte7(gbpLocations.syncedAt, windowStart),
36217
- lte4(gbpLocations.syncedAt, windowEnd)
37325
+ const selected = this.db.select().from(gbpLocations).where(and31(
37326
+ eq40(gbpLocations.projectId, projectId),
37327
+ eq40(gbpLocations.selected, true),
37328
+ gte9(gbpLocations.syncedAt, windowStart),
37329
+ lte5(gbpLocations.syncedAt, windowEnd)
36218
37330
  )).all();
36219
37331
  if (selected.length === 0) {
36220
37332
  log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
@@ -36247,12 +37359,12 @@ var IntelligenceService = class {
36247
37359
  }
36248
37360
  /** Build the per-location signal bundle the GBP analyzer consumes. */
36249
37361
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
36250
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, projectId), eq39(gbpDailyMetrics.locationName, locationName))).all();
36251
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, projectId), eq39(gbpPlaceActions.locationName, locationName))).all();
36252
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(eq39(gbpLodgingSnapshots.projectId, projectId), eq39(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
36253
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(eq39(gbpLocations.projectId, projectId), eq39(gbpLocations.locationName, locationName))).get();
37362
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and31(eq40(gbpDailyMetrics.projectId, projectId), eq40(gbpDailyMetrics.locationName, locationName))).all();
37363
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and31(eq40(gbpPlaceActions.projectId, projectId), eq40(gbpPlaceActions.locationName, locationName))).all();
37364
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and31(eq40(gbpLodgingSnapshots.projectId, projectId), eq40(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
37365
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and31(eq40(gbpLocations.projectId, projectId), eq40(gbpLocations.locationName, locationName))).get();
36254
37366
  const descriptionMissing = !(ownerRow?.description ?? "").trim();
36255
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(eq39(gbpPlaceDetails.projectId, projectId), eq39(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
37367
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and31(eq40(gbpPlaceDetails.projectId, projectId), eq40(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
36256
37368
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
36257
37369
  const summary = buildGbpSummary({
36258
37370
  locationName,
@@ -36288,7 +37400,7 @@ var IntelligenceService = class {
36288
37400
  /** Build the month-over-month keyword series for a location from the
36289
37401
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
36290
37402
  buildGbpKeywordTrend(projectId, locationName) {
36291
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(eq39(gbpKeywordMonthly.projectId, projectId), eq39(gbpKeywordMonthly.locationName, locationName))).all();
37403
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and31(eq40(gbpKeywordMonthly.projectId, projectId), eq40(gbpKeywordMonthly.locationName, locationName))).all();
36292
37404
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
36293
37405
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
36294
37406
  const recentMonth = months[0] ?? null;
@@ -36319,7 +37431,7 @@ var IntelligenceService = class {
36319
37431
  */
36320
37432
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
36321
37433
  const covered = new Set(coveredLocationNames);
36322
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(eq39(insights.projectId, projectId), eq39(insights.provider, GBP_INSIGHT_PROVIDER))).all();
37434
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and31(eq40(insights.projectId, projectId), eq40(insights.provider, GBP_INSIGHT_PROVIDER))).all();
36323
37435
  const staleIds = [];
36324
37436
  const dismissedSlots = /* @__PURE__ */ new Set();
36325
37437
  for (const row of existing) {
@@ -36330,7 +37442,7 @@ var IntelligenceService = class {
36330
37442
  }
36331
37443
  this.db.transaction((tx) => {
36332
37444
  for (const id of staleIds) {
36333
- tx.delete(insights).where(eq39(insights.id, id)).run();
37445
+ tx.delete(insights).where(eq40(insights.id, id)).run();
36334
37446
  }
36335
37447
  for (const insight of gbpInsights) {
36336
37448
  const parsed = parseGbpInsightId(insight.id);
@@ -36408,7 +37520,7 @@ var IntelligenceService = class {
36408
37520
  * create per run + aggregate). DB is left untouched.
36409
37521
  */
36410
37522
  backfill(projectName, opts, onProgress) {
36411
- const project = this.db.select().from(projects).where(eq39(projects.name, projectName)).get();
37523
+ const project = this.db.select().from(projects).where(eq40(projects.name, projectName)).get();
36412
37524
  if (!project) {
36413
37525
  throw new Error(`Project "${projectName}" not found`);
36414
37526
  }
@@ -36421,13 +37533,13 @@ var IntelligenceService = class {
36421
37533
  sinceTimestamp = parsed;
36422
37534
  }
36423
37535
  const allRuns = this.db.select().from(runs).where(
36424
- and30(
36425
- eq39(runs.projectId, project.id),
36426
- or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
37536
+ and31(
37537
+ eq40(runs.projectId, project.id),
37538
+ or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
36427
37539
  // Backfill must not replay probe runs as if they were real sweeps.
36428
37540
  ne5(runs.trigger, RunTriggers.probe)
36429
37541
  )
36430
- ).orderBy(asc6(runs.finishedAt)).all();
37542
+ ).orderBy(asc8(runs.finishedAt)).all();
36431
37543
  let startIdx = 0;
36432
37544
  let endIdx = allRuns.length;
36433
37545
  if (opts?.fromRunId) {
@@ -36502,7 +37614,7 @@ var IntelligenceService = class {
36502
37614
  return { processed, skipped, totalInsights };
36503
37615
  }
36504
37616
  loadTrackedCompetitors(projectId) {
36505
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, projectId)).all().map((r) => r.domain);
37617
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq40(competitors.projectId, projectId)).all().map((r) => r.domain);
36506
37618
  }
36507
37619
  /**
36508
37620
  * Wipe transition signals from an analysis result while keeping health.
@@ -36523,15 +37635,15 @@ var IntelligenceService = class {
36523
37635
  }
36524
37636
  persistResult(result, runId, projectId) {
36525
37637
  const previouslyDismissed = /* @__PURE__ */ new Set();
36526
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq39(insights.runId, runId)).all();
37638
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq40(insights.runId, runId)).all();
36527
37639
  for (const row of existingInsights) {
36528
37640
  if (row.dismissed) {
36529
37641
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
36530
37642
  }
36531
37643
  }
36532
37644
  this.db.transaction((tx) => {
36533
- tx.delete(insights).where(eq39(insights.runId, runId)).run();
36534
- tx.delete(healthSnapshots).where(eq39(healthSnapshots.runId, runId)).run();
37645
+ tx.delete(insights).where(eq40(insights.runId, runId)).run();
37646
+ tx.delete(healthSnapshots).where(eq40(healthSnapshots.runId, runId)).run();
36535
37647
  const now = (/* @__PURE__ */ new Date()).toISOString();
36536
37648
  for (const insight of result.insights) {
36537
37649
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -36584,24 +37696,24 @@ var IntelligenceService = class {
36584
37696
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
36585
37697
  const regressions = rawInsights.filter((i) => i.type === "regression");
36586
37698
  if (regressions.length === 0) return rawInsights;
36587
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq39(gscSearchData.projectId, projectId)).all();
37699
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq40(gscSearchData.projectId, projectId)).all();
36588
37700
  const gscConnected = gscRows.length > 0;
36589
37701
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
36590
37702
  for (const row of gscRows) {
36591
37703
  const key = row.query.toLowerCase();
36592
37704
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
36593
37705
  }
36594
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
37706
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq40(projects.id, projectId)).get();
36595
37707
  const locationCount = Math.max(
36596
37708
  1,
36597
37709
  (projectRow?.locations ?? []).length
36598
37710
  );
36599
37711
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
36600
37712
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
36601
- and30(
36602
- eq39(runs.projectId, projectId),
36603
- eq39(runs.kind, RunKinds["answer-visibility"]),
36604
- or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
37713
+ and31(
37714
+ eq40(runs.projectId, projectId),
37715
+ eq40(runs.kind, RunKinds["answer-visibility"]),
37716
+ or6(eq40(runs.status, "completed"), eq40(runs.status, "partial")),
36605
37717
  // Defensive — see top of file.
36606
37718
  ne5(runs.trigger, RunTriggers.probe)
36607
37719
  )
@@ -36621,7 +37733,7 @@ var IntelligenceService = class {
36621
37733
  const haveHistory = recentRunIds.length > 0;
36622
37734
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
36623
37735
  if (haveHistory) {
36624
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(eq39(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
37736
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and31(eq40(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
36625
37737
  const regressionGroups = /* @__PURE__ */ new Map();
36626
37738
  for (const row of priorRows) {
36627
37739
  if (!row.runId) continue;
@@ -36650,7 +37762,7 @@ var IntelligenceService = class {
36650
37762
  });
36651
37763
  }
36652
37764
  buildRunData(runId, projectId, completedAt, location = null) {
36653
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq39(projects.id, projectId)).get();
37765
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq40(projects.id, projectId)).get();
36654
37766
  const projectDomains = projectDomainRow ? effectiveDomains({
36655
37767
  canonicalDomain: projectDomainRow.canonicalDomain,
36656
37768
  ownedDomains: projectDomainRow.ownedDomains
@@ -36667,7 +37779,7 @@ var IntelligenceService = class {
36667
37779
  citedDomains: querySnapshots.citedDomains,
36668
37780
  competitorOverlap: querySnapshots.competitorOverlap,
36669
37781
  snapshotLocation: querySnapshots.location
36670
- }).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(eq39(querySnapshots.runId, runId)).all();
37782
+ }).from(querySnapshots).leftJoin(queries, eq40(querySnapshots.queryId, queries.id)).where(eq40(querySnapshots.runId, runId)).all();
36671
37783
  const snapshots = [];
36672
37784
  let orphanCount = 0;
36673
37785
  for (const r of rows) {