@canonry/canonry 4.129.1 → 4.130.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/README.md +1 -0
  2. package/assets/assets/{AuditHistoryPanel-wJqs99Ak.js → AuditHistoryPanel-Ydzb2Y_Y.js} +1 -1
  3. package/assets/assets/BacklinksPage-DYodEusK.js +1 -0
  4. package/assets/assets/{ChartPrimitives-DSr5ldMf.js → ChartPrimitives-D67hsnF7.js} +1 -1
  5. package/assets/assets/{HistoryPage-Ca-GGnTI.js → HistoryPage-_h9ITL1w.js} +1 -1
  6. package/assets/assets/ProjectPage-fCJKCcHE.js +8 -0
  7. package/assets/assets/{RunRow-O9TbOdQY.js → RunRow-DzUNyTE-.js} +1 -1
  8. package/assets/assets/{RunsPage-C1Sqd7AN.js → RunsPage-FTvAjptk.js} +1 -1
  9. package/assets/assets/{SettingsPage-DPFh62BV.js → SettingsPage-CvgpYEAt.js} +1 -1
  10. package/assets/assets/{TrafficPage-5vKoQyoP.js → TrafficPage-qaMW9k6o.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-D17oADs8.js → TrafficSourceDetailPage-CRCso7_A.js} +1 -1
  12. package/assets/assets/{arrow-left-De5P5AIE.js → arrow-left-C--lHdP2.js} +1 -1
  13. package/assets/assets/{extract-error-message-BqMd-fQ6.js → extract-error-message-DVqYbKpB.js} +1 -1
  14. package/assets/assets/index-CdXn0JEf.js +210 -0
  15. package/assets/assets/{index-jymsMBpZ.css → index-JhX-wfQO.css} +1 -1
  16. package/assets/assets/trash-2-CyKRU3tR.js +1 -0
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-LPEE5AYY.js → chunk-5XU6WEMK.js} +649 -298
  19. package/dist/{chunk-K4FDDCYG.js → chunk-NVRJZU6V.js} +2742 -2626
  20. package/dist/{chunk-GYAKRVE2.js → chunk-QR5U2K3O.js} +508 -355
  21. package/dist/{chunk-WWZE7VDQ.js → chunk-WIALMCXM.js} +118 -1
  22. package/dist/cli.js +432 -182
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-2QBAGT5X.js → intelligence-service-53GUE6EI.js} +2 -2
  25. package/dist/mcp.js +5 -5
  26. package/package.json +8 -8
  27. package/assets/assets/BacklinksPage-BBDExYo_.js +0 -1
  28. package/assets/assets/ProjectPage-BuaSDSwf.js +0 -7
  29. package/assets/assets/index-C1ABEgkr.js +0 -210
  30. package/assets/assets/trash-2-6ZpABj9G.js +0 -1
@@ -29,6 +29,7 @@ import {
29
29
  ContentActions,
30
30
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
31
31
  DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
32
+ DEFAULT_MEASUREMENT_CONFIG,
32
33
  DISCOVERY_DEFAULT_DEDUP_THRESHOLD,
33
34
  DISCOVERY_DEFAULT_PROBE_CONCURRENCY,
34
35
  DISCOVERY_PROBE_CONCURRENCY_CAP,
@@ -38,6 +39,8 @@ import {
38
39
  DiscoveryCompetitorTypes,
39
40
  DiscoverySessionStatuses,
40
41
  MODEL_ATTRIBUTION_EVENT_LIMIT,
42
+ ResearchQueryStatuses,
43
+ ResearchRunStatuses,
41
44
  RunKinds,
42
45
  RunStatuses,
43
46
  RunTriggers,
@@ -227,6 +230,7 @@ import {
227
230
  hostOf,
228
231
  indexingRequestResponseDtoSchema,
229
232
  internalError,
233
+ isBrowserProvider,
230
234
  isReadOnlyKey,
231
235
  keywordDtoSchema,
232
236
  keywordGenerateRequestSchema,
@@ -271,6 +275,9 @@ import {
271
275
  reportConfidenceLabel,
272
276
  reportHorizonLabel,
273
277
  reportSeverityLabel,
278
+ researchRunCreateSchema,
279
+ researchRunDetailSchema,
280
+ researchRunListSchema,
274
281
  resolveConfigSpecQueries,
275
282
  resolveLocations,
276
283
  resolveSnapshotRequestQueries,
@@ -337,10 +344,10 @@ import {
337
344
  wordpressSchemaDeployResultDtoSchema,
338
345
  wordpressSchemaStatusResultDtoSchema,
339
346
  wordpressStatusDtoSchema
340
- } from "./chunk-K4FDDCYG.js";
347
+ } from "./chunk-NVRJZU6V.js";
341
348
 
342
349
  // src/intelligence-service.ts
343
- import { eq as eq41, desc as desc18, asc as asc9, and as and32, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
350
+ import { eq as eq42, desc as desc19, asc as asc9, and as and33, ne as ne6, or as or8, inArray as inArray15, gte as gte10, lte as lte6 } from "drizzle-orm";
344
351
 
345
352
  // ../db/src/client.ts
346
353
  import { mkdirSync } from "fs";
@@ -379,9 +386,12 @@ __export(schema_exports, {
379
386
  discoveryProbes: () => discoveryProbes,
380
387
  discoverySessions: () => discoverySessions,
381
388
  domainClassifications: () => domainClassifications,
389
+ gaAcquisitionDaily: () => gaAcquisitionDaily,
382
390
  gaAiReferrals: () => gaAiReferrals,
383
391
  gaConnections: () => gaConnections,
384
392
  gaDailyTotals: () => gaDailyTotals,
393
+ gaLeadEventsDaily: () => gaLeadEventsDaily,
394
+ gaMeasurementSyncStates: () => gaMeasurementSyncStates,
385
395
  gaSocialReferrals: () => gaSocialReferrals,
386
396
  gaTrafficSnapshots: () => gaTrafficSnapshots,
387
397
  gaTrafficSummaries: () => gaTrafficSummaries,
@@ -411,6 +421,8 @@ __export(schema_exports, {
411
421
  rawEventSamples: () => rawEventSamples,
412
422
  recommendationBriefs: () => recommendationBriefs,
413
423
  recommendationExplanations: () => recommendationExplanations,
424
+ researchRunQueries: () => researchRunQueries,
425
+ researchRuns: () => researchRuns,
414
426
  runs: () => runs,
415
427
  schedules: () => schedules,
416
428
  siteAuditPages: () => siteAuditPages,
@@ -418,7 +430,8 @@ __export(schema_exports, {
418
430
  trafficSources: () => trafficSources,
419
431
  usageCounters: () => usageCounters
420
432
  });
421
- import { index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
433
+ import { sql } from "drizzle-orm";
434
+ import { check, index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
422
435
  var projects = sqliteTable("projects", {
423
436
  id: text("id").primaryKey(),
424
437
  name: text("name").notNull().unique(),
@@ -432,6 +445,11 @@ var projects = sqliteTable("projects", {
432
445
  labels: text("labels", { mode: "json" }).$type().notNull().default({}),
433
446
  providers: text("providers", { mode: "json" }).$type().notNull().default([]),
434
447
  providerModels: text("provider_models", { mode: "json" }).$type().notNull().default({}),
448
+ measurement: text("measurement_config", { mode: "json" }).$type().notNull().default({
449
+ marketingHosts: [],
450
+ brandTerms: [],
451
+ leadEventNames: ["generate_lead"]
452
+ }),
435
453
  locations: text("locations", { mode: "json" }).$type().notNull().default([]),
436
454
  defaultLocation: text("default_location"),
437
455
  autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
@@ -863,6 +881,97 @@ var gaDailyTotals = sqliteTable("ga_daily_totals", {
863
881
  index("idx_ga_daily_totals_project").on(table.projectId),
864
882
  index("idx_ga_daily_totals_run").on(table.syncRunId)
865
883
  ]);
884
+ var gaAcquisitionDaily = sqliteTable("ga_acquisition_daily", {
885
+ id: text("id").primaryKey(),
886
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
887
+ date: text("date").notNull(),
888
+ channelGroup: text("channel_group").notNull(),
889
+ source: text("source").notNull(),
890
+ medium: text("medium").notNull(),
891
+ hostName: text("host_name").notNull(),
892
+ landingPage: text("landing_page").notNull(),
893
+ landingPageNormalized: text("landing_page_normalized"),
894
+ sessions: integer("sessions").notNull().default(0),
895
+ syncedAt: text("synced_at").notNull(),
896
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
897
+ createdAt: text("created_at").notNull()
898
+ }, (table) => [
899
+ uniqueIndex("idx_ga_acquisition_daily_grain").on(
900
+ table.projectId,
901
+ table.date,
902
+ table.channelGroup,
903
+ table.source,
904
+ table.medium,
905
+ table.hostName,
906
+ table.landingPage
907
+ ),
908
+ index("idx_ga_acquisition_daily_project_date").on(table.projectId, table.date),
909
+ index("idx_ga_acquisition_daily_project_channel").on(table.projectId, table.date, table.channelGroup),
910
+ index("idx_ga_acquisition_daily_project_page").on(table.projectId, table.date, table.landingPageNormalized),
911
+ check("chk_ga_acquisition_daily_sessions", sql`${table.sessions} >= 0`)
912
+ ]);
913
+ var gaLeadEventsDaily = sqliteTable("ga_lead_events_daily", {
914
+ id: text("id").primaryKey(),
915
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
916
+ date: text("date").notNull(),
917
+ eventName: text("event_name").notNull(),
918
+ channelGroup: text("channel_group").notNull(),
919
+ source: text("source").notNull(),
920
+ medium: text("medium").notNull(),
921
+ hostName: text("host_name").notNull(),
922
+ landingPage: text("landing_page").notNull(),
923
+ landingPageNormalized: text("landing_page_normalized"),
924
+ attributionScope: text("attribution_scope").$type().notNull(),
925
+ eventCount: integer("event_count").notNull().default(0),
926
+ syncedAt: text("synced_at").notNull(),
927
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
928
+ createdAt: text("created_at").notNull()
929
+ }, (table) => [
930
+ uniqueIndex("idx_ga_lead_events_daily_grain").on(
931
+ table.projectId,
932
+ table.date,
933
+ table.eventName,
934
+ table.channelGroup,
935
+ table.source,
936
+ table.medium,
937
+ table.hostName,
938
+ table.landingPage,
939
+ table.attributionScope
940
+ ),
941
+ index("idx_ga_lead_events_daily_project_date").on(table.projectId, table.date),
942
+ index("idx_ga_lead_events_daily_project_channel").on(table.projectId, table.date, table.channelGroup),
943
+ index("idx_ga_lead_events_daily_project_event").on(table.projectId, table.date, table.eventName),
944
+ index("idx_ga_lead_events_daily_project_page").on(table.projectId, table.date, table.landingPageNormalized),
945
+ check("chk_ga_lead_events_daily_count", sql`${table.eventCount} >= 0`),
946
+ check(
947
+ "chk_ga_lead_events_daily_scope",
948
+ sql`${table.attributionScope} IN ('landing-page', 'channel')`
949
+ )
950
+ ]);
951
+ var gaMeasurementSyncStates = sqliteTable("ga_measurement_sync_state", {
952
+ projectId: text("project_id").primaryKey().references(() => projects.id, { onDelete: "cascade" }),
953
+ acquisitionStatus: text("acquisition_status").$type().notNull().default("never-synced"),
954
+ acquisitionError: text("acquisition_error"),
955
+ acquisitionSyncedAt: text("acquisition_synced_at"),
956
+ leadStatus: text("lead_status").$type().notNull().default("never-synced"),
957
+ leadError: text("lead_error"),
958
+ leadSyncedAt: text("lead_synced_at"),
959
+ leadAttributionScope: text("lead_attribution_scope").$type(),
960
+ updatedAt: text("updated_at").notNull()
961
+ }, (table) => [
962
+ check(
963
+ "chk_ga_measurement_sync_acquisition_status",
964
+ sql`${table.acquisitionStatus} IN ('never-synced', 'ready', 'error')`
965
+ ),
966
+ check(
967
+ "chk_ga_measurement_sync_lead_status",
968
+ sql`${table.leadStatus} IN ('never-synced', 'ready', 'error')`
969
+ ),
970
+ check(
971
+ "chk_ga_measurement_sync_lead_scope",
972
+ sql`${table.leadAttributionScope} IS NULL OR ${table.leadAttributionScope} IN ('landing-page', 'channel')`
973
+ )
974
+ ]);
866
975
  var gaAiReferrals = sqliteTable("ga_ai_referrals", {
867
976
  id: text("id").primaryKey(),
868
977
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -1310,6 +1419,45 @@ var discoverySessions = sqliteTable("discovery_sessions", {
1310
1419
  index("idx_discovery_sessions_project_created").on(table.projectId, table.createdAt),
1311
1420
  index("idx_discovery_sessions_run").on(table.runId)
1312
1421
  ]);
1422
+ var researchRuns = sqliteTable("research_runs", {
1423
+ id: text("id").primaryKey(),
1424
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1425
+ status: text("status").notNull().default("queued"),
1426
+ provider: text("provider").notNull(),
1427
+ requestedModel: text("requested_model"),
1428
+ resolvedModel: text("resolved_model").notNull(),
1429
+ location: text("location", { mode: "json" }).$type(),
1430
+ totalQueries: integer("total_queries").notNull(),
1431
+ completedQueries: integer("completed_queries").notNull().default(0),
1432
+ failedQueries: integer("failed_queries").notNull().default(0),
1433
+ idempotencyKey: text("idempotency_key"),
1434
+ requestHash: text("request_hash"),
1435
+ error: text("error"),
1436
+ startedAt: text("started_at"),
1437
+ finishedAt: text("finished_at"),
1438
+ createdAt: text("created_at").notNull()
1439
+ }, (table) => [index("idx_research_runs_project_created").on(table.projectId, table.createdAt), index("idx_research_runs_status").on(table.status), uniqueIndex("idx_research_runs_project_idempotency").on(table.projectId, table.idempotencyKey)]);
1440
+ var researchRunQueries = sqliteTable("research_run_queries", {
1441
+ id: text("id").primaryKey(),
1442
+ researchRunId: text("research_run_id").notNull().references(() => researchRuns.id, { onDelete: "cascade" }),
1443
+ position: integer("position").notNull(),
1444
+ queryText: text("query_text").notNull(),
1445
+ status: text("status").notNull().default("queued"),
1446
+ requestedModel: text("requested_model"),
1447
+ resolvedModel: text("resolved_model").notNull(),
1448
+ servedModel: text("served_model"),
1449
+ answerText: text("answer_text"),
1450
+ groundingSources: text("grounding_sources", { mode: "json" }).$type().notNull().default([]),
1451
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
1452
+ searchQueries: text("search_queries", { mode: "json" }).$type().notNull().default([]),
1453
+ answerMentioned: integer("answer_mentioned", { mode: "boolean" }),
1454
+ citationState: text("citation_state"),
1455
+ rawResponse: text("raw_response", { mode: "json" }).$type(),
1456
+ error: text("error"),
1457
+ startedAt: text("started_at"),
1458
+ finishedAt: text("finished_at"),
1459
+ createdAt: text("created_at").notNull()
1460
+ }, (table) => [index("idx_research_run_queries_run").on(table.researchRunId), uniqueIndex("idx_research_run_queries_run_position").on(table.researchRunId, table.position)]);
1313
1461
  var discoveryProbes = sqliteTable("discovery_probes", {
1314
1462
  id: text("id").primaryKey(),
1315
1463
  sessionId: text("session_id").notNull().references(() => discoverySessions.id, { onDelete: "cascade" }),
@@ -1716,7 +1864,7 @@ function parseJsonColumn(value, fallback) {
1716
1864
  }
1717
1865
 
1718
1866
  // ../db/src/migrate.ts
1719
- import { sql } from "drizzle-orm";
1867
+ import { sql as sql2 } from "drizzle-orm";
1720
1868
  var MIGRATION_SQL = `
1721
1869
  CREATE TABLE IF NOT EXISTS projects (
1722
1870
  id TEXT PRIMARY KEY,
@@ -1859,14 +2007,14 @@ CREATE TABLE IF NOT EXISTS _migrations (
1859
2007
  );
1860
2008
  `;
1861
2009
  function relinkOrphanedSnapshotQueryIds(tx) {
1862
- const orphans = tx.all(sql`
2010
+ const orphans = tx.all(sql2`
1863
2011
  SELECT qs.id AS snapId, qs.query_text AS text, r.project_id AS projectId
1864
2012
  FROM query_snapshots qs
1865
2013
  JOIN runs r ON r.id = qs.run_id
1866
2014
  WHERE qs.query_id IS NULL AND qs.query_text IS NOT NULL
1867
2015
  `);
1868
2016
  if (orphans.length === 0) return;
1869
- const queryRows = tx.all(sql`SELECT id, project_id AS projectId, query FROM queries`);
2017
+ const queryRows = tx.all(sql2`SELECT id, project_id AS projectId, query FROM queries`);
1870
2018
  const byProject = /* @__PURE__ */ new Map();
1871
2019
  for (const row of queryRows) {
1872
2020
  let perProject = byProject.get(row.projectId);
@@ -1880,12 +2028,12 @@ function relinkOrphanedSnapshotQueryIds(tx) {
1880
2028
  for (const orphan of orphans) {
1881
2029
  const queryId = byProject.get(orphan.projectId)?.get(normalizeQueryText(orphan.text));
1882
2030
  if (queryId) {
1883
- tx.run(sql`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
2031
+ tx.run(sql2`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
1884
2032
  }
1885
2033
  }
1886
2034
  }
1887
2035
  function backfillQuerySnapshotServedModel(tx) {
1888
- tx.run(sql.raw(`
2036
+ tx.run(sql2.raw(`
1889
2037
  UPDATE query_snapshots
1890
2038
  SET served_model = json_extract(raw_response, '$.apiResponse.model')
1891
2039
  WHERE served_model IS NULL
@@ -2670,10 +2818,10 @@ var MIGRATION_VERSIONS = [
2670
2818
  run: (tx) => {
2671
2819
  if (!tableExists(tx, "ga_ai_referrals")) return;
2672
2820
  if (!columnExists(tx, "ga_ai_referrals", "channel_group")) {
2673
- tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN channel_group TEXT NOT NULL DEFAULT '(not set)'`));
2821
+ tx.run(sql2.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN channel_group TEXT NOT NULL DEFAULT '(not set)'`));
2674
2822
  }
2675
- tx.run(sql.raw(`DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v3`));
2676
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
2823
+ tx.run(sql2.raw(`DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v3`));
2824
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v4
2677
2825
  ON ga_ai_referrals(project_id, date, source, medium, source_dimension, channel_group, landing_page)`));
2678
2826
  }
2679
2827
  },
@@ -3126,11 +3274,11 @@ var MIGRATION_VERSIONS = [
3126
3274
  statements: [],
3127
3275
  run: (db) => {
3128
3276
  if (tableExists(db, "google_connections") && !columnExists(db, "google_connections", "created_by_project_id")) {
3129
- db.run(sql.raw(
3277
+ db.run(sql2.raw(
3130
3278
  `ALTER TABLE google_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
3131
3279
  ));
3132
- db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_google_conn_project ON google_connections(created_by_project_id)`));
3133
- db.run(sql.raw(
3280
+ db.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_google_conn_project ON google_connections(created_by_project_id)`));
3281
+ db.run(sql2.raw(
3134
3282
  `UPDATE google_connections
3135
3283
  SET created_by_project_id = (
3136
3284
  SELECT p.id FROM projects p
@@ -3142,11 +3290,11 @@ var MIGRATION_VERSIONS = [
3142
3290
  ));
3143
3291
  }
3144
3292
  if (tableExists(db, "bing_connections") && !columnExists(db, "bing_connections", "created_by_project_id")) {
3145
- db.run(sql.raw(
3293
+ db.run(sql2.raw(
3146
3294
  `ALTER TABLE bing_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
3147
3295
  ));
3148
- db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_bing_conn_project ON bing_connections(created_by_project_id)`));
3149
- db.run(sql.raw(
3296
+ db.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_bing_conn_project ON bing_connections(created_by_project_id)`));
3297
+ db.run(sql2.raw(
3150
3298
  `UPDATE bing_connections
3151
3299
  SET created_by_project_id = (
3152
3300
  SELECT p.id FROM projects p
@@ -3516,10 +3664,10 @@ var MIGRATION_VERSIONS = [
3516
3664
  run: (db) => {
3517
3665
  if (!tableExists(db, "health_snapshots")) return;
3518
3666
  if (!columnExists(db, "health_snapshots", "overall_mention_rate")) {
3519
- db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN overall_mention_rate TEXT`));
3667
+ db.run(sql2.raw(`ALTER TABLE health_snapshots ADD COLUMN overall_mention_rate TEXT`));
3520
3668
  }
3521
3669
  if (!columnExists(db, "health_snapshots", "mentioned_pairs")) {
3522
- db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
3670
+ db.run(sql2.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
3523
3671
  }
3524
3672
  }
3525
3673
  },
@@ -3540,7 +3688,7 @@ var MIGRATION_VERSIONS = [
3540
3688
  ];
3541
3689
  for (const col of cols) {
3542
3690
  if (!columnExists(db, "gbp_locations", col)) {
3543
- db.run(sql.raw(`ALTER TABLE gbp_locations ADD COLUMN ${col} TEXT`));
3691
+ db.run(sql2.raw(`ALTER TABLE gbp_locations ADD COLUMN ${col} TEXT`));
3544
3692
  }
3545
3693
  }
3546
3694
  }
@@ -3736,9 +3884,9 @@ var MIGRATION_VERSIONS = [
3736
3884
  run: (tx) => {
3737
3885
  if (!tableExists(tx, "ga_ai_referrals")) return;
3738
3886
  if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) {
3739
- tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
3887
+ tx.run(sql2.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
3740
3888
  }
3741
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
3889
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
3742
3890
  }
3743
3891
  },
3744
3892
  {
@@ -4022,14 +4170,98 @@ var MIGRATION_VERSIONS = [
4022
4170
  `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_query ON gsc_query_daily_totals(query)`,
4023
4171
  `CREATE INDEX IF NOT EXISTS idx_gsc_query_daily_totals_run ON gsc_query_daily_totals(sync_run_id)`
4024
4172
  ]
4173
+ },
4174
+ {
4175
+ version: 108,
4176
+ name: "research-query-runs",
4177
+ statements: [
4178
+ `CREATE TABLE IF NOT EXISTS research_runs (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'queued', provider TEXT NOT NULL, requested_model TEXT, resolved_model TEXT NOT NULL, location TEXT, total_queries INTEGER NOT NULL, completed_queries INTEGER NOT NULL DEFAULT 0, failed_queries INTEGER NOT NULL DEFAULT 0, idempotency_key TEXT, request_hash TEXT, error TEXT, started_at TEXT, finished_at TEXT, created_at TEXT NOT NULL)`,
4179
+ `CREATE INDEX IF NOT EXISTS idx_research_runs_project_created ON research_runs(project_id, created_at)`,
4180
+ `CREATE INDEX IF NOT EXISTS idx_research_runs_status ON research_runs(status)`,
4181
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_research_runs_project_idempotency ON research_runs(project_id, idempotency_key)`,
4182
+ `CREATE TABLE IF NOT EXISTS research_run_queries (id TEXT PRIMARY KEY, research_run_id TEXT NOT NULL REFERENCES research_runs(id) ON DELETE CASCADE, position INTEGER NOT NULL, query_text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'queued', requested_model TEXT, resolved_model TEXT NOT NULL, served_model TEXT, answer_text TEXT, grounding_sources TEXT NOT NULL DEFAULT '[]', cited_domains TEXT NOT NULL DEFAULT '[]', search_queries TEXT NOT NULL DEFAULT '[]', answer_mentioned INTEGER, citation_state TEXT, raw_response TEXT, error TEXT, started_at TEXT, finished_at TEXT, created_at TEXT NOT NULL)`,
4183
+ `CREATE INDEX IF NOT EXISTS idx_research_run_queries_run ON research_run_queries(research_run_id)`,
4184
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_research_run_queries_run_position ON research_run_queries(research_run_id, position)`
4185
+ ]
4186
+ },
4187
+ {
4188
+ version: 109,
4189
+ name: "ga-measurement-foundation",
4190
+ statements: [
4191
+ `ALTER TABLE projects ADD COLUMN measurement_config TEXT NOT NULL DEFAULT '{"marketingHosts":[],"brandTerms":[],"leadEventNames":["generate_lead"]}'`,
4192
+ `CREATE TABLE IF NOT EXISTS ga_acquisition_daily (
4193
+ id TEXT PRIMARY KEY,
4194
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4195
+ date TEXT NOT NULL,
4196
+ channel_group TEXT NOT NULL,
4197
+ source TEXT NOT NULL,
4198
+ medium TEXT NOT NULL,
4199
+ host_name TEXT NOT NULL,
4200
+ landing_page TEXT NOT NULL,
4201
+ landing_page_normalized TEXT,
4202
+ sessions INTEGER NOT NULL DEFAULT 0 CHECK (sessions >= 0),
4203
+ synced_at TEXT NOT NULL,
4204
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
4205
+ created_at TEXT NOT NULL
4206
+ )`,
4207
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_acquisition_daily_grain
4208
+ ON ga_acquisition_daily(project_id, date, channel_group, source, medium, host_name, landing_page)`,
4209
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_date
4210
+ ON ga_acquisition_daily(project_id, date)`,
4211
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_channel
4212
+ ON ga_acquisition_daily(project_id, date, channel_group)`,
4213
+ `CREATE INDEX IF NOT EXISTS idx_ga_acquisition_daily_project_page
4214
+ ON ga_acquisition_daily(project_id, date, landing_page_normalized)`,
4215
+ `CREATE TABLE IF NOT EXISTS ga_lead_events_daily (
4216
+ id TEXT PRIMARY KEY,
4217
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4218
+ date TEXT NOT NULL,
4219
+ event_name TEXT NOT NULL,
4220
+ channel_group TEXT NOT NULL,
4221
+ source TEXT NOT NULL,
4222
+ medium TEXT NOT NULL,
4223
+ host_name TEXT NOT NULL,
4224
+ landing_page TEXT NOT NULL,
4225
+ landing_page_normalized TEXT,
4226
+ attribution_scope TEXT NOT NULL CHECK (attribution_scope IN ('landing-page', 'channel')),
4227
+ event_count INTEGER NOT NULL DEFAULT 0 CHECK (event_count >= 0),
4228
+ synced_at TEXT NOT NULL,
4229
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
4230
+ created_at TEXT NOT NULL
4231
+ )`,
4232
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_lead_events_daily_grain
4233
+ ON ga_lead_events_daily(project_id, date, event_name, channel_group, source, medium, host_name, landing_page, attribution_scope)`,
4234
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_date
4235
+ ON ga_lead_events_daily(project_id, date)`,
4236
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_channel
4237
+ ON ga_lead_events_daily(project_id, date, channel_group)`,
4238
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_event
4239
+ ON ga_lead_events_daily(project_id, date, event_name)`,
4240
+ `CREATE INDEX IF NOT EXISTS idx_ga_lead_events_daily_project_page
4241
+ ON ga_lead_events_daily(project_id, date, landing_page_normalized)`,
4242
+ `CREATE TABLE IF NOT EXISTS ga_measurement_sync_state (
4243
+ project_id TEXT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
4244
+ acquisition_status TEXT NOT NULL DEFAULT 'never-synced'
4245
+ CHECK (acquisition_status IN ('never-synced', 'ready', 'error')),
4246
+ acquisition_error TEXT,
4247
+ acquisition_synced_at TEXT,
4248
+ lead_status TEXT NOT NULL DEFAULT 'never-synced'
4249
+ CHECK (lead_status IN ('never-synced', 'ready', 'error')),
4250
+ lead_error TEXT,
4251
+ lead_synced_at TEXT,
4252
+ lead_attribution_scope TEXT
4253
+ CHECK (lead_attribution_scope IS NULL OR lead_attribution_scope IN ('landing-page', 'channel')),
4254
+ updated_at TEXT NOT NULL
4255
+ )`
4256
+ ]
4025
4257
  }
4026
4258
  ];
4027
4259
  function rebuildBacklinkTableWithSource(tx, table) {
4028
4260
  if (!tableExists(tx, table)) return;
4029
4261
  if (columnExists(tx, table, "source")) return;
4030
4262
  if (table === "backlink_domains") {
4031
- tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
4032
- tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
4263
+ tx.run(sql2.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
4264
+ tx.run(sql2.raw(`CREATE TABLE backlink_domains_v78 (
4033
4265
  id TEXT PRIMARY KEY,
4034
4266
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4035
4267
  release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
@@ -4040,24 +4272,24 @@ function rebuildBacklinkTableWithSource(tx, table) {
4040
4272
  num_hosts INTEGER NOT NULL,
4041
4273
  created_at TEXT NOT NULL
4042
4274
  )`));
4043
- tx.run(sql.raw(`INSERT INTO backlink_domains_v78
4275
+ tx.run(sql2.raw(`INSERT INTO backlink_domains_v78
4044
4276
  (id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
4045
4277
  SELECT bd.id, bd.project_id,
4046
4278
  CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
4047
4279
  'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
4048
4280
  FROM backlink_domains bd
4049
4281
  WHERE bd.project_id IN (SELECT id FROM projects)`));
4050
- tx.run(sql.raw(`DROP TABLE backlink_domains`));
4051
- tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
4052
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
4053
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
4054
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
4055
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
4056
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
4282
+ tx.run(sql2.raw(`DROP TABLE backlink_domains`));
4283
+ tx.run(sql2.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
4284
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
4285
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
4286
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
4287
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
4288
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
4057
4289
  return;
4058
4290
  }
4059
- tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
4060
- tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
4291
+ tx.run(sql2.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
4292
+ tx.run(sql2.raw(`CREATE TABLE backlink_summaries_v78 (
4061
4293
  id TEXT PRIMARY KEY,
4062
4294
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
4063
4295
  release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
@@ -4070,17 +4302,17 @@ function rebuildBacklinkTableWithSource(tx, table) {
4070
4302
  queried_at TEXT NOT NULL,
4071
4303
  created_at TEXT NOT NULL
4072
4304
  )`));
4073
- tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
4305
+ tx.run(sql2.raw(`INSERT INTO backlink_summaries_v78
4074
4306
  (id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
4075
4307
  SELECT bs.id, bs.project_id,
4076
4308
  CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
4077
4309
  'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
4078
4310
  FROM backlink_summaries bs
4079
4311
  WHERE bs.project_id IN (SELECT id FROM projects)`));
4080
- tx.run(sql.raw(`DROP TABLE backlink_summaries`));
4081
- tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
4082
- tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
4083
- tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
4312
+ tx.run(sql2.raw(`DROP TABLE backlink_summaries`));
4313
+ tx.run(sql2.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
4314
+ tx.run(sql2.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
4315
+ tx.run(sql2.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
4084
4316
  }
4085
4317
  function addBacklinkSourceDiscriminator(tx) {
4086
4318
  rebuildBacklinkTableWithSource(tx, "backlink_domains");
@@ -4093,25 +4325,25 @@ function isDuplicateColumnError(err) {
4093
4325
  return false;
4094
4326
  }
4095
4327
  function columnExists(db, table, column) {
4096
- const rows = db.all(sql.raw(
4328
+ const rows = db.all(sql2.raw(
4097
4329
  `SELECT COUNT(*) as c FROM pragma_table_info('${table}') WHERE name = '${column}'`
4098
4330
  ));
4099
4331
  return (rows[0]?.c ?? 0) > 0;
4100
4332
  }
4101
4333
  function tableExists(db, table) {
4102
- const rows = db.all(sql.raw(
4334
+ const rows = db.all(sql2.raw(
4103
4335
  `SELECT COUNT(*) as c FROM sqlite_master WHERE type = 'table' AND name = '${table}'`
4104
4336
  ));
4105
4337
  return (rows[0]?.c ?? 0) > 0;
4106
4338
  }
4107
4339
  function tableIsEmpty(db, table) {
4108
- const rows = db.all(sql.raw(`SELECT COUNT(*) as c FROM ${table}`));
4340
+ const rows = db.all(sql2.raw(`SELECT COUNT(*) as c FROM ${table}`));
4109
4341
  return (rows[0]?.c ?? 0) === 0;
4110
4342
  }
4111
4343
  function backfillGaAiReferralTrafficClass(tx) {
4112
4344
  if (!tableExists(tx, "ga_ai_referrals")) return 0;
4113
4345
  if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) return 0;
4114
- const rows = tx.all(sql.raw(
4346
+ const rows = tx.all(sql2.raw(
4115
4347
  `SELECT id, source, medium, channel_group, landing_page, traffic_class FROM ga_ai_referrals`
4116
4348
  ));
4117
4349
  let updated = 0;
@@ -4123,7 +4355,7 @@ function backfillGaAiReferralTrafficClass(tx) {
4123
4355
  landingPage: row.landing_page
4124
4356
  });
4125
4357
  if (next === row.traffic_class) continue;
4126
- tx.run(sql`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
4358
+ tx.run(sql2`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
4127
4359
  updated += 1;
4128
4360
  }
4129
4361
  return updated;
@@ -4137,28 +4369,28 @@ function normalizeLegacyQuerySchema(db) {
4137
4369
  if (!tableIsEmpty(db, "queries")) {
4138
4370
  throw new Error("Cannot migrate keywords to queries because both tables contain data");
4139
4371
  }
4140
- db.run(sql.raw(`DROP TABLE queries`));
4372
+ db.run(sql2.raw(`DROP TABLE queries`));
4141
4373
  }
4142
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project`));
4143
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project_keyword`));
4144
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_snapshots_keyword`));
4145
- db.run(sql.raw(`DROP INDEX IF EXISTS idx_insights_keyword_provider`));
4374
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_keywords_project`));
4375
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_keywords_project_keyword`));
4376
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_snapshots_keyword`));
4377
+ db.run(sql2.raw(`DROP INDEX IF EXISTS idx_insights_keyword_provider`));
4146
4378
  if (tableExists(db, "keywords")) {
4147
- db.run(sql.raw(`ALTER TABLE keywords RENAME TO queries`));
4379
+ db.run(sql2.raw(`ALTER TABLE keywords RENAME TO queries`));
4148
4380
  }
4149
4381
  if (columnExists(db, "queries", "keyword")) {
4150
- db.run(sql.raw(`ALTER TABLE queries RENAME COLUMN keyword TO query`));
4382
+ db.run(sql2.raw(`ALTER TABLE queries RENAME COLUMN keyword TO query`));
4151
4383
  }
4152
4384
  if (columnExists(db, "query_snapshots", "keyword_id")) {
4153
- db.run(sql.raw(`ALTER TABLE query_snapshots RENAME COLUMN keyword_id TO query_id`));
4385
+ db.run(sql2.raw(`ALTER TABLE query_snapshots RENAME COLUMN keyword_id TO query_id`));
4154
4386
  }
4155
4387
  if (columnExists(db, "insights", "keyword")) {
4156
- db.run(sql.raw(`ALTER TABLE insights RENAME COLUMN keyword TO query`));
4388
+ db.run(sql2.raw(`ALTER TABLE insights RENAME COLUMN keyword TO query`));
4157
4389
  }
4158
4390
  }
4159
4391
  function dropColumnIfExists(db, table, column) {
4160
4392
  try {
4161
- db.run(sql.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
4393
+ db.run(sql2.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
4162
4394
  } catch (err) {
4163
4395
  if (!(err instanceof Error)) throw err;
4164
4396
  const msg = err.message;
@@ -4173,7 +4405,7 @@ function dropColumnIfExists(db, table, column) {
4173
4405
  function extractLegacyCredentials(db) {
4174
4406
  const out = { google: [], ga4: [] };
4175
4407
  if (columnExists(db, "google_connections", "access_token")) {
4176
- const rows = db.all(sql.raw(
4408
+ const rows = db.all(sql2.raw(
4177
4409
  `SELECT domain, connection_type, property_id, sitemap_url, access_token, refresh_token, token_expires_at, scopes, created_at, updated_at
4178
4410
  FROM google_connections
4179
4411
  WHERE refresh_token IS NOT NULL AND refresh_token != ''`
@@ -4194,7 +4426,7 @@ function extractLegacyCredentials(db) {
4194
4426
  }
4195
4427
  }
4196
4428
  if (columnExists(db, "ga_connections", "private_key")) {
4197
- const rows = db.all(sql.raw(
4429
+ const rows = db.all(sql2.raw(
4198
4430
  `SELECT p.name AS project_name, ga.property_id, ga.client_email, ga.private_key, ga.created_at, ga.updated_at
4199
4431
  FROM ga_connections ga
4200
4432
  INNER JOIN projects p ON p.id = ga.project_id
@@ -4228,11 +4460,11 @@ function dropLegacyCredentialColumns(db) {
4228
4460
  }
4229
4461
  }
4230
4462
  function getAppliedVersion(db) {
4231
- const rows = db.all(sql`SELECT MAX(version) as max_version FROM _migrations`);
4463
+ const rows = db.all(sql2`SELECT MAX(version) as max_version FROM _migrations`);
4232
4464
  return rows[0]?.max_version ?? 0;
4233
4465
  }
4234
4466
  function recordMigration(db, version, name) {
4235
- db.run(sql`INSERT OR IGNORE INTO _migrations (version, name) VALUES (${version}, ${name})`);
4467
+ db.run(sql2`INSERT OR IGNORE INTO _migrations (version, name) VALUES (${version}, ${name})`);
4236
4468
  }
4237
4469
  function migrate(db, versions = MIGRATION_VERSIONS) {
4238
4470
  db.transaction((tx) => {
@@ -4240,7 +4472,7 @@ function migrate(db, versions = MIGRATION_VERSIONS) {
4240
4472
  });
4241
4473
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
4242
4474
  for (const statement of statements) {
4243
- db.run(sql.raw(statement));
4475
+ db.run(sql2.raw(statement));
4244
4476
  }
4245
4477
  const appliedVersion = getAppliedVersion(db);
4246
4478
  for (const mv of versions) {
@@ -4248,7 +4480,7 @@ function migrate(db, versions = MIGRATION_VERSIONS) {
4248
4480
  db.transaction((tx) => {
4249
4481
  for (const statement of mv.statements) {
4250
4482
  try {
4251
- tx.run(sql.raw(statement));
4483
+ tx.run(sql2.raw(statement));
4252
4484
  } catch (err) {
4253
4485
  if (isDuplicateColumnError(err)) continue;
4254
4486
  throw err;
@@ -6554,11 +6786,11 @@ async function authPlugin(app, opts = {}) {
6554
6786
 
6555
6787
  // ../api-routes/src/projects.ts
6556
6788
  import crypto4 from "crypto";
6557
- import { eq as eq3, sql as sql3 } from "drizzle-orm";
6789
+ import { eq as eq3, sql as sql4 } from "drizzle-orm";
6558
6790
 
6559
6791
  // ../api-routes/src/helpers.ts
6560
6792
  import crypto3 from "crypto";
6561
- import { eq as eq2, ne, sql as sql2 } from "drizzle-orm";
6793
+ import { eq as eq2, ne, sql as sql3 } from "drizzle-orm";
6562
6794
  function notProbeRun() {
6563
6795
  return ne(runs.trigger, RunTriggers.probe);
6564
6796
  }
@@ -6836,6 +7068,7 @@ async function projectRoutes(app, opts) {
6836
7068
  });
6837
7069
  }
6838
7070
  const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks : existing?.autoExtractBacklinks ?? false;
7071
+ const nextMeasurement = body.measurement ?? existing?.measurement ?? DEFAULT_MEASUREMENT_CONFIG;
6839
7072
  const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
6840
7073
  if (existing) {
6841
7074
  const prevAliases = existing.aliases;
@@ -6852,6 +7085,7 @@ async function projectRoutes(app, opts) {
6852
7085
  labels: body.labels ?? {},
6853
7086
  providers: body.providers ?? [],
6854
7087
  providerModels,
7088
+ measurement: nextMeasurement,
6855
7089
  locations: nextLocations,
6856
7090
  defaultLocation: nextDefaultLocation,
6857
7091
  autoExtractBacklinks: nextAutoExtractBacklinks,
@@ -6887,6 +7121,7 @@ async function projectRoutes(app, opts) {
6887
7121
  labels: body.labels ?? {},
6888
7122
  providers: body.providers ?? [],
6889
7123
  providerModels,
7124
+ measurement: nextMeasurement,
6890
7125
  locations: nextLocations,
6891
7126
  defaultLocation: nextDefaultLocation,
6892
7127
  autoExtractBacklinks: nextAutoExtractBacklinks,
@@ -6920,12 +7155,12 @@ async function projectRoutes(app, opts) {
6920
7155
  const project = resolveProject(app.db, request.params.name);
6921
7156
  const pid = project.id;
6922
7157
  const count2 = (n) => n ?? 0;
6923
- const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
6924
- const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
6925
- const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
6926
- const snapshotCount = app.db.select({ n: sql3`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
6927
- const insightCount = app.db.select({ n: sql3`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
6928
- const auditLogCount = app.db.select({ n: sql3`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
7158
+ const queryCount = app.db.select({ n: sql4`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
7159
+ const competitorCount = app.db.select({ n: sql4`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
7160
+ const runCount = app.db.select({ n: sql4`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
7161
+ const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
7162
+ const insightCount = app.db.select({ n: sql4`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
7163
+ const auditLogCount = app.db.select({ n: sql4`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
6929
7164
  return reply.send({
6930
7165
  project: { id: project.id, name: project.name },
6931
7166
  cascadeRows: {
@@ -7059,6 +7294,7 @@ async function projectRoutes(app, opts) {
7059
7294
  competitors: comps.map((c) => c.domain),
7060
7295
  providers: project.providers,
7061
7296
  ...Object.keys(project.providerModels).length > 0 ? { providerModels: project.providerModels } : {},
7297
+ measurement: project.measurement,
7062
7298
  locations: project.locations,
7063
7299
  ...project.defaultLocation ? { defaultLocation: project.defaultLocation } : {},
7064
7300
  ...project.autoExtractBacklinks ? { autoExtractBacklinks: true } : {},
@@ -7107,6 +7343,7 @@ function formatProject(row) {
7107
7343
  labels: row.labels,
7108
7344
  providers: row.providers,
7109
7345
  providerModels: row.providerModels,
7346
+ measurement: row.measurement,
7110
7347
  locations: row.locations,
7111
7348
  defaultLocation: row.defaultLocation,
7112
7349
  autoExtractBacklinks: row.autoExtractBacklinks,
@@ -7126,7 +7363,7 @@ function aliasArraysEqual(a, b) {
7126
7363
 
7127
7364
  // ../api-routes/src/queries.ts
7128
7365
  import crypto6 from "crypto";
7129
- import { and, eq as eq5, inArray as inArray2, sql as sql4 } from "drizzle-orm";
7366
+ import { and, eq as eq5, inArray as inArray2, sql as sql5 } from "drizzle-orm";
7130
7367
 
7131
7368
  // ../api-routes/src/query-replace.ts
7132
7369
  import crypto5 from "crypto";
@@ -7248,9 +7485,9 @@ async function queryRoutes(app, opts) {
7248
7485
  let snapshotsDetached = 0;
7249
7486
  let affectedQueries = 0;
7250
7487
  if (removedIds.length > 0) {
7251
- const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7488
+ const snapshotCount = app.db.select({ n: sql5`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7252
7489
  snapshotsDetached = snapshotCount?.n ?? 0;
7253
- const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7490
+ const distinctAffected = app.db.select({ n: sql5`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
7254
7491
  affectedQueries = distinctAffected?.n ?? 0;
7255
7492
  }
7256
7493
  return reply.send({
@@ -7659,7 +7896,7 @@ function parseCompetitorBatch(value) {
7659
7896
 
7660
7897
  // ../api-routes/src/runs.ts
7661
7898
  import crypto9 from "crypto";
7662
- import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
7899
+ import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql6 } from "drizzle-orm";
7663
7900
  import { gte } from "drizzle-orm";
7664
7901
 
7665
7902
  // ../api-routes/src/run-queue.ts
@@ -7834,7 +8071,7 @@ async function runRoutes(app, opts) {
7834
8071
  });
7835
8072
  app.get("/projects/:name/runs/latest", async (request, reply) => {
7836
8073
  const project = resolveProject(app.db, request.params.name);
7837
- const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
8074
+ const countRow = app.db.select({ count: sql6`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
7838
8075
  const totalRuns = countRow?.count ?? 0;
7839
8076
  const latestRun = app.db.select().from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
7840
8077
  if (!latestRun) {
@@ -8440,6 +8677,7 @@ async function applyRoutes(app, opts) {
8440
8677
  labels: config.metadata.labels,
8441
8678
  providers: config.spec.providers ?? [],
8442
8679
  providerModels,
8680
+ measurement: config.spec.measurement,
8443
8681
  locations: config.spec.locations ?? [],
8444
8682
  defaultLocation: config.spec.defaultLocation ?? null,
8445
8683
  autoExtractBacklinks: config.spec.autoExtractBacklinks ?? false,
@@ -8469,6 +8707,7 @@ async function applyRoutes(app, opts) {
8469
8707
  labels: config.metadata.labels,
8470
8708
  providers: config.spec.providers ?? [],
8471
8709
  providerModels,
8710
+ measurement: config.spec.measurement,
8472
8711
  locations: config.spec.locations ?? [],
8473
8712
  defaultLocation: config.spec.defaultLocation ?? null,
8474
8713
  autoExtractBacklinks: config.spec.autoExtractBacklinks ?? false,
@@ -8594,6 +8833,7 @@ async function applyRoutes(app, opts) {
8594
8833
  labels: project.labels,
8595
8834
  providers: project.providers,
8596
8835
  providerModels: project.providerModels,
8836
+ measurement: project.measurement,
8597
8837
  locations: project.locations,
8598
8838
  defaultLocation: project.defaultLocation,
8599
8839
  autoExtractBacklinks: project.autoExtractBacklinks,
@@ -9931,7 +10171,7 @@ async function intelligenceRoutes(app) {
9931
10171
  }
9932
10172
 
9933
10173
  // ../api-routes/src/report.ts
9934
- import { and as and12, desc as desc7, eq as eq16, gte as gte4, inArray as inArray7, lt as lt2, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
10174
+ import { and as and12, desc as desc7, eq as eq16, gte as gte4, inArray as inArray7, lt as lt2, lte, ne as ne2, or as or3, sql as sql8 } from "drizzle-orm";
9935
10175
 
9936
10176
  // ../api-routes/src/content.ts
9937
10177
  import crypto12 from "crypto";
@@ -10672,7 +10912,7 @@ function winnabilityClassRank(winnabilityClass) {
10672
10912
  }
10673
10913
 
10674
10914
  // ../api-routes/src/gsc-totals.ts
10675
- import { and as and11, asc as asc3, eq as eq15, sql as sql6 } from "drizzle-orm";
10915
+ import { and as and11, asc as asc3, eq as eq15, sql as sql7 } from "drizzle-orm";
10676
10916
  function readGscDailyTotals(db, projectId, startDate, endDate) {
10677
10917
  const rows = db.select({
10678
10918
  date: gscDailyTotals.date,
@@ -10682,8 +10922,8 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
10682
10922
  }).from(gscDailyTotals).where(
10683
10923
  and11(
10684
10924
  eq15(gscDailyTotals.projectId, projectId),
10685
- sql6`${gscDailyTotals.date} >= ${startDate}`,
10686
- sql6`${gscDailyTotals.date} <= ${endDate}`
10925
+ sql7`${gscDailyTotals.date} >= ${startDate}`,
10926
+ sql7`${gscDailyTotals.date} <= ${endDate}`
10687
10927
  )
10688
10928
  ).orderBy(asc3(gscDailyTotals.date)).all();
10689
10929
  return rows.map((r) => {
@@ -10712,8 +10952,8 @@ function readGscQueryDailyRows(db, projectId, startDate, endDate) {
10712
10952
  }).from(gscQueryDailyTotals).where(
10713
10953
  and11(
10714
10954
  eq15(gscQueryDailyTotals.projectId, projectId),
10715
- sql6`${gscQueryDailyTotals.date} >= ${startDate}`,
10716
- sql6`${gscQueryDailyTotals.date} <= ${endDate}`
10955
+ sql7`${gscQueryDailyTotals.date} >= ${startDate}`,
10956
+ sql7`${gscQueryDailyTotals.date} <= ${endDate}`
10717
10957
  )
10718
10958
  ).all();
10719
10959
  return rows.map((r) => {
@@ -13225,7 +13465,7 @@ function buildGaSection(db, projectId, windowDays) {
13225
13465
  const aiSessionRows = db.select({
13226
13466
  trafficClass: gaAiReferrals.trafficClass,
13227
13467
  channelGroup: gaAiReferrals.channelGroup,
13228
- sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
13468
+ sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
13229
13469
  }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.trafficClass, gaAiReferrals.channelGroup).all();
13230
13470
  let paidAiSessions = 0;
13231
13471
  let organicAiSessions = 0;
@@ -13319,8 +13559,8 @@ function buildAiReferrals(db, projectId, windowDays) {
13319
13559
  const window = resolveAiReferralWindow(db, projectId, windowDays);
13320
13560
  const rows = db.select().from(gaAiReferrals).where(and12(
13321
13561
  eq16(gaAiReferrals.projectId, projectId),
13322
- sql7`${gaAiReferrals.date} >= ${window.start}`,
13323
- sql7`${gaAiReferrals.date} <= ${window.end}`
13562
+ sql8`${gaAiReferrals.date} >= ${window.start}`,
13563
+ sql8`${gaAiReferrals.date} <= ${window.end}`
13324
13564
  )).all();
13325
13565
  if (rows.length === 0) return null;
13326
13566
  const dimSessionsByTuple = /* @__PURE__ */ new Map();
@@ -13396,7 +13636,7 @@ function buildAiReferrals(db, projectId, windowDays) {
13396
13636
  };
13397
13637
  }
13398
13638
  function nonSubresourceReferralPathCondition() {
13399
- return sql7`
13639
+ return sql8`
13400
13640
  LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
13401
13641
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
13402
13642
  AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
@@ -13435,7 +13675,7 @@ function buildServerActivity(db, projectId, windowDays) {
13435
13675
  const priorStart = new Date(priorStartMs).toISOString();
13436
13676
  const trendStart = new Date(trendStartMs).toISOString();
13437
13677
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13438
- db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13678
+ db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13439
13679
  and12(
13440
13680
  eq16(crawlerEventsHourly.projectId, projectId),
13441
13681
  eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -13445,7 +13685,7 @@ function buildServerActivity(db, projectId, windowDays) {
13445
13685
  ).get()?.total ?? 0
13446
13686
  );
13447
13687
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13448
- db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13688
+ db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
13449
13689
  and12(
13450
13690
  eq16(crawlerEventsHourly.projectId, projectId),
13451
13691
  ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
@@ -13456,9 +13696,9 @@ function buildServerActivity(db, projectId, windowDays) {
13456
13696
  );
13457
13697
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => {
13458
13698
  const row = db.select({
13459
- total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13460
- paid: sql7`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
13461
- organic: sql7`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
13699
+ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13700
+ paid: sql8`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
13701
+ organic: sql8`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
13462
13702
  }).from(aiReferralEventsHourly).where(
13463
13703
  and12(
13464
13704
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13470,7 +13710,7 @@ function buildServerActivity(db, projectId, windowDays) {
13470
13710
  return aiReferralClassCounts(Number(row?.total ?? 0), Number(row?.paid ?? 0), Number(row?.organic ?? 0));
13471
13711
  };
13472
13712
  const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
13473
- db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
13713
+ db.select({ total: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
13474
13714
  and12(
13475
13715
  eq16(aiUserFetchEventsHourly.projectId, projectId),
13476
13716
  gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
@@ -13489,7 +13729,7 @@ function buildServerActivity(db, projectId, windowDays) {
13489
13729
  const crawlerByOperatorRows = db.select({
13490
13730
  operator: crawlerEventsHourly.operator,
13491
13731
  verificationStatus: crawlerEventsHourly.verificationStatus,
13492
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13732
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13493
13733
  }).from(crawlerEventsHourly).where(
13494
13734
  and12(
13495
13735
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13499,7 +13739,7 @@ function buildServerActivity(db, projectId, windowDays) {
13499
13739
  ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
13500
13740
  const crawlerByOperatorPriorRows = db.select({
13501
13741
  operator: crawlerEventsHourly.operator,
13502
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13742
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13503
13743
  }).from(crawlerEventsHourly).where(
13504
13744
  and12(
13505
13745
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13510,7 +13750,7 @@ function buildServerActivity(db, projectId, windowDays) {
13510
13750
  ).groupBy(crawlerEventsHourly.operator).all();
13511
13751
  const referralByOperatorRows = db.select({
13512
13752
  operator: aiReferralEventsHourly.operator,
13513
- hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13753
+ hits: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13514
13754
  }).from(aiReferralEventsHourly).where(
13515
13755
  and12(
13516
13756
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13521,7 +13761,7 @@ function buildServerActivity(db, projectId, windowDays) {
13521
13761
  ).groupBy(aiReferralEventsHourly.operator).all();
13522
13762
  const userFetchByOperatorRows = db.select({
13523
13763
  operator: aiUserFetchEventsHourly.operator,
13524
- hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13764
+ hits: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13525
13765
  }).from(aiUserFetchEventsHourly).where(
13526
13766
  and12(
13527
13767
  eq16(aiUserFetchEventsHourly.projectId, projectId),
@@ -13564,8 +13804,8 @@ function buildServerActivity(db, projectId, windowDays) {
13564
13804
  );
13565
13805
  const topPathsRows = db.select({
13566
13806
  path: crawlerEventsHourly.pathNormalized,
13567
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
13568
- operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
13807
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
13808
+ operators: sql8`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
13569
13809
  }).from(crawlerEventsHourly).where(
13570
13810
  and12(
13571
13811
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13573,7 +13813,7 @@ function buildServerActivity(db, projectId, windowDays) {
13573
13813
  gte4(crawlerEventsHourly.tsHour, headlineStart),
13574
13814
  lte(crawlerEventsHourly.tsHour, headlineEnd)
13575
13815
  )
13576
- ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13816
+ ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql8`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13577
13817
  const topCrawledPaths = topPathsRows.map((r) => ({
13578
13818
  path: r.path,
13579
13819
  verifiedHits: Number(r.hits),
@@ -13581,8 +13821,8 @@ function buildServerActivity(db, projectId, windowDays) {
13581
13821
  }));
13582
13822
  const referralProductsRows = db.select({
13583
13823
  product: aiReferralEventsHourly.product,
13584
- arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13585
- landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
13824
+ arrivals: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13825
+ landingPaths: sql8`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
13586
13826
  }).from(aiReferralEventsHourly).where(
13587
13827
  and12(
13588
13828
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13590,7 +13830,7 @@ function buildServerActivity(db, projectId, windowDays) {
13590
13830
  gte4(aiReferralEventsHourly.tsHour, headlineStart),
13591
13831
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13592
13832
  )
13593
- ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
13833
+ ).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql8`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
13594
13834
  const referralProducts = referralProductsRows.map((r) => ({
13595
13835
  product: r.product,
13596
13836
  arrivals: Number(r.arrivals),
@@ -13598,8 +13838,8 @@ function buildServerActivity(db, projectId, windowDays) {
13598
13838
  }));
13599
13839
  const topReferralRows = db.select({
13600
13840
  path: aiReferralEventsHourly.landingPathNormalized,
13601
- arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13602
- products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
13841
+ arrivals: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
13842
+ products: sql8`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
13603
13843
  }).from(aiReferralEventsHourly).where(
13604
13844
  and12(
13605
13845
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13607,15 +13847,15 @@ function buildServerActivity(db, projectId, windowDays) {
13607
13847
  gte4(aiReferralEventsHourly.tsHour, headlineStart),
13608
13848
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13609
13849
  )
13610
- ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13850
+ ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql8`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
13611
13851
  const topReferralLandingPaths = topReferralRows.map((r) => ({
13612
13852
  path: r.path,
13613
13853
  arrivals: Number(r.arrivals),
13614
13854
  distinctProducts: Number(r.products)
13615
13855
  }));
13616
13856
  const crawlerTrendRows = db.select({
13617
- date: sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
13618
- hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13857
+ date: sql8`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
13858
+ hits: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
13619
13859
  }).from(crawlerEventsHourly).where(
13620
13860
  and12(
13621
13861
  eq16(crawlerEventsHourly.projectId, projectId),
@@ -13623,10 +13863,10 @@ function buildServerActivity(db, projectId, windowDays) {
13623
13863
  gte4(crawlerEventsHourly.tsHour, trendStart),
13624
13864
  lte(crawlerEventsHourly.tsHour, headlineEnd)
13625
13865
  )
13626
- ).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
13866
+ ).groupBy(sql8`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
13627
13867
  const referralTrendRows = db.select({
13628
- date: sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
13629
- hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13868
+ date: sql8`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
13869
+ hits: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
13630
13870
  }).from(aiReferralEventsHourly).where(
13631
13871
  and12(
13632
13872
  eq16(aiReferralEventsHourly.projectId, projectId),
@@ -13634,17 +13874,17 @@ function buildServerActivity(db, projectId, windowDays) {
13634
13874
  gte4(aiReferralEventsHourly.tsHour, trendStart),
13635
13875
  lte(aiReferralEventsHourly.tsHour, headlineEnd)
13636
13876
  )
13637
- ).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
13877
+ ).groupBy(sql8`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
13638
13878
  const userFetchTrendRows = db.select({
13639
- date: sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
13640
- hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13879
+ date: sql8`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
13880
+ hits: sql8`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
13641
13881
  }).from(aiUserFetchEventsHourly).where(
13642
13882
  and12(
13643
13883
  eq16(aiUserFetchEventsHourly.projectId, projectId),
13644
13884
  gte4(aiUserFetchEventsHourly.tsHour, trendStart),
13645
13885
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
13646
13886
  )
13647
- ).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
13887
+ ).groupBy(sql8`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
13648
13888
  const emptyTrendEntry = () => ({ verifiedCrawlerHits: 0, userFetchHits: 0, referralArrivals: 0 });
13649
13889
  const dailyTrendMap = /* @__PURE__ */ new Map();
13650
13890
  for (const r of crawlerTrendRows) {
@@ -15496,7 +15736,7 @@ async function resultsExportRoutes(app) {
15496
15736
  }
15497
15737
 
15498
15738
  // ../api-routes/src/composites.ts
15499
- 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";
15739
+ import { eq as eq20, and as and16, desc as desc9, sql as sql9, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
15500
15740
  var TOP_INSIGHT_LIMIT = 5;
15501
15741
  var SEARCH_HIT_HARD_LIMIT = 50;
15502
15742
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -15673,9 +15913,9 @@ async function compositeRoutes(app) {
15673
15913
  and16(
15674
15914
  eq20(queries.projectId, project.id),
15675
15915
  or4(
15676
- sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
15677
- sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
15678
- sql8`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
15916
+ sql9`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
15917
+ sql9`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
15918
+ sql9`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
15679
15919
  like2(queries.query, pattern)
15680
15920
  )
15681
15921
  )
@@ -15686,8 +15926,8 @@ async function compositeRoutes(app) {
15686
15926
  or4(
15687
15927
  like2(insights.title, pattern),
15688
15928
  like2(insights.query, pattern),
15689
- sql8`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
15690
- sql8`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
15929
+ sql9`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
15930
+ sql9`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
15691
15931
  )
15692
15932
  )
15693
15933
  ).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
@@ -15893,16 +16133,16 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
15893
16133
  const dimensionedRows = app.db.select({
15894
16134
  date: gscSearchData.date,
15895
16135
  query: gscSearchData.query,
15896
- impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
15897
- clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
16136
+ impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
16137
+ clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
15898
16138
  // Weighted average: SUM(position * impressions) / SUM(impressions).
15899
16139
  // NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
15900
16140
  // which the JS coerces to 0 — caught by the impression floor anyway).
15901
- avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
16141
+ avgPosition: sql9`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
15902
16142
  }).from(gscSearchData).where(and16(
15903
16143
  eq20(gscSearchData.projectId, projectId),
15904
- sql8`${gscSearchData.date} >= ${cutoff}`,
15905
- sql8`${gscSearchData.impressions} > 0`
16144
+ sql9`${gscSearchData.date} >= ${cutoff}`,
16145
+ sql9`${gscSearchData.impressions} > 0`
15906
16146
  )).groupBy(gscSearchData.date, gscSearchData.query).all();
15907
16147
  const todayIso = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15908
16148
  const merged = mergeGscQueryTotalsWithFallback(
@@ -16114,6 +16354,7 @@ function formatProject2(row) {
16114
16354
  labels: row.labels,
16115
16355
  providers: row.providers,
16116
16356
  providerModels: row.providerModels,
16357
+ measurement: row.measurement,
16117
16358
  locations: row.locations,
16118
16359
  defaultLocation: row.defaultLocation,
16119
16360
  autoExtractBacklinks: row.autoExtractBacklinks,
@@ -16265,6 +16506,9 @@ var SCHEMA_TABLE = {
16265
16506
  DiscoveryPromoteResult: discoveryPromoteResultSchema,
16266
16507
  DiscoverySessionDetailDto: discoverySessionDetailDtoSchema,
16267
16508
  DiscoverySessionDto: discoverySessionDtoSchema,
16509
+ ResearchRunCreate: researchRunCreateSchema,
16510
+ ResearchRunDetailDto: researchRunDetailSchema,
16511
+ ResearchRunListDto: researchRunListSchema,
16268
16512
  DoctorReportDto: doctorReportSchema,
16269
16513
  GA4AiReferralHistoryEntry: ga4AiReferralHistoryEntrySchema,
16270
16514
  GA4SessionHistoryEntry: ga4SessionHistoryEntrySchema,
@@ -16767,6 +17011,32 @@ var routeCatalog = [
16767
17011
  404: errorResponse("Project not found.")
16768
17012
  }
16769
17013
  },
17014
+ {
17015
+ method: "post",
17016
+ path: "/api/v1/projects/{name}/research/runs",
17017
+ summary: "Start an isolated research query batch",
17018
+ description: "Runs one to fifty ad-hoc queries through one API provider and saves the answer evidence. Research never creates tracked queries, shared runs, snapshots, insights, or notifications.",
17019
+ tags: ["research"],
17020
+ parameters: [nameParameter],
17021
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResearchRunCreate" } } } },
17022
+ responses: { 200: jsonResponse("Idempotent request returned its existing research run.", "ResearchRunDetailDto"), 202: jsonResponse("Research batch queued.", "ResearchRunDetailDto"), 400: errorResponse("Invalid provider, model, location, or request."), 404: errorResponse("Project not found."), 409: errorResponse("Idempotency key was reused with a different payload."), 422: errorResponse("Research executor is unavailable on this deployment.") }
17023
+ },
17024
+ {
17025
+ method: "get",
17026
+ path: "/api/v1/projects/{name}/research/runs",
17027
+ summary: "List saved research query batches",
17028
+ tags: ["research"],
17029
+ parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }],
17030
+ responses: { 200: jsonResponse("Research runs returned newest first.", "ResearchRunListDto"), 404: errorResponse("Project not found.") }
17031
+ },
17032
+ {
17033
+ method: "get",
17034
+ path: "/api/v1/projects/{name}/research/runs/{runId}",
17035
+ summary: "Get a research query batch and saved answers",
17036
+ tags: ["research"],
17037
+ parameters: [nameParameter, { name: "runId", in: "path", required: true, description: "Research run ID.", schema: stringSchema }],
17038
+ responses: { 200: jsonResponse("Research run detail returned.", "ResearchRunDetailDto"), 404: errorResponse("Project or research run not found.") }
17039
+ },
16770
17040
  {
16771
17041
  method: "post",
16772
17042
  path: "/api/v1/projects/{name}/locations",
@@ -21723,7 +21993,7 @@ function formatNotification(row) {
21723
21993
 
21724
21994
  // ../api-routes/src/google.ts
21725
21995
  import crypto19 from "crypto";
21726
- import { eq as eq24, and as and18, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
21996
+ import { eq as eq24, and as and18, desc as desc11, sql as sql10, inArray as inArray11 } from "drizzle-orm";
21727
21997
 
21728
21998
  // ../api-routes/src/gbp-summary.ts
21729
21999
  function computeMetricTotals(rows) {
@@ -23726,11 +23996,11 @@ async function googleRoutes(app, opts) {
23726
23996
  const { startDate, endDate, query, page, limit, offset } = request.query;
23727
23997
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
23728
23998
  const conditions = [eq24(gscSearchData.projectId, project.id)];
23729
- if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
23730
- else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
23731
- if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
23732
- if (query) conditions.push(sql9`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
23733
- if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
23999
+ if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
24000
+ else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
24001
+ if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
24002
+ if (query) conditions.push(sql10`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
24003
+ if (page) conditions.push(sql10`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
23734
24004
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
23735
24005
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
23736
24006
  const rows = app.db.select().from(gscSearchData).where(and18(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
@@ -23754,13 +24024,13 @@ async function googleRoutes(app, opts) {
23754
24024
  const windowEnd = endDate ?? "9999-12-31";
23755
24025
  const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
23756
24026
  const conditions = [eq24(gscSearchData.projectId, project.id)];
23757
- if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
23758
- else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
23759
- if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
24027
+ if (startDate) conditions.push(sql10`${gscSearchData.date} >= ${startDate}`);
24028
+ else if (cutoffDate) conditions.push(sql10`${gscSearchData.date} >= ${cutoffDate}`);
24029
+ if (endDate) conditions.push(sql10`${gscSearchData.date} <= ${endDate}`);
23760
24030
  const dimensionedRows = app.db.select({
23761
24031
  date: gscSearchData.date,
23762
- clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
23763
- impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
24032
+ clicks: sql10`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
24033
+ impressions: sql10`COALESCE(SUM(${gscSearchData.impressions}), 0)`
23764
24034
  }).from(gscSearchData).where(and18(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
23765
24035
  const daily = mergeGscDailyTotalsWithFallback(
23766
24036
  dailyTotals,
@@ -24656,11 +24926,11 @@ async function googleRoutes(app, opts) {
24656
24926
 
24657
24927
  // ../api-routes/src/ads.ts
24658
24928
  import crypto22 from "crypto";
24659
- import { eq as eq26, and as and20, asc as asc6, gt as gt2, gte as gte6, lt as lt3, lte as lte4, ne as ne3, inArray as inArray12, or as or6, isNull as isNull2, isNotNull as isNotNull2, sql as sql11 } from "drizzle-orm";
24929
+ import { eq as eq26, and as and20, asc as asc6, gt as gt2, gte as gte6, lt as lt3, lte as lte4, ne as ne3, inArray as inArray12, or as or6, isNull as isNull2, isNotNull as isNotNull2, sql as sql12 } from "drizzle-orm";
24660
24930
 
24661
24931
  // ../api-routes/src/ads-activation-routes.ts
24662
24932
  import crypto21 from "crypto";
24663
- import { and as and19, asc as asc5, eq as eq25, gt, isNotNull, isNull, lte as lte3, or as or5, sql as sql10 } from "drizzle-orm";
24933
+ import { and as and19, asc as asc5, eq as eq25, gt, isNotNull, isNull, lte as lte3, or as or5, sql as sql11 } from "drizzle-orm";
24664
24934
 
24665
24935
  // ../api-routes/src/ads-activation.ts
24666
24936
  import crypto20 from "crypto";
@@ -26533,7 +26803,7 @@ function selectWatchdogOperations(app, nowIso, batchSize, allowLiveRevocationLea
26533
26803
  ),
26534
26804
  leaseAvailable
26535
26805
  );
26536
- const freshRevocationPriority = sql10`CASE
26806
+ const freshRevocationPriority = sql11`CASE
26537
26807
  WHEN ${adsActivationGrants.revocationRequestedAt} IS NOT NULL
26538
26808
  AND (
26539
26809
  ${adsOperations.lastReconciledAt} IS NULL
@@ -27629,7 +27899,7 @@ function claimOperationForReconciliation(app, row, leaseOwner, now, leaseMs, pol
27629
27899
  state: AdsOperationStates.reconciling,
27630
27900
  leaseOwner,
27631
27901
  leaseExpiresAt,
27632
- reconcileAttempts: sql11`${adsOperations.reconcileAttempts} + 1`,
27902
+ reconcileAttempts: sql12`${adsOperations.reconcileAttempts} + 1`,
27633
27903
  updatedAt: nowIso
27634
27904
  }).where(and20(
27635
27905
  eq26(adsOperations.id, row.id),
@@ -29701,7 +29971,7 @@ async function cdpRoutes(app, opts) {
29701
29971
 
29702
29972
  // ../api-routes/src/ga.ts
29703
29973
  import crypto24 from "crypto";
29704
- import { eq as eq29, desc as desc13, and as and23, sql as sql12 } from "drizzle-orm";
29974
+ import { eq as eq29, desc as desc13, and as and23, sql as sql13 } from "drizzle-orm";
29705
29975
 
29706
29976
  // ../api-routes/src/ga-session-history.ts
29707
29977
  function buildSessionHistory(landingPageDays, dailyTotals) {
@@ -30088,8 +30358,8 @@ async function ga4Routes(app, opts) {
30088
30358
  tx.delete(gaTrafficSnapshots).where(
30089
30359
  and23(
30090
30360
  eq29(gaTrafficSnapshots.projectId, project.id),
30091
- sql12`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
30092
- sql12`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
30361
+ sql13`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
30362
+ sql13`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
30093
30363
  )
30094
30364
  ).run();
30095
30365
  for (const row of rows) {
@@ -30111,8 +30381,8 @@ async function ga4Routes(app, opts) {
30111
30381
  tx.delete(gaDailyTotals).where(
30112
30382
  and23(
30113
30383
  eq29(gaDailyTotals.projectId, project.id),
30114
- sql12`${gaDailyTotals.date} >= ${summary.periodStart}`,
30115
- sql12`${gaDailyTotals.date} <= ${summary.periodEnd}`
30384
+ sql13`${gaDailyTotals.date} >= ${summary.periodStart}`,
30385
+ sql13`${gaDailyTotals.date} <= ${summary.periodEnd}`
30116
30386
  )
30117
30387
  ).run();
30118
30388
  for (const row of dailyTotals) {
@@ -30131,8 +30401,8 @@ async function ga4Routes(app, opts) {
30131
30401
  tx.delete(gaAiReferrals).where(
30132
30402
  and23(
30133
30403
  eq29(gaAiReferrals.projectId, project.id),
30134
- sql12`${gaAiReferrals.date} >= ${summary.periodStart}`,
30135
- sql12`${gaAiReferrals.date} <= ${summary.periodEnd}`
30404
+ sql13`${gaAiReferrals.date} >= ${summary.periodStart}`,
30405
+ sql13`${gaAiReferrals.date} <= ${summary.periodEnd}`
30136
30406
  )
30137
30407
  ).run();
30138
30408
  for (const row of aiReferrals) {
@@ -30163,8 +30433,8 @@ async function ga4Routes(app, opts) {
30163
30433
  tx.delete(gaSocialReferrals).where(
30164
30434
  and23(
30165
30435
  eq29(gaSocialReferrals.projectId, project.id),
30166
- sql12`${gaSocialReferrals.date} >= ${summary.periodStart}`,
30167
- sql12`${gaSocialReferrals.date} <= ${summary.periodEnd}`
30436
+ sql13`${gaSocialReferrals.date} >= ${summary.periodStart}`,
30437
+ sql13`${gaSocialReferrals.date} <= ${summary.periodEnd}`
30168
30438
  )
30169
30439
  ).run();
30170
30440
  for (const row of socialReferrals) {
@@ -30254,11 +30524,11 @@ async function ga4Routes(app, opts) {
30254
30524
  const cutoff = windowCutoff(window);
30255
30525
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
30256
30526
  const snapshotConditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
30257
- if (cutoffDate) snapshotConditions.push(sql12`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30527
+ if (cutoffDate) snapshotConditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30258
30528
  const aiConditions = [eq29(gaAiReferrals.projectId, project.id)];
30259
- if (cutoffDate) aiConditions.push(sql12`${gaAiReferrals.date} >= ${cutoffDate}`);
30529
+ if (cutoffDate) aiConditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
30260
30530
  const socialConditions = [eq29(gaSocialReferrals.projectId, project.id)];
30261
- if (cutoffDate) socialConditions.push(sql12`${gaSocialReferrals.date} >= ${cutoffDate}`);
30531
+ if (cutoffDate) socialConditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
30262
30532
  const windowSummaryRow = cutoffDate ? app.db.select({
30263
30533
  totalSessions: gaTrafficWindowSummaries.totalSessions,
30264
30534
  totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
@@ -30271,9 +30541,9 @@ async function ga4Routes(app, opts) {
30271
30541
  )
30272
30542
  ).get() : null;
30273
30543
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
30274
- totalSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
30275
- totalOrganicSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
30276
- totalUsers: sql12`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
30544
+ totalSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
30545
+ totalOrganicSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
30546
+ totalUsers: sql13`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
30277
30547
  }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get() : null;
30278
30548
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
30279
30549
  totalSessions: gaTrafficSummaries.totalSessions,
@@ -30281,41 +30551,41 @@ async function ga4Routes(app, opts) {
30281
30551
  totalUsers: gaTrafficSummaries.totalUsers
30282
30552
  }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
30283
30553
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
30284
- totalDirectSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
30554
+ totalDirectSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
30285
30555
  }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).get();
30286
30556
  const summaryMeta = app.db.select({
30287
30557
  periodStart: gaTrafficSummaries.periodStart,
30288
30558
  periodEnd: gaTrafficSummaries.periodEnd
30289
30559
  }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).get();
30290
30560
  const rows = app.db.select({
30291
- landingPage: sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30292
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30293
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30294
- directSessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
30295
- users: sql12`SUM(${gaTrafficSnapshots.users})`
30296
- }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql12`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
30561
+ landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30562
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
30563
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
30564
+ directSessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
30565
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
30566
+ }).from(gaTrafficSnapshots).where(and23(...snapshotConditions)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
30297
30567
  const aiReferralRows = app.db.select({
30298
30568
  source: gaAiReferrals.source,
30299
30569
  medium: gaAiReferrals.medium,
30300
30570
  trafficClass: gaAiReferrals.trafficClass,
30301
30571
  sourceDimension: gaAiReferrals.sourceDimension,
30302
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30303
- users: sql12`SUM(${gaAiReferrals.users})`
30572
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
30573
+ users: sql13`SUM(${gaAiReferrals.users})`
30304
30574
  }).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
30305
30575
  const aiReferralLandingPageRows = app.db.select({
30306
30576
  source: gaAiReferrals.source,
30307
30577
  medium: gaAiReferrals.medium,
30308
30578
  trafficClass: gaAiReferrals.trafficClass,
30309
30579
  sourceDimension: gaAiReferrals.sourceDimension,
30310
- landingPage: sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30311
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30312
- users: sql12`SUM(${gaAiReferrals.users})`
30580
+ landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30581
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
30582
+ users: sql13`SUM(${gaAiReferrals.users})`
30313
30583
  }).from(gaAiReferrals).where(and23(...aiConditions)).groupBy(
30314
30584
  gaAiReferrals.source,
30315
30585
  gaAiReferrals.medium,
30316
30586
  gaAiReferrals.trafficClass,
30317
30587
  gaAiReferrals.sourceDimension,
30318
- sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30588
+ sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30319
30589
  ).all();
30320
30590
  const aiRowsForTotals = app.db.select({
30321
30591
  date: gaAiReferrals.date,
@@ -30340,12 +30610,12 @@ async function ga4Routes(app, opts) {
30340
30610
  source: gaSocialReferrals.source,
30341
30611
  medium: gaSocialReferrals.medium,
30342
30612
  channelGroup: gaSocialReferrals.channelGroup,
30343
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`,
30344
- users: sql12`SUM(${gaSocialReferrals.users})`
30345
- }).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql12`SUM(${gaSocialReferrals.sessions}) DESC`).all();
30613
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
30614
+ users: sql13`SUM(${gaSocialReferrals.users})`
30615
+ }).from(gaSocialReferrals).where(and23(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql13`SUM(${gaSocialReferrals.sessions}) DESC`).all();
30346
30616
  const socialTotals = app.db.select({
30347
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`,
30348
- users: sql12`SUM(${gaSocialReferrals.users})`
30617
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`,
30618
+ users: sql13`SUM(${gaSocialReferrals.users})`
30349
30619
  }).from(gaSocialReferrals).where(and23(...socialConditions)).get();
30350
30620
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq29(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
30351
30621
  const total = summaryRow?.totalSessions ?? 0;
@@ -30446,23 +30716,23 @@ async function ga4Routes(app, opts) {
30446
30716
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30447
30717
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30448
30718
  const conditions = [eq29(gaAiReferrals.projectId, project.id)];
30449
- if (cutoffDate) conditions.push(sql12`${gaAiReferrals.date} >= ${cutoffDate}`);
30719
+ if (cutoffDate) conditions.push(sql13`${gaAiReferrals.date} >= ${cutoffDate}`);
30450
30720
  const rows = app.db.select({
30451
30721
  date: gaAiReferrals.date,
30452
30722
  source: gaAiReferrals.source,
30453
30723
  medium: gaAiReferrals.medium,
30454
30724
  trafficClass: gaAiReferrals.trafficClass,
30455
- landingPage: sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30725
+ landingPage: sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
30456
30726
  sourceDimension: gaAiReferrals.sourceDimension,
30457
- sessions: sql12`SUM(${gaAiReferrals.sessions})`,
30458
- users: sql12`SUM(${gaAiReferrals.users})`
30727
+ sessions: sql13`SUM(${gaAiReferrals.sessions})`,
30728
+ users: sql13`SUM(${gaAiReferrals.users})`
30459
30729
  }).from(gaAiReferrals).where(and23(...conditions)).groupBy(
30460
30730
  gaAiReferrals.date,
30461
30731
  gaAiReferrals.source,
30462
30732
  gaAiReferrals.medium,
30463
30733
  gaAiReferrals.trafficClass,
30464
30734
  gaAiReferrals.sourceDimension,
30465
- sql12`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30735
+ sql13`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
30466
30736
  ).orderBy(gaAiReferrals.date).all();
30467
30737
  return rows;
30468
30738
  });
@@ -30471,7 +30741,7 @@ async function ga4Routes(app, opts) {
30471
30741
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30472
30742
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30473
30743
  const conditions = [eq29(gaSocialReferrals.projectId, project.id)];
30474
- if (cutoffDate) conditions.push(sql12`${gaSocialReferrals.date} >= ${cutoffDate}`);
30744
+ if (cutoffDate) conditions.push(sql13`${gaSocialReferrals.date} >= ${cutoffDate}`);
30475
30745
  const rows = app.db.select({
30476
30746
  date: gaSocialReferrals.date,
30477
30747
  source: gaSocialReferrals.source,
@@ -30492,10 +30762,10 @@ async function ga4Routes(app, opts) {
30492
30762
  d.setDate(d.getDate() - n);
30493
30763
  return fmt(d);
30494
30764
  };
30495
- const sumSocial = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(
30765
+ const sumSocial = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(
30496
30766
  eq29(gaSocialReferrals.projectId, project.id),
30497
- sql12`${gaSocialReferrals.date} >= ${from}`,
30498
- sql12`${gaSocialReferrals.date} < ${to}`
30767
+ sql13`${gaSocialReferrals.date} >= ${from}`,
30768
+ sql13`${gaSocialReferrals.date} < ${to}`
30499
30769
  )).get();
30500
30770
  const current7d = sumSocial(daysAgo(7), fmt(today));
30501
30771
  const prev7d = sumSocial(daysAgo(14), daysAgo(7));
@@ -30504,19 +30774,19 @@ async function ga4Routes(app, opts) {
30504
30774
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
30505
30775
  const sourceCurrent = app.db.select({
30506
30776
  source: gaSocialReferrals.source,
30507
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`
30777
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`
30508
30778
  }).from(gaSocialReferrals).where(and23(
30509
30779
  eq29(gaSocialReferrals.projectId, project.id),
30510
- sql12`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
30511
- sql12`${gaSocialReferrals.date} < ${fmt(today)}`
30780
+ sql13`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
30781
+ sql13`${gaSocialReferrals.date} < ${fmt(today)}`
30512
30782
  )).groupBy(gaSocialReferrals.source).all();
30513
30783
  const sourcePrev = app.db.select({
30514
30784
  source: gaSocialReferrals.source,
30515
- sessions: sql12`SUM(${gaSocialReferrals.sessions})`
30785
+ sessions: sql13`SUM(${gaSocialReferrals.sessions})`
30516
30786
  }).from(gaSocialReferrals).where(and23(
30517
30787
  eq29(gaSocialReferrals.projectId, project.id),
30518
- sql12`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
30519
- sql12`${gaSocialReferrals.date} < ${daysAgo(7)}`
30788
+ sql13`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
30789
+ sql13`${gaSocialReferrals.date} < ${daysAgo(7)}`
30520
30790
  )).groupBy(gaSocialReferrals.source).all();
30521
30791
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
30522
30792
  let biggestMover = null;
@@ -30555,16 +30825,16 @@ async function ga4Routes(app, opts) {
30555
30825
  return fmt(d);
30556
30826
  };
30557
30827
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
30558
- const sumTotal = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30559
- const sumOrganic = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30560
- const sumDirect = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql12`${gaTrafficSnapshots.date} >= ${from}`, sql12`${gaTrafficSnapshots.date} < ${to}`)).get();
30561
- const sumAi = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30828
+ const sumTotal = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
30829
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
30830
+ const sumDirect = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and23(eq29(gaTrafficSnapshots.projectId, project.id), sql13`${gaTrafficSnapshots.date} >= ${from}`, sql13`${gaTrafficSnapshots.date} < ${to}`)).get();
30831
+ const sumAi = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30562
30832
  eq29(gaAiReferrals.projectId, project.id),
30563
- sql12`${gaAiReferrals.date} >= ${from}`,
30564
- sql12`${gaAiReferrals.date} < ${to}`,
30833
+ sql13`${gaAiReferrals.date} >= ${from}`,
30834
+ sql13`${gaAiReferrals.date} < ${to}`,
30565
30835
  eq29(gaAiReferrals.sourceDimension, "session")
30566
30836
  )).get();
30567
- const sumSocial = (from, to) => app.db.select({ sessions: sql12`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${from}`, sql12`${gaSocialReferrals.date} < ${to}`)).get();
30837
+ const sumSocial = (from, to) => app.db.select({ sessions: sql13`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${from}`, sql13`${gaSocialReferrals.date} < ${to}`)).get();
30568
30838
  const todayStr = fmt(today);
30569
30839
  const buildTrend = (sum) => {
30570
30840
  const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
@@ -30573,16 +30843,16 @@ async function ga4Routes(app, opts) {
30573
30843
  const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
30574
30844
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
30575
30845
  };
30576
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30846
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30577
30847
  eq29(gaAiReferrals.projectId, project.id),
30578
- sql12`${gaAiReferrals.date} >= ${daysAgo(7)}`,
30579
- sql12`${gaAiReferrals.date} < ${todayStr}`,
30848
+ sql13`${gaAiReferrals.date} >= ${daysAgo(7)}`,
30849
+ sql13`${gaAiReferrals.date} < ${todayStr}`,
30580
30850
  eq29(gaAiReferrals.sourceDimension, "session")
30581
30851
  )).groupBy(gaAiReferrals.source).all();
30582
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql12`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30852
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql13`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and23(
30583
30853
  eq29(gaAiReferrals.projectId, project.id),
30584
- sql12`${gaAiReferrals.date} >= ${daysAgo(14)}`,
30585
- sql12`${gaAiReferrals.date} < ${daysAgo(7)}`,
30854
+ sql13`${gaAiReferrals.date} >= ${daysAgo(14)}`,
30855
+ sql13`${gaAiReferrals.date} < ${daysAgo(7)}`,
30586
30856
  eq29(gaAiReferrals.sourceDimension, "session")
30587
30857
  )).groupBy(gaAiReferrals.source).all();
30588
30858
  const findBiggestMover = (current, prev) => {
@@ -30599,8 +30869,8 @@ async function ga4Routes(app, opts) {
30599
30869
  }
30600
30870
  return mover;
30601
30871
  };
30602
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql12`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql12`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
30603
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql12`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql12`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql12`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
30872
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql13`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql13`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
30873
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql13`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and23(eq29(gaSocialReferrals.projectId, project.id), sql13`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql13`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
30604
30874
  return {
30605
30875
  total: buildTrend(sumTotal),
30606
30876
  organic: buildTrend(sumOrganic),
@@ -30616,15 +30886,15 @@ async function ga4Routes(app, opts) {
30616
30886
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30617
30887
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
30618
30888
  const conditions = [eq29(gaTrafficSnapshots.projectId, project.id)];
30619
- if (cutoffDate) conditions.push(sql12`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30889
+ if (cutoffDate) conditions.push(sql13`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
30620
30890
  const rows = app.db.select({
30621
30891
  date: gaTrafficSnapshots.date,
30622
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30623
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30624
- users: sql12`SUM(${gaTrafficSnapshots.users})`
30892
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
30893
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
30894
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
30625
30895
  }).from(gaTrafficSnapshots).where(and23(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
30626
30896
  const totalConditions = [eq29(gaDailyTotals.projectId, project.id)];
30627
- if (cutoffDate) totalConditions.push(sql12`${gaDailyTotals.date} >= ${cutoffDate}`);
30897
+ if (cutoffDate) totalConditions.push(sql13`${gaDailyTotals.date} >= ${cutoffDate}`);
30628
30898
  const totals = app.db.select({ date: gaDailyTotals.date, users: gaDailyTotals.users }).from(gaDailyTotals).where(and23(...totalConditions)).all();
30629
30899
  return buildSessionHistory(
30630
30900
  rows.map((r) => ({
@@ -30640,11 +30910,11 @@ async function ga4Routes(app, opts) {
30640
30910
  const project = resolveProject(app.db, request.params.name);
30641
30911
  requireGa4Connection(opts, project.name, project.canonicalDomain);
30642
30912
  const trafficPages = app.db.select({
30643
- landingPage: sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30644
- sessions: sql12`SUM(${gaTrafficSnapshots.sessions})`,
30645
- organicSessions: sql12`SUM(${gaTrafficSnapshots.organicSessions})`,
30646
- users: sql12`SUM(${gaTrafficSnapshots.users})`
30647
- }).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(sql12`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql12`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
30913
+ landingPage: sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
30914
+ sessions: sql13`SUM(${gaTrafficSnapshots.sessions})`,
30915
+ organicSessions: sql13`SUM(${gaTrafficSnapshots.organicSessions})`,
30916
+ users: sql13`SUM(${gaTrafficSnapshots.users})`
30917
+ }).from(gaTrafficSnapshots).where(eq29(gaTrafficSnapshots.projectId, project.id)).groupBy(sql13`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql13`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
30648
30918
  return {
30649
30919
  pages: trafficPages.map((r) => ({
30650
30920
  landingPage: r.landingPage,
@@ -31752,9 +32022,9 @@ async function wordpressRoutes(app, opts) {
31752
32022
  throw validationError("WordPress connection storage is not configured for this deployment");
31753
32023
  }
31754
32024
  async function assertWordpressUrlAllowed(rawUrl, field) {
31755
- const check = await resolveWebhookTarget(rawUrl, { allowLoopback });
31756
- if (!check.ok) {
31757
- throw validationError(`${field} ${check.message.replace(/^"url" /, "")}`);
32025
+ const check2 = await resolveWebhookTarget(rawUrl, { allowLoopback });
32026
+ if (!check2.ok) {
32027
+ throw validationError(`${field} ${check2.message.replace(/^"url" /, "")}`);
31758
32028
  }
31759
32029
  }
31760
32030
  function requireConnection(store, projectName) {
@@ -32292,7 +32562,7 @@ async function wordpressRoutes(app, opts) {
32292
32562
 
32293
32563
  // ../api-routes/src/backlinks.ts
32294
32564
  import crypto26 from "crypto";
32295
- import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql13 } from "drizzle-orm";
32565
+ import { and as and25, asc as asc7, desc as desc14, eq as eq30, sql as sql14 } from "drizzle-orm";
32296
32566
 
32297
32567
  // ../integration-commoncrawl/src/constants.ts
32298
32568
  import os2 from "os";
@@ -32582,7 +32852,7 @@ async function queryBacklinks(opts) {
32582
32852
  const reversed = opts.targets.map(reverseDomain);
32583
32853
  const targetList = reversed.map(quote).join(", ");
32584
32854
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
32585
- const sql16 = `
32855
+ const sql17 = `
32586
32856
  WITH vertices AS (
32587
32857
  SELECT * FROM read_csv(
32588
32858
  ${quote(opts.vertexPath)},
@@ -32618,7 +32888,7 @@ async function queryBacklinks(opts) {
32618
32888
  const conn = await instance.connect();
32619
32889
  let rows;
32620
32890
  try {
32621
- const reader = await conn.runAndReadAll(sql16);
32891
+ const reader = await conn.runAndReadAll(sql17);
32622
32892
  rows = reader.getRowObjects();
32623
32893
  } finally {
32624
32894
  conn.disconnectSync?.();
@@ -32808,12 +33078,12 @@ function computeFilteredSummary(db, base) {
32808
33078
  );
32809
33079
  const filteredCondition = and25(baseDomainCondition, backlinkCrawlerExclusionClause());
32810
33080
  const unfilteredAgg = db.select({
32811
- count: sql13`count(*)`,
32812
- total: sql13`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33081
+ count: sql14`count(*)`,
33082
+ total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
32813
33083
  }).from(backlinkDomains).where(baseDomainCondition).get();
32814
33084
  const filteredAgg = db.select({
32815
- count: sql13`count(*)`,
32816
- total: sql13`coalesce(sum(${backlinkDomains.numHosts}), 0)`
33085
+ count: sql14`count(*)`,
33086
+ total: sql14`coalesce(sum(${backlinkDomains.numHosts}), 0)`
32817
33087
  }).from(backlinkDomains).where(filteredCondition).get();
32818
33088
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
32819
33089
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -32839,7 +33109,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
32839
33109
  const summary = db.select().from(backlinkSummaries).where(and25(eq30(backlinkSummaries.projectId, projectId), eq30(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
32840
33110
  let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
32841
33111
  if (summary && excludeCrawlers) {
32842
- const filtered = db.select({ count: sql13`count(*)` }).from(backlinkDomains).where(and25(
33112
+ const filtered = db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(and25(
32843
33113
  eq30(backlinkDomains.projectId, projectId),
32844
33114
  eq30(backlinkDomains.source, source),
32845
33115
  eq30(backlinkDomains.release, summary.release),
@@ -33028,7 +33298,7 @@ async function backlinksRoutes(app, opts) {
33028
33298
  eq30(backlinkDomains.release, targetRelease)
33029
33299
  );
33030
33300
  const domainCondition = excludeCrawlers ? and25(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
33031
- const totalRow = app.db.select({ count: sql13`count(*)` }).from(backlinkDomains).where(domainCondition).get();
33301
+ const totalRow = app.db.select({ count: sql14`count(*)` }).from(backlinkDomains).where(domainCondition).get();
33032
33302
  const rows = app.db.select({
33033
33303
  linkingDomain: backlinkDomains.linkingDomain,
33034
33304
  numHosts: backlinkDomains.numHosts,
@@ -33107,7 +33377,7 @@ async function backlinksRoutes(app, opts) {
33107
33377
  // ../api-routes/src/traffic.ts
33108
33378
  import crypto28 from "crypto";
33109
33379
  import { Agent as UndiciAgent } from "undici";
33110
- import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql14 } from "drizzle-orm";
33380
+ import { and as and26, desc as desc15, eq as eq31, gte as gte7, lte as lte5, sql as sql15 } from "drizzle-orm";
33111
33381
 
33112
33382
  // ../integration-cloud-run/src/auth.ts
33113
33383
  import crypto27 from "crypto";
@@ -37168,11 +37438,11 @@ async function trafficRoutes(app, opts) {
37168
37438
  const pullVercelEvents = opts.pullVercelTrafficEvents ?? listVercelTrafficEvents;
37169
37439
  const allowLoopback = opts.allowLoopbackWebhooks === true;
37170
37440
  async function assertWordpressTargetAllowed(baseUrl) {
37171
- const check = await resolveWebhookTarget(baseUrl, { allowLoopback });
37172
- if (!check.ok) {
37173
- throw validationError(`WordPress baseUrl rejected: ${check.message}`);
37441
+ const check2 = await resolveWebhookTarget(baseUrl, { allowLoopback });
37442
+ if (!check2.ok) {
37443
+ throw validationError(`WordPress baseUrl rejected: ${check2.message}`);
37174
37444
  }
37175
- const { address, family } = check.target;
37445
+ const { address, family } = check2.target;
37176
37446
  return new UndiciAgent({
37177
37447
  connect: {
37178
37448
  lookup: (_hostname, options, cb) => {
@@ -37808,7 +38078,7 @@ async function trafficRoutes(app, opts) {
37808
38078
  crawlerEventsHourly.status
37809
38079
  ],
37810
38080
  set: {
37811
- hits: sql14`${crawlerEventsHourly.hits} + ${bucket.hits}`,
38081
+ hits: sql15`${crawlerEventsHourly.hits} + ${bucket.hits}`,
37812
38082
  sampledUserAgent: bucket.sampledUserAgent,
37813
38083
  updatedAt: finishedAt
37814
38084
  }
@@ -37841,7 +38111,7 @@ async function trafficRoutes(app, opts) {
37841
38111
  aiUserFetchEventsHourly.status
37842
38112
  ],
37843
38113
  set: {
37844
- hits: sql14`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
38114
+ hits: sql15`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
37845
38115
  sampledUserAgent: bucket.sampledUserAgent,
37846
38116
  updatedAt: finishedAt
37847
38117
  }
@@ -37881,9 +38151,9 @@ async function trafficRoutes(app, opts) {
37881
38151
  // colliding bucket adds its paid and organic arrivals to the ones
37882
38152
  // already there rather than overwriting a single label.
37883
38153
  set: {
37884
- sessionsOrHits: sql14`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
37885
- paidSessionsOrHits: sql14`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
37886
- organicSessionsOrHits: sql14`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
38154
+ sessionsOrHits: sql15`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
38155
+ paidSessionsOrHits: sql15`${aiReferralEventsHourly.paidSessionsOrHits} + ${bucket.paidHits}`,
38156
+ organicSessionsOrHits: sql15`${aiReferralEventsHourly.organicSessionsOrHits} + ${bucket.organicHits}`,
37887
38157
  updatedAt: finishedAt
37888
38158
  }
37889
38159
  }).run();
@@ -38174,7 +38444,7 @@ async function trafficRoutes(app, opts) {
38174
38444
  function buildSourceDetail(projectId, row, since) {
38175
38445
  const crawlerPathRows = app.db.select({
38176
38446
  pathNormalized: crawlerEventsHourly.pathNormalized,
38177
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38447
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38178
38448
  }).from(crawlerEventsHourly).where(
38179
38449
  and26(
38180
38450
  eq31(crawlerEventsHourly.sourceId, row.id),
@@ -38185,19 +38455,19 @@ async function trafficRoutes(app, opts) {
38185
38455
  crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38186
38456
  );
38187
38457
  const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38188
- const aiUserFetchTotals = app.db.select({ total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
38458
+ const aiUserFetchTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
38189
38459
  and26(
38190
38460
  eq31(aiUserFetchEventsHourly.sourceId, row.id),
38191
38461
  gte7(aiUserFetchEventsHourly.tsHour, since)
38192
38462
  )
38193
38463
  ).get();
38194
- const aiTotals = app.db.select({ total: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
38464
+ const aiTotals = app.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
38195
38465
  and26(
38196
38466
  eq31(aiReferralEventsHourly.sourceId, row.id),
38197
38467
  gte7(aiReferralEventsHourly.tsHour, since)
38198
38468
  )
38199
38469
  ).get();
38200
- const sampleTotals = app.db.select({ total: sql14`COUNT(*)` }).from(rawEventSamples).where(
38470
+ const sampleTotals = app.db.select({ total: sql15`COUNT(*)` }).from(rawEventSamples).where(
38201
38471
  and26(
38202
38472
  eq31(rawEventSamples.sourceId, row.id),
38203
38473
  gte7(rawEventSamples.ts, since)
@@ -38359,18 +38629,18 @@ async function trafficRoutes(app, opts) {
38359
38629
  const crawlerWhere = and26(...crawlerFilters);
38360
38630
  const pathTotals = app.db.select({
38361
38631
  pathNormalized: crawlerEventsHourly.pathNormalized,
38362
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38363
- rows: sql14`COUNT(*)`
38632
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
38633
+ rows: sql15`COUNT(*)`
38364
38634
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
38365
38635
  crawlerSegments = segmentCrawlerHits(
38366
38636
  pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
38367
38637
  );
38368
38638
  crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
38369
38639
  totalEventRows += pathTotals.reduce((sum, row) => sum + Number(row.rows), 0);
38370
- const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38640
+ const crawlerSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${crawlerEventsHourly.tsHour}, 1, 10)` : crawlerEventsHourly.tsHour;
38371
38641
  const crawlerSeries = app.db.select({
38372
38642
  bucket: crawlerSeriesBucket,
38373
- hits: sql14`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38643
+ hits: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
38374
38644
  }).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerSeriesBucket).all();
38375
38645
  for (const row of crawlerSeries) {
38376
38646
  trafficSeriesPoint(seriesByBucket, row.bucket).crawlerHits = Number(row.hits);
@@ -38400,15 +38670,15 @@ async function trafficRoutes(app, opts) {
38400
38670
  if (sourceIdParam) userFetchFilters.push(eq31(aiUserFetchEventsHourly.sourceId, sourceIdParam));
38401
38671
  const userFetchWhere = and26(...userFetchFilters);
38402
38672
  const total = app.db.select({
38403
- total: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38404
- rows: sql14`COUNT(*)`
38673
+ total: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`,
38674
+ rows: sql15`COUNT(*)`
38405
38675
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
38406
38676
  aiUserFetchTotal = Number(total?.total ?? 0);
38407
38677
  totalEventRows += Number(total?.rows ?? 0);
38408
- const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38678
+ const userFetchSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiUserFetchEventsHourly.tsHour}, 1, 10)` : aiUserFetchEventsHourly.tsHour;
38409
38679
  const userFetchSeries = app.db.select({
38410
38680
  bucket: userFetchSeriesBucket,
38411
- hits: sql14`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38681
+ hits: sql15`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
38412
38682
  }).from(aiUserFetchEventsHourly).where(userFetchWhere).groupBy(userFetchSeriesBucket).all();
38413
38683
  for (const row of userFetchSeries) {
38414
38684
  trafficSeriesPoint(seriesByBucket, row.bucket).aiUserFetchHits = Number(row.hits);
@@ -38437,10 +38707,10 @@ async function trafficRoutes(app, opts) {
38437
38707
  if (sourceIdParam) aiFilters.push(eq31(aiReferralEventsHourly.sourceId, sourceIdParam));
38438
38708
  const aiWhere = and26(...aiFilters);
38439
38709
  const total = app.db.select({
38440
- total: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
38441
- paid: sql14`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
38442
- organic: sql14`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
38443
- rows: sql14`COUNT(*)`
38710
+ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
38711
+ paid: sql15`COALESCE(SUM(${aiReferralEventsHourly.paidSessionsOrHits}), 0)`,
38712
+ organic: sql15`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`,
38713
+ rows: sql15`COUNT(*)`
38444
38714
  }).from(aiReferralEventsHourly).where(aiWhere).get();
38445
38715
  aiReferralCounts = aiReferralClassCounts(
38446
38716
  Number(total?.total ?? 0),
@@ -38448,10 +38718,10 @@ async function trafficRoutes(app, opts) {
38448
38718
  Number(total?.organic ?? 0)
38449
38719
  );
38450
38720
  totalEventRows += Number(total?.rows ?? 0);
38451
- const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql14`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
38721
+ const referralSeriesBucket = granularity === TrafficSeriesGranularities.day ? sql15`substr(${aiReferralEventsHourly.tsHour}, 1, 10)` : aiReferralEventsHourly.tsHour;
38452
38722
  const referralSeries = app.db.select({
38453
38723
  bucket: referralSeriesBucket,
38454
- hits: sql14`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
38724
+ hits: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
38455
38725
  }).from(aiReferralEventsHourly).where(aiWhere).groupBy(referralSeriesBucket).all();
38456
38726
  for (const row of referralSeries) {
38457
38727
  trafficSeriesPoint(seriesByBucket, row.bucket).aiReferralHits = Number(row.hits);
@@ -39049,7 +39319,7 @@ var winnabilityCoverageCheck = {
39049
39319
  };
39050
39320
  var CONTENT_CHECKS = [winnabilityCoverageCheck];
39051
39321
  var CONTENT_CHECK_BY_ID = Object.fromEntries(
39052
- CONTENT_CHECKS.map((check) => [check.id, check])
39322
+ CONTENT_CHECKS.map((check2) => [check2.id, check2])
39053
39323
  );
39054
39324
 
39055
39325
  // ../api-routes/src/doctor/checks/ads.ts
@@ -39600,7 +39870,7 @@ var GBP_AUTH_CHECKS = [
39600
39870
  recentSyncCheck
39601
39871
  ];
39602
39872
  var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
39603
- GBP_AUTH_CHECKS.map((check) => [check.id, check])
39873
+ GBP_AUTH_CHECKS.map((check2) => [check2.id, check2])
39604
39874
  );
39605
39875
 
39606
39876
  // ../api-routes/src/doctor/checks/places.ts
@@ -39678,7 +39948,7 @@ var apiKeyCheck = {
39678
39948
  };
39679
39949
  var PLACES_CHECKS = [apiKeyCheck];
39680
39950
  var PLACES_CHECK_BY_ID = Object.fromEntries(
39681
- PLACES_CHECKS.map((check) => [check.id, check])
39951
+ PLACES_CHECKS.map((check2) => [check2.id, check2])
39682
39952
  );
39683
39953
 
39684
39954
  // ../api-routes/src/doctor/checks/google-auth.ts
@@ -39985,7 +40255,7 @@ var GOOGLE_AUTH_CHECKS = [
39985
40255
  scopesCheck2
39986
40256
  ];
39987
40257
  var GOOGLE_AUTH_CHECK_BY_ID = Object.fromEntries(
39988
- GOOGLE_AUTH_CHECKS.map((check) => [check.id, check])
40258
+ GOOGLE_AUTH_CHECKS.map((check2) => [check2.id, check2])
39989
40259
  );
39990
40260
 
39991
40261
  // ../api-routes/src/doctor/checks/providers.ts
@@ -40146,7 +40416,7 @@ var RUNTIME_STATE_CHECKS = [
40146
40416
  ];
40147
40417
 
40148
40418
  // ../api-routes/src/doctor/checks/traffic-source.ts
40149
- import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql15 } from "drizzle-orm";
40419
+ import { and as and29, eq as eq37, gte as gte8, ne as ne5, sql as sql16 } from "drizzle-orm";
40150
40420
  var RECENT_DATA_WARN_DAYS = 7;
40151
40421
  var RECENT_DATA_FAIL_DAYS = 30;
40152
40422
  function skippedNoProject5() {
@@ -40240,7 +40510,7 @@ var recentDataCheck = {
40240
40510
  const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
40241
40511
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
40242
40512
  const recentCrawlers = Number(
40243
- ctx.db.select({ total: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40513
+ ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40244
40514
  and29(
40245
40515
  eq37(crawlerEventsHourly.projectId, ctx.project.id),
40246
40516
  gte8(crawlerEventsHourly.tsHour, warnCutoff)
@@ -40248,7 +40518,7 @@ var recentDataCheck = {
40248
40518
  ).get()?.total ?? 0
40249
40519
  );
40250
40520
  const recentReferrals = Number(
40251
- ctx.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40521
+ ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40252
40522
  and29(
40253
40523
  eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40254
40524
  gte8(aiReferralEventsHourly.tsHour, warnCutoff)
@@ -40264,7 +40534,7 @@ var recentDataCheck = {
40264
40534
  };
40265
40535
  }
40266
40536
  const olderCrawlers = Number(
40267
- ctx.db.select({ total: sql15`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40537
+ ctx.db.select({ total: sql16`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
40268
40538
  and29(
40269
40539
  eq37(crawlerEventsHourly.projectId, ctx.project.id),
40270
40540
  gte8(crawlerEventsHourly.tsHour, failCutoff)
@@ -40272,7 +40542,7 @@ var recentDataCheck = {
40272
40542
  ).get()?.total ?? 0
40273
40543
  );
40274
40544
  const olderReferrals = Number(
40275
- ctx.db.select({ total: sql15`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40545
+ ctx.db.select({ total: sql16`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
40276
40546
  and29(
40277
40547
  eq37(aiReferralEventsHourly.projectId, ctx.project.id),
40278
40548
  gte8(aiReferralEventsHourly.tsHour, failCutoff)
@@ -40584,9 +40854,9 @@ async function runChecks(ctx, checks, options = {}) {
40584
40854
  const filters = options.checkIds ?? [];
40585
40855
  const targetScope = ctx.project ? CheckScopes.project : CheckScopes.global;
40586
40856
  const projectName = ctx.project?.name ?? null;
40587
- const selected = checks.filter((check) => {
40588
- if (check.scope !== targetScope) return false;
40589
- return matchesCheckId(check.id, filters);
40857
+ const selected = checks.filter((check2) => {
40858
+ if (check2.scope !== targetScope) return false;
40859
+ return matchesCheckId(check2.id, filters);
40590
40860
  });
40591
40861
  const results = [];
40592
40862
  for (const definition of selected) {
@@ -41524,6 +41794,80 @@ async function technicalAeoRoutes(app, opts) {
41524
41794
  });
41525
41795
  }
41526
41796
 
41797
+ // ../api-routes/src/research.ts
41798
+ import crypto33 from "crypto";
41799
+ import { and as and32, desc as desc18, eq as eq41 } from "drizzle-orm";
41800
+ var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
41801
+ async function researchRoutes(app, opts) {
41802
+ app.post("/projects/:name/research/runs", async (request, reply) => {
41803
+ const project = resolveProject(app.db, request.params.name);
41804
+ if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
41805
+ const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
41806
+ if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
41807
+ const input = parsed.data;
41808
+ const adapters = opts.providerAdapters ?? [];
41809
+ const configured = new Set(opts.configuredProviderNames ?? []);
41810
+ const providerName = input.provider ?? project.providers.find((name) => configured.has(name) && adapters.some((adapter2) => adapter2.name === name && adapter2.mode === "api")) ?? adapters.find((adapter2) => adapter2.mode === "api" && configured.has(adapter2.name))?.name;
41811
+ const adapter = adapters.find((candidate) => candidate.name === providerName);
41812
+ if (!providerName || !adapter || adapter.mode !== "api" || isBrowserProvider(providerName) || !configured.has(providerName)) throw validationError("Research requires a configured API provider.", { provider: input.provider, validProviders: adapters.filter((a) => a.mode === "api" && configured.has(a.name)).map((a) => a.name) });
41813
+ if (input.model) {
41814
+ adapter.modelValidationPattern.lastIndex = 0;
41815
+ if (!adapter.modelConfigurable || !adapter.modelValidationPattern.test(input.model)) throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
41816
+ }
41817
+ const location = input.location === void 0 ? project.defaultLocation ? project.locations.find((item) => item.label === project.defaultLocation) ?? null : null : input.location;
41818
+ if (location && !project.locations.some((item) => sameLocation(item, location))) throw validationError("Research location must exactly match a configured project location.", { location });
41819
+ const requestedModel = input.model ?? null;
41820
+ const resolvedModel = requestedModel ?? (project.providerModels[providerName] || adapter.defaultModel);
41821
+ adapter.modelValidationPattern.lastIndex = 0;
41822
+ if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError(`Invalid resolved model "${resolvedModel}" for provider "${providerName}".`, { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
41823
+ if (new Set(input.queries.map((query) => query.toLocaleLowerCase())).size !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
41824
+ const normalized = { queries: input.queries, provider: providerName, model: requestedModel, location: location ?? null };
41825
+ const requestHash2 = crypto33.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
41826
+ const now = (/* @__PURE__ */ new Date()).toISOString();
41827
+ const decision = app.db.transaction((tx) => {
41828
+ if (input.idempotencyKey) {
41829
+ const existing = tx.select().from(researchRuns).where(and32(eq41(researchRuns.projectId, project.id), eq41(researchRuns.idempotencyKey, input.idempotencyKey))).get();
41830
+ if (existing) {
41831
+ if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
41832
+ return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
41833
+ }
41834
+ }
41835
+ const id = crypto33.randomUUID();
41836
+ tx.insert(researchRuns).values({ id, projectId: project.id, status: ResearchRunStatuses.queued, provider: providerName, requestedModel, resolvedModel, location: location ?? null, totalQueries: input.queries.length, idempotencyKey: input.idempotencyKey ?? null, requestHash: input.idempotencyKey ? requestHash2 : null, createdAt: now }).run();
41837
+ for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto33.randomUUID(), researchRunId: id, position, queryText: query, status: ResearchQueryStatuses.queued, requestedModel, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
41838
+ writeAuditLog(tx, { projectId: project.id, actor: "api", action: "research.created", entityType: "research_run", entityId: id });
41839
+ return { reused: false, id, shouldDispatch: true };
41840
+ });
41841
+ const result = getDetail(app, project.id, decision.id);
41842
+ if (decision.shouldDispatch) opts.onResearchRunRequested(decision.id, project.id);
41843
+ if (decision.reused) return reply.status(200).send(result);
41844
+ return reply.status(202).send(result);
41845
+ });
41846
+ app.get("/projects/:name/research/runs", async (request) => {
41847
+ const project = resolveProject(app.db, request.params.name);
41848
+ const requested = Number.parseInt(request.query.limit ?? "", 10);
41849
+ const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
41850
+ const runs2 = app.db.select().from(researchRuns).where(eq41(researchRuns.projectId, project.id)).orderBy(desc18(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
41851
+ return { runs: runs2 };
41852
+ });
41853
+ app.get("/projects/:name/research/runs/:runId", async (request) => {
41854
+ const project = resolveProject(app.db, request.params.name);
41855
+ return getDetail(app, project.id, request.params.runId);
41856
+ });
41857
+ }
41858
+ function getDetail(app, projectId, id) {
41859
+ const row = app.db.select().from(researchRuns).where(and32(eq41(researchRuns.id, id), eq41(researchRuns.projectId, projectId))).get();
41860
+ if (!row) throw notFound("Research run", id);
41861
+ const queries2 = app.db.select().from(researchRunQueries).where(eq41(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
41862
+ return { ...serializeRun(row), queries: queries2 };
41863
+ }
41864
+ function serializeRun(row) {
41865
+ return { id: row.id, projectId: row.projectId, status: row.status, provider: row.provider, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, location: row.location ?? null, totalQueries: row.totalQueries, completedQueries: row.completedQueries, failedQueries: row.failedQueries, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
41866
+ }
41867
+ function serializeQuery(row) {
41868
+ return { id: row.id, position: row.position, query: row.queryText, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
41869
+ }
41870
+
41527
41871
  // ../api-routes/src/index.ts
41528
41872
  async function apiRoutes(app, opts) {
41529
41873
  app.decorate("db", opts.db);
@@ -41710,6 +42054,11 @@ async function apiRoutes(app, opts) {
41710
42054
  harvestSearchQueries: opts.harvestSearchQueries,
41711
42055
  embedQueries: opts.embedQueries
41712
42056
  });
42057
+ await api.register(researchRoutes, {
42058
+ providerAdapters: opts.providerAdapters,
42059
+ configuredProviderNames: opts.providerSummary?.filter((provider) => provider.configured).map((provider) => provider.name),
42060
+ onResearchRunRequested: opts.onResearchRunRequested
42061
+ });
41713
42062
  await api.register(technicalAeoRoutes, {
41714
42063
  onSiteAuditRequested: opts.onSiteAuditRequested
41715
42064
  });
@@ -41871,7 +42220,7 @@ function buildTrafficSourceValidators(opts) {
41871
42220
  }
41872
42221
 
41873
42222
  // src/intelligence-service.ts
41874
- import crypto33 from "crypto";
42223
+ import crypto34 from "crypto";
41875
42224
 
41876
42225
  // src/logger.ts
41877
42226
  var IS_TTY = process.stdout.isTTY === true;
@@ -42103,16 +42452,16 @@ var IntelligenceService = class {
42103
42452
  */
42104
42453
  analyzeAndPersist(runId, projectId) {
42105
42454
  const recentRuns = this.db.select().from(runs).where(
42106
- and32(
42107
- eq41(runs.projectId, projectId),
42108
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
42455
+ and33(
42456
+ eq42(runs.projectId, projectId),
42457
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42109
42458
  // Defensive: RunCoordinator already skips probes before this is
42110
42459
  // called, but if a future call site invokes analyzeAndPersist
42111
42460
  // directly for a probe, probes still must not pollute the
42112
42461
  // intelligence window.
42113
42462
  ne6(runs.trigger, RunTriggers.probe)
42114
42463
  )
42115
- ).orderBy(desc18(runs.finishedAt), desc18(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
42464
+ ).orderBy(desc19(runs.finishedAt), desc19(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
42116
42465
  if (recentRuns.length === 0) {
42117
42466
  log.info("intelligence.skip", { runId, reason: "no completed runs" });
42118
42467
  return null;
@@ -42187,7 +42536,7 @@ var IntelligenceService = class {
42187
42536
  * Returns the persisted insights so the coordinator can count critical/high.
42188
42537
  */
42189
42538
  analyzeAndPersistGbp(runId, projectId) {
42190
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq41(runs.id, runId)).get();
42539
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq42(runs.id, runId)).get();
42191
42540
  if (!runRow) {
42192
42541
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
42193
42542
  this.persistGbpInsights(runId, projectId, [], []);
@@ -42195,9 +42544,9 @@ var IntelligenceService = class {
42195
42544
  }
42196
42545
  const windowStart = runRow.startedAt ?? runRow.createdAt;
42197
42546
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
42198
- const selected = this.db.select().from(gbpLocations).where(and32(
42199
- eq41(gbpLocations.projectId, projectId),
42200
- eq41(gbpLocations.selected, true),
42547
+ const selected = this.db.select().from(gbpLocations).where(and33(
42548
+ eq42(gbpLocations.projectId, projectId),
42549
+ eq42(gbpLocations.selected, true),
42201
42550
  gte10(gbpLocations.syncedAt, windowStart),
42202
42551
  lte6(gbpLocations.syncedAt, windowEnd)
42203
42552
  )).all();
@@ -42232,12 +42581,12 @@ var IntelligenceService = class {
42232
42581
  }
42233
42582
  /** Build the per-location signal bundle the GBP analyzer consumes. */
42234
42583
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
42235
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and32(eq41(gbpDailyMetrics.projectId, projectId), eq41(gbpDailyMetrics.locationName, locationName))).all();
42236
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and32(eq41(gbpPlaceActions.projectId, projectId), eq41(gbpPlaceActions.locationName, locationName))).all();
42237
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and32(eq41(gbpLodgingSnapshots.projectId, projectId), eq41(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
42238
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and32(eq41(gbpLocations.projectId, projectId), eq41(gbpLocations.locationName, locationName))).get();
42584
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and33(eq42(gbpDailyMetrics.projectId, projectId), eq42(gbpDailyMetrics.locationName, locationName))).all();
42585
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and33(eq42(gbpPlaceActions.projectId, projectId), eq42(gbpPlaceActions.locationName, locationName))).all();
42586
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and33(eq42(gbpLodgingSnapshots.projectId, projectId), eq42(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc19(gbpLodgingSnapshots.syncedAt)).limit(1).get();
42587
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and33(eq42(gbpLocations.projectId, projectId), eq42(gbpLocations.locationName, locationName))).get();
42239
42588
  const descriptionMissing = !(ownerRow?.description ?? "").trim();
42240
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and32(eq41(gbpPlaceDetails.projectId, projectId), eq41(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
42589
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and33(eq42(gbpPlaceDetails.projectId, projectId), eq42(gbpPlaceDetails.locationName, locationName))).orderBy(desc19(gbpPlaceDetails.syncedAt)).limit(1).get();
42241
42590
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
42242
42591
  const summary = buildGbpSummary({
42243
42592
  locationName,
@@ -42273,7 +42622,7 @@ var IntelligenceService = class {
42273
42622
  /** Build the month-over-month keyword series for a location from the
42274
42623
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
42275
42624
  buildGbpKeywordTrend(projectId, locationName) {
42276
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and32(eq41(gbpKeywordMonthly.projectId, projectId), eq41(gbpKeywordMonthly.locationName, locationName))).all();
42625
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and33(eq42(gbpKeywordMonthly.projectId, projectId), eq42(gbpKeywordMonthly.locationName, locationName))).all();
42277
42626
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
42278
42627
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
42279
42628
  const recentMonth = months[0] ?? null;
@@ -42304,7 +42653,7 @@ var IntelligenceService = class {
42304
42653
  */
42305
42654
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
42306
42655
  const covered = new Set(coveredLocationNames);
42307
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and32(eq41(insights.projectId, projectId), eq41(insights.provider, GBP_INSIGHT_PROVIDER))).all();
42656
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and33(eq42(insights.projectId, projectId), eq42(insights.provider, GBP_INSIGHT_PROVIDER))).all();
42308
42657
  const staleIds = [];
42309
42658
  const dismissedSlots = /* @__PURE__ */ new Set();
42310
42659
  for (const row of existing) {
@@ -42315,7 +42664,7 @@ var IntelligenceService = class {
42315
42664
  }
42316
42665
  this.db.transaction((tx) => {
42317
42666
  for (const id of staleIds) {
42318
- tx.delete(insights).where(eq41(insights.id, id)).run();
42667
+ tx.delete(insights).where(eq42(insights.id, id)).run();
42319
42668
  }
42320
42669
  for (const insight of gbpInsights) {
42321
42670
  const parsed = parseGbpInsightId(insight.id);
@@ -42393,7 +42742,7 @@ var IntelligenceService = class {
42393
42742
  * create per run + aggregate). DB is left untouched.
42394
42743
  */
42395
42744
  backfill(projectName, opts, onProgress) {
42396
- const project = this.db.select().from(projects).where(eq41(projects.name, projectName)).get();
42745
+ const project = this.db.select().from(projects).where(eq42(projects.name, projectName)).get();
42397
42746
  if (!project) {
42398
42747
  throw new Error(`Project "${projectName}" not found`);
42399
42748
  }
@@ -42406,9 +42755,9 @@ var IntelligenceService = class {
42406
42755
  sinceTimestamp = parsed;
42407
42756
  }
42408
42757
  const allRuns = this.db.select().from(runs).where(
42409
- and32(
42410
- eq41(runs.projectId, project.id),
42411
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
42758
+ and33(
42759
+ eq42(runs.projectId, project.id),
42760
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42412
42761
  // Backfill must not replay probe runs as if they were real sweeps.
42413
42762
  ne6(runs.trigger, RunTriggers.probe)
42414
42763
  )
@@ -42487,7 +42836,7 @@ var IntelligenceService = class {
42487
42836
  return { processed, skipped, totalInsights };
42488
42837
  }
42489
42838
  loadTrackedCompetitors(projectId) {
42490
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq41(competitors.projectId, projectId)).all().map((r) => r.domain);
42839
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq42(competitors.projectId, projectId)).all().map((r) => r.domain);
42491
42840
  }
42492
42841
  /**
42493
42842
  * Wipe transition signals from an analysis result while keeping health.
@@ -42508,15 +42857,15 @@ var IntelligenceService = class {
42508
42857
  }
42509
42858
  persistResult(result, runId, projectId) {
42510
42859
  const previouslyDismissed = /* @__PURE__ */ new Set();
42511
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq41(insights.runId, runId)).all();
42860
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq42(insights.runId, runId)).all();
42512
42861
  for (const row of existingInsights) {
42513
42862
  if (row.dismissed) {
42514
42863
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
42515
42864
  }
42516
42865
  }
42517
42866
  this.db.transaction((tx) => {
42518
- tx.delete(insights).where(eq41(insights.runId, runId)).run();
42519
- tx.delete(healthSnapshots).where(eq41(healthSnapshots.runId, runId)).run();
42867
+ tx.delete(insights).where(eq42(insights.runId, runId)).run();
42868
+ tx.delete(healthSnapshots).where(eq42(healthSnapshots.runId, runId)).run();
42520
42869
  const now = (/* @__PURE__ */ new Date()).toISOString();
42521
42870
  for (const insight of result.insights) {
42522
42871
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -42536,7 +42885,7 @@ var IntelligenceService = class {
42536
42885
  }).run();
42537
42886
  }
42538
42887
  tx.insert(healthSnapshots).values({
42539
- id: crypto33.randomUUID(),
42888
+ id: crypto34.randomUUID(),
42540
42889
  projectId,
42541
42890
  runId,
42542
42891
  overallCitedRate: String(result.health.overallCitedRate),
@@ -42569,28 +42918,28 @@ var IntelligenceService = class {
42569
42918
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
42570
42919
  const regressions = rawInsights.filter((i) => i.type === "regression");
42571
42920
  if (regressions.length === 0) return rawInsights;
42572
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq41(gscSearchData.projectId, projectId)).all();
42921
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq42(gscSearchData.projectId, projectId)).all();
42573
42922
  const gscConnected = gscRows.length > 0;
42574
42923
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
42575
42924
  for (const row of gscRows) {
42576
42925
  const key = row.query.toLowerCase();
42577
42926
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
42578
42927
  }
42579
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq41(projects.id, projectId)).get();
42928
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq42(projects.id, projectId)).get();
42580
42929
  const locationCount = Math.max(
42581
42930
  1,
42582
42931
  (projectRow?.locations ?? []).length
42583
42932
  );
42584
42933
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
42585
42934
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
42586
- and32(
42587
- eq41(runs.projectId, projectId),
42588
- eq41(runs.kind, RunKinds["answer-visibility"]),
42589
- or8(eq41(runs.status, "completed"), eq41(runs.status, "partial")),
42935
+ and33(
42936
+ eq42(runs.projectId, projectId),
42937
+ eq42(runs.kind, RunKinds["answer-visibility"]),
42938
+ or8(eq42(runs.status, "completed"), eq42(runs.status, "partial")),
42590
42939
  // Defensive — see top of file.
42591
42940
  ne6(runs.trigger, RunTriggers.probe)
42592
42941
  )
42593
- ).orderBy(desc18(runs.createdAt), desc18(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
42942
+ ).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
42594
42943
  const recentGroups = groupRunsByCreatedAt(recentRunRows);
42595
42944
  const recentRunIds = [];
42596
42945
  const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
@@ -42606,7 +42955,7 @@ var IntelligenceService = class {
42606
42955
  const haveHistory = recentRunIds.length > 0;
42607
42956
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
42608
42957
  if (haveHistory) {
42609
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and32(eq41(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
42958
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and33(eq42(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
42610
42959
  const regressionGroups = /* @__PURE__ */ new Map();
42611
42960
  for (const row of priorRows) {
42612
42961
  if (!row.runId) continue;
@@ -42635,7 +42984,7 @@ var IntelligenceService = class {
42635
42984
  });
42636
42985
  }
42637
42986
  buildRunData(runId, projectId, completedAt, location = null) {
42638
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq41(projects.id, projectId)).get();
42987
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq42(projects.id, projectId)).get();
42639
42988
  const projectDomains = projectDomainRow ? effectiveDomains({
42640
42989
  canonicalDomain: projectDomainRow.canonicalDomain,
42641
42990
  ownedDomains: projectDomainRow.ownedDomains
@@ -42652,7 +43001,7 @@ var IntelligenceService = class {
42652
43001
  citedDomains: querySnapshots.citedDomains,
42653
43002
  competitorOverlap: querySnapshots.competitorOverlap,
42654
43003
  snapshotLocation: querySnapshots.location
42655
- }).from(querySnapshots).leftJoin(queries, eq41(querySnapshots.queryId, queries.id)).where(eq41(querySnapshots.runId, runId)).all();
43004
+ }).from(querySnapshots).leftJoin(queries, eq42(querySnapshots.queryId, queries.id)).where(eq42(querySnapshots.runId, runId)).all();
42656
43005
  const snapshots = [];
42657
43006
  let orphanCount = 0;
42658
43007
  for (const r of rows) {
@@ -42728,6 +43077,8 @@ export {
42728
43077
  aiUserFetchEventsHourly,
42729
43078
  rawEventSamples,
42730
43079
  discoverySessions,
43080
+ researchRuns,
43081
+ researchRunQueries,
42731
43082
  gbpLocations,
42732
43083
  gbpDailyMetrics,
42733
43084
  gbpKeywordImpressions,