@ainyc/canonry 4.114.1 → 4.115.1

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 (25) hide show
  1. package/assets/assets/{BacklinksPage-C60ma4Sg.js → BacklinksPage-yUb8oJuq.js} +1 -1
  2. package/assets/assets/{ChartPrimitives-Blgw1l6J.js → ChartPrimitives-CdQM3YDG.js} +1 -1
  3. package/assets/assets/ProjectPage-CHj9fvTs.js +7 -0
  4. package/assets/assets/{RunRow-DgQiXWZl.js → RunRow-BU_x7EcP.js} +1 -1
  5. package/assets/assets/{RunsPage-C9upHccV.js → RunsPage-CFIBWaO7.js} +1 -1
  6. package/assets/assets/{SettingsPage-Bz_-_DnJ.js → SettingsPage-PyMryUad.js} +1 -1
  7. package/assets/assets/{TrafficPage-BhBnQl2N.js → TrafficPage-471ypAdQ.js} +1 -1
  8. package/assets/assets/{TrafficSourceDetailPage-DWXqcRNO.js → TrafficSourceDetailPage-CI7TEQge.js} +1 -1
  9. package/assets/assets/{arrow-left-C6SuuEtD.js → arrow-left-D2N9u-0i.js} +1 -1
  10. package/assets/assets/{extract-error-message-DPZ5yqzd.js → extract-error-message-CAdsqKp3.js} +1 -1
  11. package/assets/assets/index-B4R1-8hR.css +1 -0
  12. package/assets/assets/{index-C3oQZHf8.js → index-CoL5HENE.js} +66 -66
  13. package/assets/assets/{trash-2-D3Nju7AK.js → trash-2-UgQrN5Np.js} +1 -1
  14. package/assets/index.html +2 -2
  15. package/dist/{chunk-VEUBAZRG.js → chunk-6LOKMEL3.js} +101 -0
  16. package/dist/{chunk-WMWZW34J.js → chunk-B2SNXXAJ.js} +4 -4
  17. package/dist/{chunk-DUBZH5JW.js → chunk-CW4JISBL.js} +1 -1
  18. package/dist/{chunk-3VSTTH4M.js → chunk-Z3OGDQHB.js} +489 -244
  19. package/dist/cli.js +52 -8
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-I5EQ77KZ.js → intelligence-service-4AWB2V7U.js} +2 -2
  22. package/dist/mcp.js +2 -2
  23. package/package.json +8 -8
  24. package/assets/assets/ProjectPage-DfvEEwNU.js +0 -7
  25. package/assets/assets/index-CReX5Jn9.css +0 -1
@@ -20,6 +20,7 @@ import {
20
20
  DiscoveryBuckets,
21
21
  DiscoveryCompetitorTypes,
22
22
  DiscoverySessionStatuses,
23
+ GA4AiReferralTrafficClasses,
23
24
  RunKinds,
24
25
  RunStatuses,
25
26
  RunTriggers,
@@ -87,6 +88,7 @@ import {
87
88
  citationStateToCited,
88
89
  citationVisibilityResponseSchema,
89
90
  classifyCitedSurface,
91
+ classifyGa4AiReferralTrafficClass,
90
92
  classifySkillFile,
91
93
  classifySurfaceFromCategory,
92
94
  classifyTrafficPath,
@@ -273,10 +275,10 @@ import {
273
275
  wordpressSchemaDeployResultDtoSchema,
274
276
  wordpressSchemaStatusResultDtoSchema,
275
277
  wordpressStatusDtoSchema
276
- } from "./chunk-VEUBAZRG.js";
278
+ } from "./chunk-6LOKMEL3.js";
277
279
 
278
280
  // src/intelligence-service.ts
279
- import { eq as eq38, desc as desc18, asc as asc6, and as and29, ne as ne5, or as or6, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
281
+ import { eq as eq38, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
280
282
 
281
283
  // ../db/src/client.ts
282
284
  import { mkdirSync } from "fs";
@@ -758,6 +760,7 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
758
760
  date: text("date").notNull(),
759
761
  source: text("source").notNull(),
760
762
  medium: text("medium").notNull(),
763
+ trafficClass: text("traffic_class").$type().notNull().default("organic"),
761
764
  /** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
762
765
  sourceDimension: text("source_dimension").notNull().default("session"),
763
766
  /** GA4 default channel group for the session (e.g. 'Referral', 'Organic Social'). */
@@ -772,6 +775,7 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
772
775
  index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
773
776
  index("idx_ga_ai_ref_source").on(table.source),
774
777
  index("idx_ga_ai_ref_landing_page").on(table.projectId, table.date, table.landingPageNormalized),
778
+ index("idx_ga_ai_ref_traffic_class").on(table.projectId, table.date, table.trafficClass),
775
779
  uniqueIndex("idx_ga_ai_ref_unique_v4").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension, table.channelGroup, table.landingPage),
776
780
  index("idx_ga_ai_ref_run").on(table.syncRunId)
777
781
  ]);
@@ -3482,6 +3486,41 @@ var MIGRATION_VERSIONS = [
3482
3486
  // post-truncation). Additive + nullable (downgrade-safe).
3483
3487
  `ALTER TABLE discovery_sessions ADD COLUMN canonical_count INTEGER`
3484
3488
  ]
3489
+ },
3490
+ {
3491
+ version: 95,
3492
+ name: "ga-ai-referral-traffic-class",
3493
+ statements: [],
3494
+ run: (tx) => {
3495
+ if (!tableExists(tx, "ga_ai_referrals")) return;
3496
+ if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) {
3497
+ tx.run(sql.raw(`ALTER TABLE ga_ai_referrals ADD COLUMN traffic_class TEXT NOT NULL DEFAULT 'organic'`));
3498
+ }
3499
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_traffic_class ON ga_ai_referrals(project_id, date, traffic_class)`));
3500
+ }
3501
+ },
3502
+ {
3503
+ // v95 added `ga_ai_referrals.traffic_class` with `DEFAULT 'organic'` and did
3504
+ // not classify the rows that already existed. Every historical AI referral
3505
+ // was therefore stamped 'organic', including paid ChatGPT-ads traffic
3506
+ // carrying `medium='cpc'` / `channel_group='Paid Other'`. That silently
3507
+ // reports purchased sessions as organic wins.
3508
+ //
3509
+ // The class is a pure function of columns already stored on each row, so
3510
+ // re-derive it here instead of making an operator delete and re-fetch
3511
+ // identical data from GA4.
3512
+ //
3513
+ // Every row is re-classified, not only the "unclassified" ones: v95's
3514
+ // default is indistinguishable from a genuinely-organic classification, so
3515
+ // there is no way to tell the two apart. `classifyGa4AiReferralTrafficClass`
3516
+ // is pure and deterministic, so re-running it over already-correct rows
3517
+ // writes nothing, which is what makes this version safe to replay.
3518
+ version: 96,
3519
+ name: "ga-ai-referral-traffic-class-backfill",
3520
+ statements: [],
3521
+ run: (tx) => {
3522
+ backfillGaAiReferralTrafficClass(tx);
3523
+ }
3485
3524
  }
3486
3525
  ];
3487
3526
  function rebuildBacklinkTableWithSource(tx, table) {
@@ -3568,6 +3607,26 @@ function tableIsEmpty(db, table) {
3568
3607
  const rows = db.all(sql.raw(`SELECT COUNT(*) as c FROM ${table}`));
3569
3608
  return (rows[0]?.c ?? 0) === 0;
3570
3609
  }
3610
+ function backfillGaAiReferralTrafficClass(tx) {
3611
+ if (!tableExists(tx, "ga_ai_referrals")) return 0;
3612
+ if (!columnExists(tx, "ga_ai_referrals", "traffic_class")) return 0;
3613
+ const rows = tx.all(sql.raw(
3614
+ `SELECT id, source, medium, channel_group, landing_page, traffic_class FROM ga_ai_referrals`
3615
+ ));
3616
+ let updated = 0;
3617
+ for (const row of rows) {
3618
+ const next = classifyGa4AiReferralTrafficClass({
3619
+ source: row.source,
3620
+ medium: row.medium,
3621
+ channelGroup: row.channel_group,
3622
+ landingPage: row.landing_page
3623
+ });
3624
+ if (next === row.traffic_class) continue;
3625
+ tx.run(sql`UPDATE ga_ai_referrals SET traffic_class = ${next} WHERE id = ${row.id}`);
3626
+ updated += 1;
3627
+ }
3628
+ return updated;
3629
+ }
3571
3630
  function hasLegacyQuerySchema(db) {
3572
3631
  return tableExists(db, "keywords") || columnExists(db, "query_snapshots", "keyword_id") || columnExists(db, "insights", "keyword");
3573
3632
  }
@@ -6706,7 +6765,7 @@ async function queryRoutes(app, opts) {
6706
6765
 
6707
6766
  // ../api-routes/src/competitors.ts
6708
6767
  import crypto6 from "crypto";
6709
- import { eq as eq5 } from "drizzle-orm";
6768
+ import { and as and2, eq as eq5 } from "drizzle-orm";
6710
6769
  function normalizeCompetitor(domain) {
6711
6770
  const reg = registrableDomain(domain);
6712
6771
  if (reg) return reg;
@@ -6725,11 +6784,14 @@ function normalizeCompetitorList(domains) {
6725
6784
  }
6726
6785
  return result;
6727
6786
  }
6787
+ function serializeCompetitor(row) {
6788
+ return { id: row.id, domain: row.domain, createdAt: row.createdAt };
6789
+ }
6728
6790
  async function competitorRoutes(app) {
6729
6791
  app.get("/projects/:name/competitors", async (request, reply) => {
6730
6792
  const project = resolveProject(app.db, request.params.name);
6731
6793
  const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
6732
- return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
6794
+ return reply.send(rows.map(serializeCompetitor));
6733
6795
  });
6734
6796
  app.put("/projects/:name/competitors", async (request, reply) => {
6735
6797
  const project = resolveProject(app.db, request.params.name);
@@ -6759,7 +6821,7 @@ async function competitorRoutes(app) {
6759
6821
  });
6760
6822
  });
6761
6823
  const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
6762
- return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
6824
+ return reply.send(rows.map(serializeCompetitor));
6763
6825
  });
6764
6826
  app.post("/projects/:name/competitors", async (request, reply) => {
6765
6827
  const project = resolveProject(app.db, request.params.name);
@@ -6791,7 +6853,7 @@ async function competitorRoutes(app) {
6791
6853
  });
6792
6854
  });
6793
6855
  const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
6794
- return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
6856
+ return reply.send(rows.map(serializeCompetitor));
6795
6857
  });
6796
6858
  app.delete("/projects/:name/competitors", async (request, reply) => {
6797
6859
  const project = resolveProject(app.db, request.params.name);
@@ -6813,7 +6875,26 @@ async function competitorRoutes(app) {
6813
6875
  });
6814
6876
  });
6815
6877
  const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
6816
- return reply.send(rows.map((r) => ({ id: r.id, domain: r.domain, createdAt: r.createdAt })));
6878
+ return reply.send(rows.map(serializeCompetitor));
6879
+ });
6880
+ app.delete("/projects/:name/competitors/:id", async (request, reply) => {
6881
+ const project = resolveProject(app.db, request.params.name);
6882
+ const competitor = app.db.select().from(competitors).where(and2(eq5(competitors.projectId, project.id), eq5(competitors.id, request.params.id))).get();
6883
+ if (!competitor) {
6884
+ throw notFound("Competitor", request.params.id);
6885
+ }
6886
+ app.db.transaction((tx) => {
6887
+ tx.delete(competitors).where(eq5(competitors.id, competitor.id)).run();
6888
+ writeAuditLog(tx, auditFromRequest(request, {
6889
+ projectId: project.id,
6890
+ actor: "api",
6891
+ action: "competitors.deleted",
6892
+ entityType: "competitor",
6893
+ entityId: competitor.id,
6894
+ diff: { deleted: [competitor.domain] }
6895
+ }));
6896
+ });
6897
+ return reply.status(204).send();
6817
6898
  });
6818
6899
  }
6819
6900
  function parseCompetitorBatch(value) {
@@ -6829,12 +6910,12 @@ function parseCompetitorBatch(value) {
6829
6910
 
6830
6911
  // ../api-routes/src/runs.ts
6831
6912
  import crypto8 from "crypto";
6832
- import { and as and3, eq as eq7, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
6913
+ import { and as and4, eq as eq7, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
6833
6914
  import { gte } from "drizzle-orm";
6834
6915
 
6835
6916
  // ../api-routes/src/run-queue.ts
6836
6917
  import crypto7 from "crypto";
6837
- import { and as and2, eq as eq6, or } from "drizzle-orm";
6918
+ import { and as and3, eq as eq6, or } from "drizzle-orm";
6838
6919
  function queueRunIfProjectIdle(db, params) {
6839
6920
  const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
6840
6921
  const kind = params.kind ?? "answer-visibility";
@@ -6842,7 +6923,7 @@ function queueRunIfProjectIdle(db, params) {
6842
6923
  const runId = crypto7.randomUUID();
6843
6924
  return db.transaction((tx) => {
6844
6925
  const activeRun = tx.select().from(runs).where(
6845
- and2(
6926
+ and3(
6846
6927
  eq6(runs.projectId, params.projectId),
6847
6928
  or(eq6(runs.status, "queued"), eq6(runs.status, "running"))
6848
6929
  )
@@ -6925,7 +7006,7 @@ async function runRoutes(app, opts) {
6925
7006
  throw validationError("No locations configured for this project");
6926
7007
  }
6927
7008
  const result = app.db.transaction((tx) => {
6928
- const activeRun = tx.select({ id: runs.id }).from(runs).where(and3(
7009
+ const activeRun = tx.select({ id: runs.id }).from(runs).where(and4(
6929
7010
  eq7(runs.projectId, project.id),
6930
7011
  or2(eq7(runs.status, "queued"), eq7(runs.status, "running"))
6931
7012
  )).get();
@@ -6998,15 +7079,15 @@ async function runRoutes(app, opts) {
6998
7079
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
6999
7080
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
7000
7081
  const kind = parseListKind(request.query.kind);
7001
- const where = kind ? and3(eq7(runs.projectId, project.id), eq7(runs.kind, kind)) : eq7(runs.projectId, project.id);
7082
+ const where = kind ? and4(eq7(runs.projectId, project.id), eq7(runs.kind, kind)) : eq7(runs.projectId, project.id);
7002
7083
  const rows = limit == null ? app.db.select().from(runs).where(where).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(where).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
7003
7084
  return reply.send(rows.map(formatRun));
7004
7085
  });
7005
7086
  app.get("/projects/:name/runs/latest", async (request, reply) => {
7006
7087
  const project = resolveProject(app.db, request.params.name);
7007
- const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and3(eq7(runs.projectId, project.id), notProbeRun())).get();
7088
+ const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(eq7(runs.projectId, project.id), notProbeRun())).get();
7008
7089
  const totalRuns = countRow?.count ?? 0;
7009
- const latestRun = app.db.select().from(runs).where(and3(eq7(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
7090
+ const latestRun = app.db.select().from(runs).where(and4(eq7(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
7010
7091
  if (!latestRun) {
7011
7092
  return reply.send({ totalRuns: 0, run: null });
7012
7093
  }
@@ -7025,7 +7106,7 @@ async function runRoutes(app, opts) {
7025
7106
  if (kind) filters.push(eq7(runs.kind, kind));
7026
7107
  const scopedProjectId = request.apiKey?.projectId;
7027
7108
  if (scopedProjectId) filters.push(eq7(runs.projectId, scopedProjectId));
7028
- const rows = app.db.select().from(runs).where(and3(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
7109
+ const rows = app.db.select().from(runs).where(and4(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
7029
7110
  return reply.send(rows.map(formatRun));
7030
7111
  });
7031
7112
  app.post("/runs", async (request, reply) => {
@@ -7236,7 +7317,7 @@ function loadRunDetail(app, run) {
7236
7317
 
7237
7318
  // ../api-routes/src/apply.ts
7238
7319
  import crypto10 from "crypto";
7239
- import { and as and4, eq as eq8 } from "drizzle-orm";
7320
+ import { and as and5, eq as eq8 } from "drizzle-orm";
7240
7321
 
7241
7322
  // ../api-routes/src/schedule-utils.ts
7242
7323
  import { CronExpressionParser } from "cron-parser";
@@ -7679,7 +7760,7 @@ async function applyRoutes(app, opts) {
7679
7760
  });
7680
7761
  const AV_KIND = SchedulableRunKinds["answer-visibility"];
7681
7762
  if (resolvedSchedule) {
7682
- const existingSched = tx.select().from(schedules).where(and4(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
7763
+ const existingSched = tx.select().from(schedules).where(and5(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
7683
7764
  if (existingSched) {
7684
7765
  tx.update(schedules).set({
7685
7766
  cronExpr: resolvedSchedule.cronExpr,
@@ -7705,9 +7786,9 @@ async function applyRoutes(app, opts) {
7705
7786
  }
7706
7787
  scheduleAction = "upsert";
7707
7788
  } else if (deleteSchedule) {
7708
- const existingSched = tx.select().from(schedules).where(and4(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
7789
+ const existingSched = tx.select().from(schedules).where(and5(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
7709
7790
  if (existingSched) {
7710
- tx.delete(schedules).where(and4(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
7791
+ tx.delete(schedules).where(and5(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
7711
7792
  scheduleAction = "delete";
7712
7793
  }
7713
7794
  }
@@ -7791,7 +7872,7 @@ function normalizeCompetitorList2(domains) {
7791
7872
  }
7792
7873
 
7793
7874
  // ../api-routes/src/history.ts
7794
- import { and as and5, eq as eq9, desc as desc2, inArray as inArray2 } from "drizzle-orm";
7875
+ import { and as and6, eq as eq9, desc as desc2, inArray as inArray2 } from "drizzle-orm";
7795
7876
 
7796
7877
  // ../api-routes/src/notification-redaction.ts
7797
7878
  var REDACTED_URL = {
@@ -7849,7 +7930,7 @@ async function historyRoutes(app) {
7849
7930
  const project = resolveProject(app.db, request.params.name);
7850
7931
  const limit = parseInt(request.query.limit ?? "50", 10);
7851
7932
  const offset = parseInt(request.query.offset ?? "0", 10);
7852
- const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and5(eq9(runs.projectId, project.id), notProbeRun())).all();
7933
+ const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and6(eq9(runs.projectId, project.id), notProbeRun())).all();
7853
7934
  if (projectRuns.length === 0) {
7854
7935
  return reply.send({ snapshots: [], total: 0 });
7855
7936
  }
@@ -7898,7 +7979,7 @@ async function historyRoutes(app) {
7898
7979
  app.get("/projects/:name/timeline", async (request, reply) => {
7899
7980
  const project = resolveProject(app.db, request.params.name);
7900
7981
  const projectQueries = app.db.select().from(queries).where(eq9(queries.projectId, project.id)).all();
7901
- const projectRuns = app.db.select().from(runs).where(and5(eq9(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
7982
+ const projectRuns = app.db.select().from(runs).where(and6(eq9(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
7902
7983
  if (projectRuns.length === 0 || projectQueries.length === 0) {
7903
7984
  return reply.send([]);
7904
7985
  }
@@ -8109,13 +8190,13 @@ function formatAuditEntry(row) {
8109
8190
  }
8110
8191
 
8111
8192
  // ../api-routes/src/analytics.ts
8112
- import { and as and6, eq as eq10, desc as desc3, inArray as inArray3 } from "drizzle-orm";
8193
+ import { and as and7, eq as eq10, desc as desc3, inArray as inArray3 } from "drizzle-orm";
8113
8194
  async function analyticsRoutes(app) {
8114
8195
  app.get("/projects/:name/analytics/metrics", async (request, reply) => {
8115
8196
  const project = resolveProject(app.db, request.params.name);
8116
8197
  const window = parseWindow(request.query.window);
8117
8198
  const cutoff = windowCutoff(window);
8118
- const projectRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8199
+ const projectRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8119
8200
  if (projectRuns.length === 0) {
8120
8201
  return reply.send({
8121
8202
  window,
@@ -8167,14 +8248,14 @@ async function analyticsRoutes(app) {
8167
8248
  const project = resolveProject(app.db, request.params.name);
8168
8249
  const window = parseWindow(request.query.window);
8169
8250
  const cutoff = windowCutoff(window);
8170
- const completedRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
8251
+ const completedRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
8171
8252
  const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
8172
8253
  const latestGroupRunIds = latestGroup.map((r) => r.id);
8173
8254
  const latestRun = pickGroupRepresentative(latestGroup);
8174
8255
  if (!latestRun) {
8175
8256
  return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
8176
8257
  }
8177
- const windowRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8258
+ const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8178
8259
  const windowRunIds = windowRuns.map((r) => r.id);
8179
8260
  const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
8180
8261
  const consistencyMap = /* @__PURE__ */ new Map();
@@ -8305,7 +8386,7 @@ async function analyticsRoutes(app) {
8305
8386
  const mapped = surfaceClassFromCompetitorType(row.competitorType);
8306
8387
  if (mapped) storedSurfaceClasses.set(normalizeProjectDomain(row.domain), mapped);
8307
8388
  }
8308
- const windowRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8389
+ const windowRuns = app.db.select().from(runs).where(and7(eq10(runs.projectId, project.id), eq10(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
8309
8390
  if (windowRuns.length === 0) {
8310
8391
  return reply.send({
8311
8392
  overall: [],
@@ -8570,7 +8651,7 @@ function buildCategoryCounts(counts2) {
8570
8651
  }
8571
8652
 
8572
8653
  // ../api-routes/src/intelligence.ts
8573
- import { eq as eq11, desc as desc4, and as and7, inArray as inArray4, like } from "drizzle-orm";
8654
+ import { eq as eq11, desc as desc4, and as and8, inArray as inArray4, like } from "drizzle-orm";
8574
8655
  var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
8575
8656
  function severitiesAtOrAbove(min) {
8576
8657
  const floor = SEVERITY_RANK[min];
@@ -8710,7 +8791,7 @@ async function intelligenceRoutes(app) {
8710
8791
  }
8711
8792
  limit = parsed;
8712
8793
  }
8713
- const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and7(...conditions)).orderBy(desc4(insights.createdAt)).all();
8794
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and8(...conditions)).orderBy(desc4(insights.createdAt)).all();
8714
8795
  const showDismissed = request.query.dismissed === "true";
8715
8796
  let result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
8716
8797
  if (limit !== void 0) result = result.slice(0, limit);
@@ -8735,7 +8816,7 @@ async function intelligenceRoutes(app) {
8735
8816
  });
8736
8817
  app.get("/projects/:name/health/latest", async (request, reply) => {
8737
8818
  const project = resolveProject(app.db, request.params.name);
8738
- const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and7(
8819
+ const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and8(
8739
8820
  eq11(runs.projectId, project.id),
8740
8821
  eq11(runs.kind, RunKinds["answer-visibility"]),
8741
8822
  inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial]),
@@ -8746,7 +8827,7 @@ async function intelligenceRoutes(app) {
8746
8827
  const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
8747
8828
  const latestGroupRunIds = latestGroup.map((r) => r.id);
8748
8829
  if (latestGroupRunIds.length > 0) {
8749
- const groupRows = app.db.select().from(healthSnapshots).where(and7(
8830
+ const groupRows = app.db.select().from(healthSnapshots).where(and8(
8750
8831
  eq11(healthSnapshots.projectId, project.id),
8751
8832
  inArray4(healthSnapshots.runId, latestGroupRunIds)
8752
8833
  )).all();
@@ -8770,14 +8851,14 @@ async function intelligenceRoutes(app) {
8770
8851
  }
8771
8852
 
8772
8853
  // ../api-routes/src/report.ts
8773
- import { and as and11, desc as desc7, eq as eq15, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
8854
+ import { and as and12, desc as desc7, eq as eq15, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
8774
8855
 
8775
8856
  // ../api-routes/src/content.ts
8776
8857
  import crypto11 from "crypto";
8777
- import { and as and9, desc as desc6, eq as eq13 } from "drizzle-orm";
8858
+ import { and as and10, desc as desc6, eq as eq13 } from "drizzle-orm";
8778
8859
 
8779
8860
  // ../api-routes/src/content-data.ts
8780
- import { and as and8, eq as eq12, desc as desc5, inArray as inArray5 } from "drizzle-orm";
8861
+ import { and as and9, eq as eq12, desc as desc5, inArray as inArray5 } from "drizzle-orm";
8781
8862
  var RECENT_RUNS_WINDOW = 5;
8782
8863
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
8783
8864
  const projectId = project.id;
@@ -8907,7 +8988,7 @@ function listCompetitorDomains(db, projectId) {
8907
8988
  }
8908
8989
  function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
8909
8990
  const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
8910
- and8(
8991
+ and9(
8911
8992
  eq12(runs.projectId, projectId),
8912
8993
  eq12(runs.kind, RunKinds["answer-visibility"]),
8913
8994
  // Queued/running/failed/cancelled runs may have partial or no
@@ -9278,7 +9359,7 @@ async function contentRoutes(app, opts = {}) {
9278
9359
  dismissedAt: now
9279
9360
  }
9280
9361
  }).run();
9281
- const row = app.db.select().from(contentTargetDismissals).where(and9(
9362
+ const row = app.db.select().from(contentTargetDismissals).where(and10(
9282
9363
  eq13(contentTargetDismissals.projectId, project.id),
9283
9364
  eq13(contentTargetDismissals.targetRef, targetRef)
9284
9365
  )).get();
@@ -9288,7 +9369,7 @@ async function contentRoutes(app, opts = {}) {
9288
9369
  app.delete("/projects/:name/content/dismissals/:targetRef", async (request, reply) => {
9289
9370
  const project = resolveProject(app.db, request.params.name);
9290
9371
  const { targetRef } = request.params;
9291
- const result = app.db.delete(contentTargetDismissals).where(and9(
9372
+ const result = app.db.delete(contentTargetDismissals).where(and10(
9292
9373
  eq13(contentTargetDismissals.projectId, project.id),
9293
9374
  eq13(contentTargetDismissals.targetRef, targetRef)
9294
9375
  )).run();
@@ -9299,7 +9380,7 @@ async function contentRoutes(app, opts = {}) {
9299
9380
  });
9300
9381
  app.get("/projects/:name/content/recommendations/:targetRef/analysis", async (request, reply) => {
9301
9382
  const project = resolveProject(app.db, request.params.name);
9302
- const row = app.db.select().from(recommendationExplanations).where(and9(
9383
+ const row = app.db.select().from(recommendationExplanations).where(and10(
9303
9384
  eq13(recommendationExplanations.projectId, project.id),
9304
9385
  eq13(recommendationExplanations.targetRef, request.params.targetRef)
9305
9386
  )).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
@@ -9327,7 +9408,7 @@ async function contentRoutes(app, opts = {}) {
9327
9408
  throw notFound("contentRecommendation", targetRef);
9328
9409
  }
9329
9410
  if (!body.forceRefresh) {
9330
- const cached = app.db.select().from(recommendationExplanations).where(and9(
9411
+ const cached = app.db.select().from(recommendationExplanations).where(and10(
9331
9412
  eq13(recommendationExplanations.projectId, project.id),
9332
9413
  eq13(recommendationExplanations.targetRef, targetRef)
9333
9414
  )).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
@@ -9366,7 +9447,7 @@ async function contentRoutes(app, opts = {}) {
9366
9447
  generatedAt: now
9367
9448
  }
9368
9449
  }).run();
9369
- const row = app.db.select().from(recommendationExplanations).where(and9(
9450
+ const row = app.db.select().from(recommendationExplanations).where(and10(
9370
9451
  eq13(recommendationExplanations.projectId, project.id),
9371
9452
  eq13(recommendationExplanations.targetRef, targetRef),
9372
9453
  eq13(recommendationExplanations.promptVersion, result.promptVersion)
@@ -9450,7 +9531,7 @@ async function contentRoutes(app, opts = {}) {
9450
9531
  generatedAt: now
9451
9532
  }
9452
9533
  }).run();
9453
- const row = app.db.select().from(recommendationBriefs).where(and9(
9534
+ const row = app.db.select().from(recommendationBriefs).where(and10(
9454
9535
  eq13(recommendationBriefs.projectId, project.id),
9455
9536
  eq13(recommendationBriefs.targetRef, targetRef),
9456
9537
  eq13(recommendationBriefs.promptVersion, result.promptVersion)
@@ -9480,7 +9561,7 @@ function lookupCachedBrief(db, projectId, targetRef, promptVersion) {
9480
9561
  if (promptVersion !== void 0) {
9481
9562
  conditions.push(eq13(recommendationBriefs.promptVersion, promptVersion));
9482
9563
  }
9483
- return db.select().from(recommendationBriefs).where(and9(...conditions)).orderBy(desc6(recommendationBriefs.generatedAt)).limit(1).get();
9564
+ return db.select().from(recommendationBriefs).where(and10(...conditions)).orderBy(desc6(recommendationBriefs.generatedAt)).limit(1).get();
9484
9565
  }
9485
9566
  function parseLimitParam(raw) {
9486
9567
  if (raw === void 0) return void 0;
@@ -9506,7 +9587,7 @@ function winnabilityClassRank(winnabilityClass) {
9506
9587
  }
9507
9588
 
9508
9589
  // ../api-routes/src/gsc-totals.ts
9509
- import { and as and10, asc as asc2, eq as eq14, sql as sql6 } from "drizzle-orm";
9590
+ import { and as and11, asc as asc2, eq as eq14, sql as sql6 } from "drizzle-orm";
9510
9591
  function readGscDailyTotals(db, projectId, startDate, endDate) {
9511
9592
  const rows = db.select({
9512
9593
  date: gscDailyTotals.date,
@@ -9514,7 +9595,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
9514
9595
  impressions: gscDailyTotals.impressions,
9515
9596
  position: gscDailyTotals.position
9516
9597
  }).from(gscDailyTotals).where(
9517
- and10(
9598
+ and11(
9518
9599
  eq14(gscDailyTotals.projectId, projectId),
9519
9600
  sql6`${gscDailyTotals.date} >= ${startDate}`,
9520
9601
  sql6`${gscDailyTotals.date} <= ${endDate}`
@@ -11823,6 +11904,9 @@ function safeNum(value) {
11823
11904
  }
11824
11905
  return 0;
11825
11906
  }
11907
+ function isPaidAiTrafficClass(value) {
11908
+ return value === GA4AiReferralTrafficClasses.paid;
11909
+ }
11826
11910
  function categorizeQuery(query, projectBrandNames, canonicalDomain) {
11827
11911
  return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
11828
11912
  }
@@ -11945,7 +12029,7 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
11945
12029
  function buildGaSection(db, projectId, windowDays) {
11946
12030
  const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
11947
12031
  const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
11948
- and11(
12032
+ and12(
11949
12033
  eq15(gaTrafficWindowSummaries.projectId, projectId),
11950
12034
  eq15(gaTrafficWindowSummaries.windowKey, gaWindowKey)
11951
12035
  )
@@ -11977,14 +12061,40 @@ function buildGaSection(db, projectId, windowDays) {
11977
12061
  users: data.users,
11978
12062
  organicSessions: data.organic
11979
12063
  })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_LANDING_PAGES_LIMIT);
12064
+ const aiConditions = [
12065
+ eq15(gaAiReferrals.projectId, projectId),
12066
+ eq15(gaAiReferrals.sourceDimension, "session")
12067
+ ];
12068
+ if (snapshotStartDate && snapshotMaxDate) {
12069
+ aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
12070
+ aiConditions.push(lte(gaAiReferrals.date, snapshotMaxDate));
12071
+ }
12072
+ const aiSessionRows = db.select({
12073
+ trafficClass: gaAiReferrals.trafficClass,
12074
+ channelGroup: gaAiReferrals.channelGroup,
12075
+ sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`
12076
+ }).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.trafficClass, gaAiReferrals.channelGroup).all();
12077
+ let paidAiSessions = 0;
12078
+ let organicAiSessions = 0;
12079
+ let aiOrganicOverlap = 0;
12080
+ let aiDirectOverlap = 0;
12081
+ for (const row of aiSessionRows) {
12082
+ const sessions = Number(row.sessions ?? 0);
12083
+ if (isPaidAiTrafficClass(row.trafficClass)) paidAiSessions += sessions;
12084
+ else organicAiSessions += sessions;
12085
+ if (row.channelGroup === "Organic Search") aiOrganicOverlap += sessions;
12086
+ if (row.channelGroup === "Direct") aiDirectOverlap += sessions;
12087
+ }
11980
12088
  const channelBreakdown = [];
11981
12089
  if (totalSessions > 0) {
11982
- const organic = totalOrganicSessions;
11983
- const direct = directSessions;
11984
- const other = Math.max(totalSessions - organic - direct, 0);
12090
+ const organic = Math.max(0, totalOrganicSessions - Math.min(totalOrganicSessions, aiOrganicOverlap));
12091
+ const direct = Math.max(0, directSessions - Math.min(directSessions, aiDirectOverlap));
12092
+ const other = Math.max(totalSessions - organic - direct - paidAiSessions - organicAiSessions, 0);
11985
12093
  const buckets = [
11986
12094
  { channel: "Organic Search", sessions: organic },
11987
12095
  { channel: "Direct", sessions: direct },
12096
+ { channel: "Paid AI", sessions: paidAiSessions },
12097
+ { channel: "Organic AI referrals", sessions: organicAiSessions },
11988
12098
  { channel: "Other", sessions: other }
11989
12099
  ];
11990
12100
  for (const b of buckets) {
@@ -12071,15 +12181,34 @@ function buildAiReferrals(db, projectId) {
12071
12181
  );
12072
12182
  let total = 0;
12073
12183
  let totalUsers = 0;
12184
+ let paidSessions = 0;
12185
+ let paidUsers = 0;
12186
+ let organicSessions = 0;
12187
+ let organicUsers = 0;
12074
12188
  const sourceAgg = /* @__PURE__ */ new Map();
12075
12189
  const trendAgg = /* @__PURE__ */ new Map();
12076
12190
  const pageAgg = /* @__PURE__ */ new Map();
12077
12191
  for (const r of dedupedRows) {
12078
12192
  total += r.sessions;
12079
12193
  totalUsers += r.users;
12080
- const s = sourceAgg.get(r.source) ?? { sessions: 0, users: 0 };
12194
+ const paid = isPaidAiTrafficClass(r.trafficClass);
12195
+ if (paid) {
12196
+ paidSessions += r.sessions;
12197
+ paidUsers += r.users;
12198
+ } else {
12199
+ organicSessions += r.sessions;
12200
+ organicUsers += r.users;
12201
+ }
12202
+ const s = sourceAgg.get(r.source) ?? {
12203
+ sessions: 0,
12204
+ users: 0,
12205
+ paidSessions: 0,
12206
+ organicSessions: 0
12207
+ };
12081
12208
  s.sessions += r.sessions;
12082
12209
  s.users += r.users;
12210
+ if (paid) s.paidSessions += r.sessions;
12211
+ else s.organicSessions += r.sessions;
12083
12212
  sourceAgg.set(r.source, s);
12084
12213
  trendAgg.set(r.date, (trendAgg.get(r.date) ?? 0) + r.sessions);
12085
12214
  const page = r.landingPageNormalized ?? r.landingPage;
@@ -12092,11 +12221,23 @@ function buildAiReferrals(db, projectId) {
12092
12221
  source,
12093
12222
  sessions: data.sessions,
12094
12223
  users: data.users,
12224
+ paidSessions: data.paidSessions,
12225
+ organicSessions: data.organicSessions,
12095
12226
  sharePct: total > 0 ? Math.round(data.sessions / total * 100) : 0
12096
12227
  })).sort((a, b) => b.sessions - a.sessions);
12097
12228
  const trend = [...trendAgg.entries()].map(([date, sessions]) => ({ date, sessions })).sort((a, b) => a.date.localeCompare(b.date));
12098
12229
  const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions, users: data.users })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
12099
- return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
12230
+ return {
12231
+ totalSessions: total,
12232
+ totalUsers,
12233
+ paidSessions,
12234
+ paidUsers,
12235
+ organicSessions,
12236
+ organicUsers,
12237
+ bySource,
12238
+ trend,
12239
+ topLandingPages
12240
+ };
12100
12241
  }
12101
12242
  function nonSubresourceReferralPathCondition() {
12102
12243
  return sql7`
@@ -12123,7 +12264,7 @@ function nonSubresourceReferralPathCondition() {
12123
12264
  }
12124
12265
  function buildServerActivity(db, projectId, windowDays) {
12125
12266
  const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
12126
- and11(
12267
+ and12(
12127
12268
  eq15(trafficSources.projectId, projectId),
12128
12269
  ne2(trafficSources.status, TrafficSourceStatuses.archived)
12129
12270
  )
@@ -12139,7 +12280,7 @@ function buildServerActivity(db, projectId, windowDays) {
12139
12280
  const trendStart = new Date(trendStartMs).toISOString();
12140
12281
  const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12141
12282
  db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
12142
- and11(
12283
+ and12(
12143
12284
  eq15(crawlerEventsHourly.projectId, projectId),
12144
12285
  eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12145
12286
  gte2(crawlerEventsHourly.tsHour, windowStartIso),
@@ -12149,7 +12290,7 @@ function buildServerActivity(db, projectId, windowDays) {
12149
12290
  );
12150
12291
  const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12151
12292
  db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
12152
- and11(
12293
+ and12(
12153
12294
  eq15(crawlerEventsHourly.projectId, projectId),
12154
12295
  ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12155
12296
  gte2(crawlerEventsHourly.tsHour, windowStartIso),
@@ -12159,7 +12300,7 @@ function buildServerActivity(db, projectId, windowDays) {
12159
12300
  );
12160
12301
  const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12161
12302
  db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
12162
- and11(
12303
+ and12(
12163
12304
  eq15(aiReferralEventsHourly.projectId, projectId),
12164
12305
  nonSubresourceReferralPathCondition(),
12165
12306
  gte2(aiReferralEventsHourly.tsHour, windowStartIso),
@@ -12169,7 +12310,7 @@ function buildServerActivity(db, projectId, windowDays) {
12169
12310
  );
12170
12311
  const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
12171
12312
  db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
12172
- and11(
12313
+ and12(
12173
12314
  eq15(aiUserFetchEventsHourly.projectId, projectId),
12174
12315
  gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
12175
12316
  exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
@@ -12189,7 +12330,7 @@ function buildServerActivity(db, projectId, windowDays) {
12189
12330
  verificationStatus: crawlerEventsHourly.verificationStatus,
12190
12331
  hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
12191
12332
  }).from(crawlerEventsHourly).where(
12192
- and11(
12333
+ and12(
12193
12334
  eq15(crawlerEventsHourly.projectId, projectId),
12194
12335
  gte2(crawlerEventsHourly.tsHour, headlineStart),
12195
12336
  lte(crawlerEventsHourly.tsHour, headlineEnd)
@@ -12199,7 +12340,7 @@ function buildServerActivity(db, projectId, windowDays) {
12199
12340
  operator: crawlerEventsHourly.operator,
12200
12341
  hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
12201
12342
  }).from(crawlerEventsHourly).where(
12202
- and11(
12343
+ and12(
12203
12344
  eq15(crawlerEventsHourly.projectId, projectId),
12204
12345
  eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12205
12346
  gte2(crawlerEventsHourly.tsHour, priorStart),
@@ -12210,7 +12351,7 @@ function buildServerActivity(db, projectId, windowDays) {
12210
12351
  operator: aiReferralEventsHourly.operator,
12211
12352
  hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
12212
12353
  }).from(aiReferralEventsHourly).where(
12213
- and11(
12354
+ and12(
12214
12355
  eq15(aiReferralEventsHourly.projectId, projectId),
12215
12356
  nonSubresourceReferralPathCondition(),
12216
12357
  gte2(aiReferralEventsHourly.tsHour, headlineStart),
@@ -12221,7 +12362,7 @@ function buildServerActivity(db, projectId, windowDays) {
12221
12362
  operator: aiUserFetchEventsHourly.operator,
12222
12363
  hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
12223
12364
  }).from(aiUserFetchEventsHourly).where(
12224
- and11(
12365
+ and12(
12225
12366
  eq15(aiUserFetchEventsHourly.projectId, projectId),
12226
12367
  gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
12227
12368
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
@@ -12265,7 +12406,7 @@ function buildServerActivity(db, projectId, windowDays) {
12265
12406
  hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
12266
12407
  operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
12267
12408
  }).from(crawlerEventsHourly).where(
12268
- and11(
12409
+ and12(
12269
12410
  eq15(crawlerEventsHourly.projectId, projectId),
12270
12411
  eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12271
12412
  gte2(crawlerEventsHourly.tsHour, headlineStart),
@@ -12282,7 +12423,7 @@ function buildServerActivity(db, projectId, windowDays) {
12282
12423
  arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
12283
12424
  landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
12284
12425
  }).from(aiReferralEventsHourly).where(
12285
- and11(
12426
+ and12(
12286
12427
  eq15(aiReferralEventsHourly.projectId, projectId),
12287
12428
  nonSubresourceReferralPathCondition(),
12288
12429
  gte2(aiReferralEventsHourly.tsHour, headlineStart),
@@ -12299,7 +12440,7 @@ function buildServerActivity(db, projectId, windowDays) {
12299
12440
  arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
12300
12441
  products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
12301
12442
  }).from(aiReferralEventsHourly).where(
12302
- and11(
12443
+ and12(
12303
12444
  eq15(aiReferralEventsHourly.projectId, projectId),
12304
12445
  nonSubresourceReferralPathCondition(),
12305
12446
  gte2(aiReferralEventsHourly.tsHour, headlineStart),
@@ -12315,7 +12456,7 @@ function buildServerActivity(db, projectId, windowDays) {
12315
12456
  date: sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
12316
12457
  hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
12317
12458
  }).from(crawlerEventsHourly).where(
12318
- and11(
12459
+ and12(
12319
12460
  eq15(crawlerEventsHourly.projectId, projectId),
12320
12461
  eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
12321
12462
  gte2(crawlerEventsHourly.tsHour, trendStart),
@@ -12326,7 +12467,7 @@ function buildServerActivity(db, projectId, windowDays) {
12326
12467
  date: sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
12327
12468
  hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
12328
12469
  }).from(aiReferralEventsHourly).where(
12329
- and11(
12470
+ and12(
12330
12471
  eq15(aiReferralEventsHourly.projectId, projectId),
12331
12472
  nonSubresourceReferralPathCondition(),
12332
12473
  gte2(aiReferralEventsHourly.tsHour, trendStart),
@@ -12337,7 +12478,7 @@ function buildServerActivity(db, projectId, windowDays) {
12337
12478
  date: sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
12338
12479
  hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
12339
12480
  }).from(aiUserFetchEventsHourly).where(
12340
- and11(
12481
+ and12(
12341
12482
  eq15(aiUserFetchEventsHourly.projectId, projectId),
12342
12483
  gte2(aiUserFetchEventsHourly.tsHour, trendStart),
12343
12484
  lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
@@ -12422,7 +12563,7 @@ function buildIndexingHealth(db, projectId) {
12422
12563
  return null;
12423
12564
  }
12424
12565
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
12425
- const visibilityRuns = db.select().from(runs).where(and11(eq15(runs.projectId, projectId), eq15(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
12566
+ const visibilityRuns = db.select().from(runs).where(and12(eq15(runs.projectId, projectId), eq15(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
12426
12567
  const totalQueries = queryLookup.byId.size;
12427
12568
  const points = [];
12428
12569
  for (const run of visibilityRuns) {
@@ -12470,7 +12611,7 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
12470
12611
  }
12471
12612
  function buildInsightList(db, projectId, locationFilter) {
12472
12613
  const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
12473
- and11(
12614
+ and12(
12474
12615
  eq15(runs.projectId, projectId),
12475
12616
  eq15(runs.kind, RunKinds["answer-visibility"]),
12476
12617
  or3(eq15(runs.status, RunStatuses.completed), eq15(runs.status, RunStatuses.partial)),
@@ -12478,7 +12619,7 @@ function buildInsightList(db, projectId, locationFilter) {
12478
12619
  )
12479
12620
  ).orderBy(desc7(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
12480
12621
  if (recentRunIds.length === 0) return [];
12481
- const rows = db.select().from(insights).where(and11(eq15(insights.projectId, projectId), inArray6(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
12622
+ const rows = db.select().from(insights).where(and12(eq15(insights.projectId, projectId), inArray6(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
12482
12623
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
12483
12624
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
12484
12625
  const recommendation = r.recommendation;
@@ -13030,7 +13171,7 @@ function buildProjectReport(db, projectName, periodDays) {
13030
13171
  const project = resolveProject(db, projectName);
13031
13172
  const queryLookup = loadQueryLookup(db, project.id);
13032
13173
  const comparisonWindowDays = reportComparisonWindowDays(periodDays);
13033
- const allRuns = db.select().from(runs).where(and11(eq15(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
13174
+ const allRuns = db.select().from(runs).where(and12(eq15(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
13034
13175
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
13035
13176
  const completedVisRunGroups = groupRunsByCreatedAt(
13036
13177
  visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
@@ -13266,7 +13407,7 @@ async function reportRoutes(app) {
13266
13407
  }
13267
13408
 
13268
13409
  // ../api-routes/src/citations.ts
13269
- import { and as and12, eq as eq16, inArray as inArray7 } from "drizzle-orm";
13410
+ import { and as and13, eq as eq16, inArray as inArray7 } from "drizzle-orm";
13270
13411
  async function citationRoutes(app) {
13271
13412
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
13272
13413
  const project = resolveProject(app.db, request.params.name);
@@ -13275,7 +13416,7 @@ async function citationRoutes(app) {
13275
13416
  if (projectQueries.length === 0) {
13276
13417
  return reply.send(emptyCitationVisibility("no-queries"));
13277
13418
  }
13278
- const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and12(eq16(runs.projectId, project.id), notProbeRun())).all();
13419
+ const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and13(eq16(runs.projectId, project.id), notProbeRun())).all();
13279
13420
  if (projectRuns.length === 0) {
13280
13421
  return reply.send(emptyCitationVisibility("no-runs-yet"));
13281
13422
  }
@@ -13431,7 +13572,7 @@ function normalizeDomain2(domain) {
13431
13572
  }
13432
13573
 
13433
13574
  // ../api-routes/src/visibility-stats.ts
13434
- import { and as and13, desc as desc8, eq as eq17, inArray as inArray8 } from "drizzle-orm";
13575
+ import { and as and14, desc as desc8, eq as eq17, inArray as inArray8 } from "drizzle-orm";
13435
13576
  function round42(value) {
13436
13577
  return Math.round(value * 1e4) / 1e4;
13437
13578
  }
@@ -13583,7 +13724,7 @@ async function visibilityStatsRoutes(app) {
13583
13724
  lastRuns = n;
13584
13725
  }
13585
13726
  const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
13586
- let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and13(eq17(runs.projectId, project.id), eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
13727
+ let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq17(runs.projectId, project.id), eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
13587
13728
  if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
13588
13729
  if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
13589
13730
  if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
@@ -13653,7 +13794,7 @@ async function visibilityStatsRoutes(app) {
13653
13794
  }
13654
13795
 
13655
13796
  // ../api-routes/src/composites.ts
13656
- import { eq as eq18, and as and14, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
13797
+ import { eq as eq18, and as and15, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
13657
13798
  var TOP_INSIGHT_LIMIT = 5;
13658
13799
  var SEARCH_HIT_HARD_LIMIT = 50;
13659
13800
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -13671,7 +13812,7 @@ async function compositeRoutes(app) {
13671
13812
  const project = resolveProject(app.db, request.params.name);
13672
13813
  const filterLocation = (request.query.location ?? "").trim() || null;
13673
13814
  const sinceIso = parseSinceFilter(request.query.since);
13674
- const allRunsRaw = app.db.select().from(runs).where(and14(eq18(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
13815
+ const allRunsRaw = app.db.select().from(runs).where(and15(eq18(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
13675
13816
  const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
13676
13817
  const totalRuns = allRuns.length;
13677
13818
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
@@ -13827,7 +13968,7 @@ async function compositeRoutes(app) {
13827
13968
  rawResponse: querySnapshots.rawResponse,
13828
13969
  createdAt: querySnapshots.createdAt
13829
13970
  }).from(querySnapshots).innerJoin(queries, eq18(querySnapshots.queryId, queries.id)).where(
13830
- and14(
13971
+ and15(
13831
13972
  eq18(queries.projectId, project.id),
13832
13973
  or4(
13833
13974
  sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
@@ -13838,7 +13979,7 @@ async function compositeRoutes(app) {
13838
13979
  )
13839
13980
  ).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
13840
13981
  const insightMatches = app.db.select().from(insights).where(
13841
- and14(
13982
+ and15(
13842
13983
  eq18(insights.projectId, project.id),
13843
13984
  or4(
13844
13985
  like2(insights.title, pattern),
@@ -14055,7 +14196,7 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
14055
14196
  // NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
14056
14197
  // which the JS coerces to 0 — caught by the impression floor anyway).
14057
14198
  avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
14058
- }).from(gscSearchData).where(and14(
14199
+ }).from(gscSearchData).where(and15(
14059
14200
  eq18(gscSearchData.projectId, projectId),
14060
14201
  sql8`${gscSearchData.date} >= ${cutoff}`,
14061
14202
  sql8`${gscSearchData.impressions} > 0`
@@ -14570,6 +14711,13 @@ var queryIdParameter = {
14570
14711
  description: "Query ID.",
14571
14712
  schema: stringSchema
14572
14713
  };
14714
+ var competitorIdParameter = {
14715
+ name: "id",
14716
+ in: "path",
14717
+ required: true,
14718
+ description: "Competitor ID.",
14719
+ schema: stringSchema
14720
+ };
14573
14721
  var notificationIdParameter = {
14574
14722
  name: "id",
14575
14723
  in: "path",
@@ -15258,6 +15406,17 @@ var routeCatalog = [
15258
15406
  400: errorResponse("Invalid competitor delete request.")
15259
15407
  }
15260
15408
  },
15409
+ {
15410
+ method: "delete",
15411
+ path: "/api/v1/projects/{name}/competitors/{id}",
15412
+ summary: "Delete competitor",
15413
+ tags: ["competitors"],
15414
+ parameters: [nameParameter, competitorIdParameter],
15415
+ responses: {
15416
+ 204: { description: "Competitor deleted." },
15417
+ 404: errorResponse("Project or competitor not found.")
15418
+ }
15419
+ },
15261
15420
  {
15262
15421
  method: "post",
15263
15422
  path: "/api/v1/projects/{name}/runs",
@@ -18979,7 +19138,7 @@ async function telemetryRoutes(app, opts) {
18979
19138
 
18980
19139
  // ../api-routes/src/schedules.ts
18981
19140
  import crypto13 from "crypto";
18982
- import { and as and15, eq as eq20 } from "drizzle-orm";
19141
+ import { and as and16, eq as eq20 } from "drizzle-orm";
18983
19142
  function parseKindParam(raw) {
18984
19143
  if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
18985
19144
  const parsed = schedulableRunKindSchema.safeParse(raw);
@@ -19051,7 +19210,7 @@ async function scheduleRoutes(app, opts) {
19051
19210
  }
19052
19211
  const now = (/* @__PURE__ */ new Date()).toISOString();
19053
19212
  const enabledBool = enabled !== false;
19054
- const existing = app.db.select().from(schedules).where(and15(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19213
+ const existing = app.db.select().from(schedules).where(and16(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19055
19214
  if (existing) {
19056
19215
  app.db.update(schedules).set({
19057
19216
  cronExpr,
@@ -19085,13 +19244,13 @@ async function scheduleRoutes(app, opts) {
19085
19244
  diff: { kind, cronExpr, preset, timezone, providers, sourceId }
19086
19245
  });
19087
19246
  opts.onScheduleUpdated?.("upsert", project.id, kind);
19088
- const schedule = app.db.select().from(schedules).where(and15(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19247
+ const schedule = app.db.select().from(schedules).where(and16(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19089
19248
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
19090
19249
  });
19091
19250
  app.get("/projects/:name/schedule", async (request, reply) => {
19092
19251
  const project = resolveProject(app.db, request.params.name);
19093
19252
  const kind = parseKindParam(request.query?.kind);
19094
- const schedule = app.db.select().from(schedules).where(and15(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19253
+ const schedule = app.db.select().from(schedules).where(and16(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19095
19254
  if (!schedule) {
19096
19255
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
19097
19256
  }
@@ -19100,7 +19259,7 @@ async function scheduleRoutes(app, opts) {
19100
19259
  app.delete("/projects/:name/schedule", async (request, reply) => {
19101
19260
  const project = resolveProject(app.db, request.params.name);
19102
19261
  const kind = parseKindParam(request.query?.kind);
19103
- const schedule = app.db.select().from(schedules).where(and15(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19262
+ const schedule = app.db.select().from(schedules).where(and16(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
19104
19263
  if (!schedule) {
19105
19264
  throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
19106
19265
  }
@@ -19257,7 +19416,7 @@ function formatNotification(row) {
19257
19416
 
19258
19417
  // ../api-routes/src/google.ts
19259
19418
  import crypto18 from "crypto";
19260
- import { eq as eq22, and as and16, desc as desc11, sql as sql9, inArray as inArray10 } from "drizzle-orm";
19419
+ import { eq as eq22, and as and17, desc as desc11, sql as sql9, inArray as inArray10 } from "drizzle-orm";
19261
19420
 
19262
19421
  // ../api-routes/src/gbp-summary.ts
19263
19422
  function computeMetricTotals(rows) {
@@ -20383,16 +20542,23 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
20383
20542
  };
20384
20543
  const response = await runReport(accessToken, propertyId, request);
20385
20544
  if (!response) break;
20386
- const pageRows = (response.rows ?? []).map((row) => ({
20387
- date: row.dimensionValues[0].value,
20388
- source: row.dimensionValues[1].value,
20389
- medium: row.dimensionValues[2].value,
20390
- channelGroup: row.dimensionValues[3]?.value ?? "(not set)",
20391
- landingPage: row.dimensionValues[4]?.value ?? "(not set)",
20392
- sessions: parseInt(row.metricValues[0].value, 10) || 0,
20393
- users: parseInt(row.metricValues[1].value, 10) || 0,
20394
- sourceDimension: dimLabel
20395
- }));
20545
+ const pageRows = (response.rows ?? []).map((row) => {
20546
+ const source = row.dimensionValues[1].value;
20547
+ const medium = row.dimensionValues[2].value;
20548
+ const channelGroup = row.dimensionValues[3]?.value ?? "(not set)";
20549
+ const landingPage = row.dimensionValues[4]?.value ?? "(not set)";
20550
+ return {
20551
+ date: row.dimensionValues[0].value,
20552
+ source,
20553
+ medium,
20554
+ trafficClass: classifyGa4AiReferralTrafficClass({ source, medium, channelGroup, landingPage }),
20555
+ channelGroup,
20556
+ landingPage,
20557
+ sessions: parseInt(row.metricValues[0].value, 10) || 0,
20558
+ users: parseInt(row.metricValues[1].value, 10) || 0,
20559
+ sourceDimension: dimLabel
20560
+ };
20561
+ });
20396
20562
  rows.push(...pageRows);
20397
20563
  const totalRows = response.rowCount ?? 0;
20398
20564
  offset += pageRows.length;
@@ -21237,7 +21403,7 @@ async function googleRoutes(app, opts) {
21237
21403
  if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
21238
21404
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
21239
21405
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
21240
- const rows = app.db.select().from(gscSearchData).where(and16(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
21406
+ const rows = app.db.select().from(gscSearchData).where(and17(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
21241
21407
  return rows.map((r) => ({
21242
21408
  date: r.date,
21243
21409
  query: r.query,
@@ -21265,7 +21431,7 @@ async function googleRoutes(app, opts) {
21265
21431
  date: gscSearchData.date,
21266
21432
  clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
21267
21433
  impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
21268
- }).from(gscSearchData).where(and16(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
21434
+ }).from(gscSearchData).where(and17(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
21269
21435
  const daily = mergeGscDailyTotalsWithFallback(
21270
21436
  dailyTotals,
21271
21437
  dimensionedRows.map((r) => ({
@@ -21358,7 +21524,7 @@ async function googleRoutes(app, opts) {
21358
21524
  const { url, limit } = request.query;
21359
21525
  const conditions = [eq22(gscUrlInspections.projectId, project.id)];
21360
21526
  if (url) conditions.push(eq22(gscUrlInspections.url, url));
21361
- const rows = app.db.select().from(gscUrlInspections).where(and16(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
21527
+ const rows = app.db.select().from(gscUrlInspections).where(and17(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
21362
21528
  return rows.map((r) => ({
21363
21529
  id: r.id,
21364
21530
  url: r.url,
@@ -21853,7 +22019,7 @@ async function googleRoutes(app, opts) {
21853
22019
  app.db.transaction((tx) => {
21854
22020
  if (switching) clearGbpProjectData(tx, project.id);
21855
22021
  for (const remote of remoteLocations) {
21856
- const existing = tx.select().from(gbpLocations).where(and16(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, remote.name))).get();
22022
+ const existing = tx.select().from(gbpLocations).where(and17(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, remote.name))).get();
21857
22023
  const profile = buildLocationProfileFields(remote);
21858
22024
  if (existing) {
21859
22025
  tx.update(gbpLocations).set({
@@ -21950,7 +22116,7 @@ async function googleRoutes(app, opts) {
21950
22116
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
21951
22117
  }
21952
22118
  const { selected } = parsed.data;
21953
- const existing = app.db.select().from(gbpLocations).where(and16(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, locationName))).get();
22119
+ const existing = app.db.select().from(gbpLocations).where(and17(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, locationName))).get();
21954
22120
  if (!existing) throw notFound("GBP location", locationName);
21955
22121
  const now = (/* @__PURE__ */ new Date()).toISOString();
21956
22122
  app.db.transaction((tx) => {
@@ -22010,7 +22176,7 @@ async function googleRoutes(app, opts) {
22010
22176
  const conditions = [eq22(gbpDailyMetrics.projectId, project.id)];
22011
22177
  if (request.query.locationName) conditions.push(eq22(gbpDailyMetrics.locationName, request.query.locationName));
22012
22178
  if (request.query.metric) conditions.push(eq22(gbpDailyMetrics.metric, request.query.metric));
22013
- const rows = app.db.select().from(gbpDailyMetrics).where(and16(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
22179
+ const rows = app.db.select().from(gbpDailyMetrics).where(and17(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
22014
22180
  return {
22015
22181
  metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
22016
22182
  total: rows.length
@@ -22020,7 +22186,7 @@ async function googleRoutes(app, opts) {
22020
22186
  const project = resolveProject(app.db, request.params.name);
22021
22187
  const conditions = [eq22(gbpKeywordImpressions.projectId, project.id)];
22022
22188
  if (request.query.locationName) conditions.push(eq22(gbpKeywordImpressions.locationName, request.query.locationName));
22023
- const rows = app.db.select().from(gbpKeywordImpressions).where(and16(...conditions)).all();
22189
+ const rows = app.db.select().from(gbpKeywordImpressions).where(and17(...conditions)).all();
22024
22190
  rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
22025
22191
  const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
22026
22192
  return {
@@ -22040,7 +22206,7 @@ async function googleRoutes(app, opts) {
22040
22206
  const project = resolveProject(app.db, request.params.name);
22041
22207
  const conditions = [eq22(gbpPlaceActions.projectId, project.id)];
22042
22208
  if (request.query.locationName) conditions.push(eq22(gbpPlaceActions.locationName, request.query.locationName));
22043
- const rows = app.db.select().from(gbpPlaceActions).where(and16(...conditions)).all();
22209
+ const rows = app.db.select().from(gbpPlaceActions).where(and17(...conditions)).all();
22044
22210
  return {
22045
22211
  placeActions: rows.map((r) => ({
22046
22212
  locationName: r.locationName,
@@ -22057,7 +22223,7 @@ async function googleRoutes(app, opts) {
22057
22223
  const project = resolveProject(app.db, request.params.name);
22058
22224
  const conditions = [eq22(gbpLodgingSnapshots.projectId, project.id)];
22059
22225
  if (request.query.locationName) conditions.push(eq22(gbpLodgingSnapshots.locationName, request.query.locationName));
22060
- const rows = app.db.select().from(gbpLodgingSnapshots).where(and16(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22226
+ const rows = app.db.select().from(gbpLodgingSnapshots).where(and17(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22061
22227
  const latestByLocation = /* @__PURE__ */ new Map();
22062
22228
  for (const row of rows) {
22063
22229
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22074,7 +22240,7 @@ async function googleRoutes(app, opts) {
22074
22240
  const project = resolveProject(app.db, request.params.name);
22075
22241
  const conditions = [eq22(gbpAttributesSnapshots.projectId, project.id)];
22076
22242
  if (request.query.locationName) conditions.push(eq22(gbpAttributesSnapshots.locationName, request.query.locationName));
22077
- const rows = app.db.select().from(gbpAttributesSnapshots).where(and16(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
22243
+ const rows = app.db.select().from(gbpAttributesSnapshots).where(and17(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
22078
22244
  const latestByLocation = /* @__PURE__ */ new Map();
22079
22245
  for (const row of rows) {
22080
22246
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22094,7 +22260,7 @@ async function googleRoutes(app, opts) {
22094
22260
  const project = resolveProject(app.db, request.params.name);
22095
22261
  const conditions = [eq22(gbpPlaceDetails.projectId, project.id)];
22096
22262
  if (request.query.locationName) conditions.push(eq22(gbpPlaceDetails.locationName, request.query.locationName));
22097
- const rows = app.db.select().from(gbpPlaceDetails).where(and16(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
22263
+ const rows = app.db.select().from(gbpPlaceDetails).where(and17(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
22098
22264
  const latestByLocation = /* @__PURE__ */ new Map();
22099
22265
  for (const row of rows) {
22100
22266
  if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
@@ -22113,7 +22279,7 @@ async function googleRoutes(app, opts) {
22113
22279
  app.get("/projects/:name/gbp/summary", async (request) => {
22114
22280
  const project = resolveProject(app.db, request.params.name);
22115
22281
  const locationName = request.query.locationName ?? null;
22116
- const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and16(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.selected, true))).all().map((r) => r.n);
22282
+ const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.selected, true))).all().map((r) => r.n);
22117
22283
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22118
22284
  if (locationNames.length === 0) {
22119
22285
  return buildGbpSummary({
@@ -22127,10 +22293,10 @@ async function googleRoutes(app, opts) {
22127
22293
  locationProfiles: []
22128
22294
  });
22129
22295
  }
22130
- const metricRows = app.db.select().from(gbpDailyMetrics).where(and16(eq22(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
22131
- const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and16(eq22(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
22132
- const placeActionRows = app.db.select().from(gbpPlaceActions).where(and16(eq22(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
22133
- const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and16(eq22(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22296
+ const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(eq22(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
22297
+ const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(eq22(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
22298
+ const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(eq22(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
22299
+ const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(eq22(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
22134
22300
  const latestLodgingByLocation = /* @__PURE__ */ new Map();
22135
22301
  for (const row of lodgingRows) {
22136
22302
  if (!latestLodgingByLocation.has(row.locationName)) {
@@ -22144,7 +22310,7 @@ async function googleRoutes(app, opts) {
22144
22310
  regularHours: gbpLocations.regularHours,
22145
22311
  primaryPhone: gbpLocations.primaryPhone,
22146
22312
  openStatus: gbpLocations.openStatus
22147
- }).from(gbpLocations).where(and16(eq22(gbpLocations.projectId, project.id), inArray10(gbpLocations.locationName, locationNames))).all();
22313
+ }).from(gbpLocations).where(and17(eq22(gbpLocations.projectId, project.id), inArray10(gbpLocations.locationName, locationNames))).all();
22148
22314
  return buildGbpSummary({
22149
22315
  locationName,
22150
22316
  locationCount: locationNames.length,
@@ -22160,7 +22326,7 @@ async function googleRoutes(app, opts) {
22160
22326
 
22161
22327
  // ../api-routes/src/ads.ts
22162
22328
  import crypto19 from "crypto";
22163
- import { eq as eq23, and as and17, asc as asc3, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
22329
+ import { eq as eq23, and as and18, asc as asc3, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
22164
22330
  function statusDto(row) {
22165
22331
  if (!row) return { connected: false };
22166
22332
  return {
@@ -22276,7 +22442,7 @@ async function adsRoutes(app, opts) {
22276
22442
  if (!row) {
22277
22443
  throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
22278
22444
  }
22279
- const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and17(
22445
+ const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and18(
22280
22446
  eq23(runs.projectId, project.id),
22281
22447
  eq23(runs.kind, RunKinds["ads-sync"]),
22282
22448
  inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
@@ -22361,7 +22527,7 @@ async function adsRoutes(app, opts) {
22361
22527
  if (entityId) conditions.push(eq23(adsInsightsDaily.entityId, entityId));
22362
22528
  if (from) conditions.push(gte3(adsInsightsDaily.date, from));
22363
22529
  if (to) conditions.push(lte2(adsInsightsDaily.date, to));
22364
- const rows = app.db.select().from(adsInsightsDaily).where(and17(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
22530
+ const rows = app.db.select().from(adsInsightsDaily).where(and18(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
22365
22531
  const dtoRows = rows.map((row) => ({
22366
22532
  level: row.level,
22367
22533
  entityId: row.entityId,
@@ -22383,7 +22549,7 @@ async function adsRoutes(app, opts) {
22383
22549
  const campaignCount = app.db.select().from(adsCampaigns).where(eq23(adsCampaigns.projectId, project.id)).all().length;
22384
22550
  const adGroupCount = app.db.select().from(adsAdGroups).where(eq23(adsAdGroups.projectId, project.id)).all().length;
22385
22551
  const adCount = app.db.select().from(adsAds).where(eq23(adsAds.projectId, project.id)).all().length;
22386
- const campaignInsights = app.db.select().from(adsInsightsDaily).where(and17(
22552
+ const campaignInsights = app.db.select().from(adsInsightsDaily).where(and18(
22387
22553
  eq23(adsInsightsDaily.projectId, project.id),
22388
22554
  eq23(adsInsightsDaily.level, "campaign")
22389
22555
  )).all();
@@ -22425,7 +22591,7 @@ async function adsRoutes(app, opts) {
22425
22591
 
22426
22592
  // ../api-routes/src/bing.ts
22427
22593
  import crypto20 from "crypto";
22428
- import { eq as eq24, and as and18, desc as desc12 } from "drizzle-orm";
22594
+ import { eq as eq24, and as and19, desc as desc12 } from "drizzle-orm";
22429
22595
 
22430
22596
  // ../integration-bing/src/constants.ts
22431
22597
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -22900,7 +23066,7 @@ async function bingRoutes(app, opts) {
22900
23066
  requireConnectionStore();
22901
23067
  const project = resolveProject(app.db, request.params.name);
22902
23068
  const { url, limit } = request.query;
22903
- const whereClause = url ? and18(eq24(bingUrlInspections.projectId, project.id), eq24(bingUrlInspections.url, url)) : eq24(bingUrlInspections.projectId, project.id);
23069
+ const whereClause = url ? and19(eq24(bingUrlInspections.projectId, project.id), eq24(bingUrlInspections.url, url)) : eq24(bingUrlInspections.projectId, project.id);
22904
23070
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
22905
23071
  return filtered.map((r) => ({
22906
23072
  id: r.id,
@@ -23132,7 +23298,7 @@ async function bingRoutes(app, opts) {
23132
23298
  import fs from "fs";
23133
23299
  import path from "path";
23134
23300
  import os from "os";
23135
- import { eq as eq25, and as and19 } from "drizzle-orm";
23301
+ import { eq as eq25, and as and20 } from "drizzle-orm";
23136
23302
  function getScreenshotDir() {
23137
23303
  return path.join(os.homedir(), ".canonry", "screenshots");
23138
23304
  }
@@ -23207,7 +23373,7 @@ async function cdpRoutes(app, opts) {
23207
23373
  async (request, reply) => {
23208
23374
  const project = resolveProject(app.db, request.params.name);
23209
23375
  const { runId } = request.params;
23210
- const run = app.db.select().from(runs).where(and19(eq25(runs.id, runId), eq25(runs.projectId, project.id))).get();
23376
+ const run = app.db.select().from(runs).where(and20(eq25(runs.id, runId), eq25(runs.projectId, project.id))).get();
23211
23377
  if (!run) {
23212
23378
  const err = notFound("Run", runId);
23213
23379
  return reply.code(err.statusCode).send(err.toJSON());
@@ -23304,7 +23470,7 @@ async function cdpRoutes(app, opts) {
23304
23470
 
23305
23471
  // ../api-routes/src/ga.ts
23306
23472
  import crypto21 from "crypto";
23307
- import { eq as eq26, desc as desc13, and as and20, sql as sql10 } from "drizzle-orm";
23473
+ import { eq as eq26, desc as desc13, and as and21, sql as sql10 } from "drizzle-orm";
23308
23474
  function gaLog(level, action, ctx) {
23309
23475
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
23310
23476
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -23360,6 +23526,76 @@ function pickWinningDimension(rows, tupleKey) {
23360
23526
  }
23361
23527
  return [...winners.values()].sort((a, b) => (b.sessions ?? 0) - (a.sessions ?? 0));
23362
23528
  }
23529
+ function normalizeAiTrafficClass(value) {
23530
+ return value === GA4AiReferralTrafficClasses.paid ? GA4AiReferralTrafficClasses.paid : GA4AiReferralTrafficClasses.organic;
23531
+ }
23532
+ function emptyAiCounts() {
23533
+ return { sessions: 0, users: 0 };
23534
+ }
23535
+ function summarizeAiReferralCounts(rows) {
23536
+ const dedupeGroups = /* @__PURE__ */ new Map();
23537
+ const bySessionChannelGroup = /* @__PURE__ */ new Map();
23538
+ const paidDeduped = emptyAiCounts();
23539
+ const organicDeduped = emptyAiCounts();
23540
+ const paidBySession = emptyAiCounts();
23541
+ const organicBySession = emptyAiCounts();
23542
+ for (const row of rows) {
23543
+ const isPaid = normalizeAiTrafficClass(row.trafficClass) === GA4AiReferralTrafficClasses.paid;
23544
+ const sessions = row.sessions ?? 0;
23545
+ const users = row.users ?? 0;
23546
+ const key = `${row.date}\0${row.source}\0${row.medium}`;
23547
+ let byDimension = dedupeGroups.get(key);
23548
+ if (!byDimension) {
23549
+ byDimension = /* @__PURE__ */ new Map();
23550
+ dedupeGroups.set(key, byDimension);
23551
+ }
23552
+ let dim = byDimension.get(row.sourceDimension);
23553
+ if (!dim) {
23554
+ dim = { paidSessions: 0, organicSessions: 0, paidUsers: 0, organicUsers: 0 };
23555
+ byDimension.set(row.sourceDimension, dim);
23556
+ }
23557
+ if (isPaid) {
23558
+ dim.paidSessions += sessions;
23559
+ dim.paidUsers += users;
23560
+ } else {
23561
+ dim.organicSessions += sessions;
23562
+ dim.organicUsers += users;
23563
+ }
23564
+ if (row.sourceDimension === "session") {
23565
+ bySessionChannelGroup.set(
23566
+ row.channelGroup,
23567
+ (bySessionChannelGroup.get(row.channelGroup) ?? 0) + sessions
23568
+ );
23569
+ const sessionBucket = isPaid ? paidBySession : organicBySession;
23570
+ sessionBucket.sessions += sessions;
23571
+ sessionBucket.users += users;
23572
+ }
23573
+ }
23574
+ for (const byDimension of dedupeGroups.values()) {
23575
+ const dims = [...byDimension.values()];
23576
+ const bestSessions = dims.reduce((best, d) => d.paidSessions + d.organicSessions > best.paidSessions + best.organicSessions ? d : best);
23577
+ const bestUsers = dims.reduce((best, d) => d.paidUsers + d.organicUsers > best.paidUsers + best.organicUsers ? d : best);
23578
+ paidDeduped.sessions += bestSessions.paidSessions;
23579
+ organicDeduped.sessions += bestSessions.organicSessions;
23580
+ paidDeduped.users += bestUsers.paidUsers;
23581
+ organicDeduped.users += bestUsers.organicUsers;
23582
+ }
23583
+ return {
23584
+ paidDeduped,
23585
+ organicDeduped,
23586
+ paidBySession,
23587
+ organicBySession,
23588
+ bySessionChannelGroup,
23589
+ deduped: {
23590
+ sessions: paidDeduped.sessions + organicDeduped.sessions,
23591
+ users: paidDeduped.users + organicDeduped.users
23592
+ },
23593
+ bySession: {
23594
+ sessions: paidBySession.sessions + organicBySession.sessions,
23595
+ users: paidBySession.users + organicBySession.users
23596
+ }
23597
+ };
23598
+ }
23363
23599
  async function refreshOAuthTokenIfNeeded(googleStore, authConfig, canonicalDomain, oauthConn) {
23364
23600
  const expiresAt = oauthConn.tokenExpiresAt ? new Date(oauthConn.tokenExpiresAt).getTime() : 0;
23365
23601
  const fiveMinutes = 5 * 60 * 1e3;
@@ -23599,7 +23835,7 @@ async function ga4Routes(app, opts) {
23599
23835
  app.db.transaction((tx) => {
23600
23836
  if (syncTraffic) {
23601
23837
  tx.delete(gaTrafficSnapshots).where(
23602
- and20(
23838
+ and21(
23603
23839
  eq26(gaTrafficSnapshots.projectId, project.id),
23604
23840
  sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
23605
23841
  sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
@@ -23623,7 +23859,7 @@ async function ga4Routes(app, opts) {
23623
23859
  }
23624
23860
  if (syncAi) {
23625
23861
  tx.delete(gaAiReferrals).where(
23626
- and20(
23862
+ and21(
23627
23863
  eq26(gaAiReferrals.projectId, project.id),
23628
23864
  sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
23629
23865
  sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
@@ -23636,6 +23872,12 @@ async function ga4Routes(app, opts) {
23636
23872
  date: row.date,
23637
23873
  source: row.source,
23638
23874
  medium: row.medium,
23875
+ trafficClass: row.trafficClass ?? classifyGa4AiReferralTrafficClass({
23876
+ source: row.source,
23877
+ medium: row.medium,
23878
+ channelGroup: row.channelGroup,
23879
+ landingPage: row.landingPage
23880
+ }),
23639
23881
  sourceDimension: row.sourceDimension,
23640
23882
  channelGroup: row.channelGroup,
23641
23883
  landingPage: row.landingPage,
@@ -23649,7 +23891,7 @@ async function ga4Routes(app, opts) {
23649
23891
  }
23650
23892
  if (syncSocial) {
23651
23893
  tx.delete(gaSocialReferrals).where(
23652
- and20(
23894
+ and21(
23653
23895
  eq26(gaSocialReferrals.projectId, project.id),
23654
23896
  sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
23655
23897
  sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
@@ -23753,7 +23995,7 @@ async function ga4Routes(app, opts) {
23753
23995
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
23754
23996
  totalUsers: gaTrafficWindowSummaries.totalUsers
23755
23997
  }).from(gaTrafficWindowSummaries).where(
23756
- and20(
23998
+ and21(
23757
23999
  eq26(gaTrafficWindowSummaries.projectId, project.id),
23758
24000
  eq26(gaTrafficWindowSummaries.windowKey, window)
23759
24001
  )
@@ -23762,7 +24004,7 @@ async function ga4Routes(app, opts) {
23762
24004
  totalSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
23763
24005
  totalOrganicSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
23764
24006
  totalUsers: sql10`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
23765
- }).from(gaTrafficSnapshots).where(and20(...snapshotConditions)).get() : null;
24007
+ }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get() : null;
23766
24008
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
23767
24009
  totalSessions: gaTrafficSummaries.totalSessions,
23768
24010
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
@@ -23770,7 +24012,7 @@ async function ga4Routes(app, opts) {
23770
24012
  }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).get();
23771
24013
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
23772
24014
  totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
23773
- }).from(gaTrafficSnapshots).where(and20(...snapshotConditions)).get();
24015
+ }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get();
23774
24016
  const summaryMeta = app.db.select({
23775
24017
  periodStart: gaTrafficSummaries.periodStart,
23776
24018
  periodEnd: gaTrafficSummaries.periodEnd
@@ -23781,77 +24023,60 @@ async function ga4Routes(app, opts) {
23781
24023
  organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
23782
24024
  directSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
23783
24025
  users: sql10`SUM(${gaTrafficSnapshots.users})`
23784
- }).from(gaTrafficSnapshots).where(and20(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
24026
+ }).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
23785
24027
  const aiReferralRows = app.db.select({
23786
24028
  source: gaAiReferrals.source,
23787
24029
  medium: gaAiReferrals.medium,
24030
+ trafficClass: gaAiReferrals.trafficClass,
23788
24031
  sourceDimension: gaAiReferrals.sourceDimension,
23789
24032
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
23790
24033
  users: sql10`SUM(${gaAiReferrals.users})`
23791
- }).from(gaAiReferrals).where(and20(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
24034
+ }).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.trafficClass, gaAiReferrals.sourceDimension).all();
23792
24035
  const aiReferralLandingPageRows = app.db.select({
23793
24036
  source: gaAiReferrals.source,
23794
24037
  medium: gaAiReferrals.medium,
24038
+ trafficClass: gaAiReferrals.trafficClass,
23795
24039
  sourceDimension: gaAiReferrals.sourceDimension,
23796
24040
  landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
23797
24041
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
23798
24042
  users: sql10`SUM(${gaAiReferrals.users})`
23799
- }).from(gaAiReferrals).where(and20(...aiConditions)).groupBy(
24043
+ }).from(gaAiReferrals).where(and21(...aiConditions)).groupBy(
23800
24044
  gaAiReferrals.source,
23801
24045
  gaAiReferrals.medium,
24046
+ gaAiReferrals.trafficClass,
23802
24047
  gaAiReferrals.sourceDimension,
23803
24048
  sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
23804
24049
  ).all();
24050
+ const aiRowsForTotals = app.db.select({
24051
+ date: gaAiReferrals.date,
24052
+ source: gaAiReferrals.source,
24053
+ medium: gaAiReferrals.medium,
24054
+ trafficClass: gaAiReferrals.trafficClass,
24055
+ sourceDimension: gaAiReferrals.sourceDimension,
24056
+ channelGroup: gaAiReferrals.channelGroup,
24057
+ sessions: gaAiReferrals.sessions,
24058
+ users: gaAiReferrals.users
24059
+ }).from(gaAiReferrals).where(and21(...aiConditions)).all();
23805
24060
  const aiReferrals = pickWinningDimension(
23806
24061
  aiReferralRows,
23807
- (r) => `${r.source}\0${r.medium}`
24062
+ (r) => `${r.source}\0${r.medium}\0${r.trafficClass}`
23808
24063
  );
23809
24064
  const aiReferralLandingPages = pickWinningDimension(
23810
24065
  aiReferralLandingPageRows,
23811
- (r) => `${r.source}\0${r.medium}\0${r.landingPage}`
24066
+ (r) => `${r.source}\0${r.medium}\0${r.trafficClass}\0${r.landingPage}`
23812
24067
  );
23813
- const aiDeduped = app.db.select({
23814
- sessions: sql10`COALESCE(SUM(max_sessions), 0)`,
23815
- users: sql10`COALESCE(SUM(max_users), 0)`
23816
- }).from(
23817
- sql10`(
23818
- SELECT date, source, medium,
23819
- MAX(dimension_sessions) AS max_sessions,
23820
- MAX(dimension_users) AS max_users
23821
- FROM (
23822
- SELECT date, source, medium, source_dimension,
23823
- SUM(sessions) AS dimension_sessions,
23824
- SUM(users) AS dimension_users
23825
- FROM ga_ai_referrals
23826
- WHERE project_id = ${project.id}${cutoffDate ? sql10` AND date >= ${cutoffDate}` : sql10``}
23827
- GROUP BY date, source, medium, source_dimension
23828
- )
23829
- GROUP BY date, source, medium
23830
- )`
23831
- ).get();
23832
- const aiBySessionRows = app.db.select({
23833
- channelGroup: gaAiReferrals.channelGroup,
23834
- sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
23835
- users: sql10`COALESCE(SUM(${gaAiReferrals.users}), 0)`
23836
- }).from(gaAiReferrals).where(and20(...aiConditions, eq26(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
23837
- const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
23838
- let aiBySessionUsers = 0;
23839
- for (const row of aiBySessionRows) {
23840
- aiSessionsByChannelGroup.set(row.channelGroup, row.sessions ?? 0);
23841
- aiBySessionUsers += row.users ?? 0;
23842
- }
23843
- const aiBySessionSessions = [...aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
24068
+ const aiSummary = summarizeAiReferralCounts(aiRowsForTotals);
23844
24069
  const socialReferrals = app.db.select({
23845
24070
  source: gaSocialReferrals.source,
23846
24071
  medium: gaSocialReferrals.medium,
23847
24072
  channelGroup: gaSocialReferrals.channelGroup,
23848
24073
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
23849
24074
  users: sql10`SUM(${gaSocialReferrals.users})`
23850
- }).from(gaSocialReferrals).where(and20(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
24075
+ }).from(gaSocialReferrals).where(and21(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
23851
24076
  const socialTotals = app.db.select({
23852
24077
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
23853
24078
  users: sql10`SUM(${gaSocialReferrals.users})`
23854
- }).from(gaSocialReferrals).where(and20(...socialConditions)).get();
24079
+ }).from(gaSocialReferrals).where(and21(...socialConditions)).get();
23855
24080
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
23856
24081
  const total = summaryRow?.totalSessions ?? 0;
23857
24082
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
@@ -23862,7 +24087,7 @@ async function ga4Routes(app, opts) {
23862
24087
  organicSessions: totalOrganicSessions,
23863
24088
  socialSessions,
23864
24089
  directSessions: totalDirectSessions,
23865
- aiSessionsByChannelGroup
24090
+ aiSessionsByChannelGroup: aiSummary.bySessionChannelGroup
23866
24091
  });
23867
24092
  return {
23868
24093
  totalSessions: total,
@@ -23879,6 +24104,7 @@ async function ga4Routes(app, opts) {
23879
24104
  aiReferrals: aiReferrals.map((r) => ({
23880
24105
  source: r.source,
23881
24106
  medium: r.medium,
24107
+ trafficClass: normalizeAiTrafficClass(r.trafficClass),
23882
24108
  sourceDimension: r.sourceDimension,
23883
24109
  sessions: r.sessions ?? 0,
23884
24110
  users: r.users ?? 0
@@ -23886,15 +24112,24 @@ async function ga4Routes(app, opts) {
23886
24112
  aiReferralLandingPages: aiReferralLandingPages.map((r) => ({
23887
24113
  source: r.source,
23888
24114
  medium: r.medium,
24115
+ trafficClass: normalizeAiTrafficClass(r.trafficClass),
23889
24116
  sourceDimension: r.sourceDimension,
23890
24117
  landingPage: r.landingPage,
23891
24118
  sessions: r.sessions ?? 0,
23892
24119
  users: r.users ?? 0
23893
24120
  })),
23894
- aiSessionsDeduped: aiDeduped?.sessions ?? 0,
23895
- aiUsersDeduped: aiDeduped?.users ?? 0,
23896
- aiSessionsBySession: aiBySessionSessions,
23897
- aiUsersBySession: aiBySessionUsers,
24121
+ aiSessionsDeduped: aiSummary.deduped.sessions,
24122
+ aiUsersDeduped: aiSummary.deduped.users,
24123
+ paidAiSessionsDeduped: aiSummary.paidDeduped.sessions,
24124
+ paidAiUsersDeduped: aiSummary.paidDeduped.users,
24125
+ organicAiSessionsDeduped: aiSummary.organicDeduped.sessions,
24126
+ organicAiUsersDeduped: aiSummary.organicDeduped.users,
24127
+ aiSessionsBySession: aiSummary.bySession.sessions,
24128
+ aiUsersBySession: aiSummary.bySession.users,
24129
+ paidAiSessionsBySession: aiSummary.paidBySession.sessions,
24130
+ paidAiUsersBySession: aiSummary.paidBySession.users,
24131
+ organicAiSessionsBySession: aiSummary.organicBySession.sessions,
24132
+ organicAiUsersBySession: aiSummary.organicBySession.users,
23898
24133
  socialReferrals: socialReferrals.map((r) => ({
23899
24134
  source: r.source,
23900
24135
  medium: r.medium,
@@ -23906,16 +24141,24 @@ async function ga4Routes(app, opts) {
23906
24141
  socialUsers: socialTotals?.users ?? 0,
23907
24142
  channelBreakdown,
23908
24143
  organicSharePct: total > 0 ? Math.round(totalOrganicSessions / total * 100) : 0,
23909
- aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
23910
- aiSharePctBySession: total > 0 ? Math.round(aiBySessionSessions / total * 100) : 0,
24144
+ aiSharePct: total > 0 ? Math.round(aiSummary.deduped.sessions / total * 100) : 0,
24145
+ aiSharePctBySession: total > 0 ? Math.round(aiSummary.bySession.sessions / total * 100) : 0,
24146
+ paidAiSharePct: total > 0 ? Math.round(aiSummary.paidDeduped.sessions / total * 100) : 0,
24147
+ paidAiSharePctBySession: total > 0 ? Math.round(aiSummary.paidBySession.sessions / total * 100) : 0,
24148
+ organicAiSharePct: total > 0 ? Math.round(aiSummary.organicDeduped.sessions / total * 100) : 0,
24149
+ organicAiSharePctBySession: total > 0 ? Math.round(aiSummary.organicBySession.sessions / total * 100) : 0,
23911
24150
  directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
23912
24151
  socialSharePct: total > 0 ? Math.round(socialSessions / total * 100) : 0,
23913
24152
  otherSessions: channelBreakdown.other.sessions,
23914
24153
  otherSharePct: channelBreakdown.other.sharePct,
23915
24154
  otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
23916
24155
  organicSharePctDisplay: formatSharePct(totalOrganicSessions, total),
23917
- aiSharePctDisplay: formatSharePct(aiDeduped?.sessions ?? 0, total),
23918
- aiSharePctBySessionDisplay: formatSharePct(aiBySessionSessions, total),
24156
+ aiSharePctDisplay: formatSharePct(aiSummary.deduped.sessions, total),
24157
+ aiSharePctBySessionDisplay: formatSharePct(aiSummary.bySession.sessions, total),
24158
+ paidAiSharePctDisplay: formatSharePct(aiSummary.paidDeduped.sessions, total),
24159
+ paidAiSharePctBySessionDisplay: formatSharePct(aiSummary.paidBySession.sessions, total),
24160
+ organicAiSharePctDisplay: formatSharePct(aiSummary.organicDeduped.sessions, total),
24161
+ organicAiSharePctBySessionDisplay: formatSharePct(aiSummary.organicBySession.sessions, total),
23919
24162
  directSharePctDisplay: formatSharePct(totalDirectSessions, total),
23920
24163
  socialSharePctDisplay: formatSharePct(socialSessions, total),
23921
24164
  lastSyncedAt: latestSync?.syncedAt ?? null,
@@ -23938,14 +24181,16 @@ async function ga4Routes(app, opts) {
23938
24181
  date: gaAiReferrals.date,
23939
24182
  source: gaAiReferrals.source,
23940
24183
  medium: gaAiReferrals.medium,
24184
+ trafficClass: gaAiReferrals.trafficClass,
23941
24185
  landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
23942
24186
  sourceDimension: gaAiReferrals.sourceDimension,
23943
24187
  sessions: sql10`SUM(${gaAiReferrals.sessions})`,
23944
24188
  users: sql10`SUM(${gaAiReferrals.users})`
23945
- }).from(gaAiReferrals).where(and20(...conditions)).groupBy(
24189
+ }).from(gaAiReferrals).where(and21(...conditions)).groupBy(
23946
24190
  gaAiReferrals.date,
23947
24191
  gaAiReferrals.source,
23948
24192
  gaAiReferrals.medium,
24193
+ gaAiReferrals.trafficClass,
23949
24194
  gaAiReferrals.sourceDimension,
23950
24195
  sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
23951
24196
  ).orderBy(gaAiReferrals.date).all();
@@ -23964,7 +24209,7 @@ async function ga4Routes(app, opts) {
23964
24209
  channelGroup: gaSocialReferrals.channelGroup,
23965
24210
  sessions: gaSocialReferrals.sessions,
23966
24211
  users: gaSocialReferrals.users
23967
- }).from(gaSocialReferrals).where(and20(...conditions)).orderBy(gaSocialReferrals.date).all();
24212
+ }).from(gaSocialReferrals).where(and21(...conditions)).orderBy(gaSocialReferrals.date).all();
23968
24213
  return rows;
23969
24214
  });
23970
24215
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -23977,7 +24222,7 @@ async function ga4Routes(app, opts) {
23977
24222
  d.setDate(d.getDate() - n);
23978
24223
  return fmt(d);
23979
24224
  };
23980
- const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and20(
24225
+ const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
23981
24226
  eq26(gaSocialReferrals.projectId, project.id),
23982
24227
  sql10`${gaSocialReferrals.date} >= ${from}`,
23983
24228
  sql10`${gaSocialReferrals.date} < ${to}`
@@ -23990,7 +24235,7 @@ async function ga4Routes(app, opts) {
23990
24235
  const sourceCurrent = app.db.select({
23991
24236
  source: gaSocialReferrals.source,
23992
24237
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`
23993
- }).from(gaSocialReferrals).where(and20(
24238
+ }).from(gaSocialReferrals).where(and21(
23994
24239
  eq26(gaSocialReferrals.projectId, project.id),
23995
24240
  sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
23996
24241
  sql10`${gaSocialReferrals.date} < ${fmt(today)}`
@@ -23998,7 +24243,7 @@ async function ga4Routes(app, opts) {
23998
24243
  const sourcePrev = app.db.select({
23999
24244
  source: gaSocialReferrals.source,
24000
24245
  sessions: sql10`SUM(${gaSocialReferrals.sessions})`
24001
- }).from(gaSocialReferrals).where(and20(
24246
+ }).from(gaSocialReferrals).where(and21(
24002
24247
  eq26(gaSocialReferrals.projectId, project.id),
24003
24248
  sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
24004
24249
  sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
@@ -24040,16 +24285,16 @@ async function ga4Routes(app, opts) {
24040
24285
  return fmt(d);
24041
24286
  };
24042
24287
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
24043
- const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and20(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24044
- const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and20(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24045
- const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and20(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24046
- const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and20(
24288
+ const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24289
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24290
+ const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
24291
+ const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24047
24292
  eq26(gaAiReferrals.projectId, project.id),
24048
24293
  sql10`${gaAiReferrals.date} >= ${from}`,
24049
24294
  sql10`${gaAiReferrals.date} < ${to}`,
24050
24295
  eq26(gaAiReferrals.sourceDimension, "session")
24051
24296
  )).get();
24052
- const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and20(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
24297
+ const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
24053
24298
  const todayStr = fmt(today);
24054
24299
  const buildTrend = (sum) => {
24055
24300
  const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
@@ -24058,13 +24303,13 @@ async function ga4Routes(app, opts) {
24058
24303
  const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
24059
24304
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
24060
24305
  };
24061
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and20(
24306
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24062
24307
  eq26(gaAiReferrals.projectId, project.id),
24063
24308
  sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
24064
24309
  sql10`${gaAiReferrals.date} < ${todayStr}`,
24065
24310
  eq26(gaAiReferrals.sourceDimension, "session")
24066
24311
  )).groupBy(gaAiReferrals.source).all();
24067
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and20(
24312
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
24068
24313
  eq26(gaAiReferrals.projectId, project.id),
24069
24314
  sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
24070
24315
  sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
@@ -24084,8 +24329,8 @@ async function ga4Routes(app, opts) {
24084
24329
  }
24085
24330
  return mover;
24086
24331
  };
24087
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and20(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
24088
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and20(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
24332
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
24333
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
24089
24334
  return {
24090
24335
  total: buildTrend(sumTotal),
24091
24336
  organic: buildTrend(sumOrganic),
@@ -24107,7 +24352,7 @@ async function ga4Routes(app, opts) {
24107
24352
  sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
24108
24353
  organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
24109
24354
  users: sql10`SUM(${gaTrafficSnapshots.users})`
24110
- }).from(gaTrafficSnapshots).where(and20(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
24355
+ }).from(gaTrafficSnapshots).where(and21(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
24111
24356
  return rows.map((r) => ({
24112
24357
  date: r.date,
24113
24358
  sessions: r.sessions ?? 0,
@@ -25771,7 +26016,7 @@ async function wordpressRoutes(app, opts) {
25771
26016
 
25772
26017
  // ../api-routes/src/backlinks.ts
25773
26018
  import crypto23 from "crypto";
25774
- import { and as and22, asc as asc4, desc as desc14, eq as eq27, sql as sql11 } from "drizzle-orm";
26019
+ import { and as and23, asc as asc4, desc as desc14, eq as eq27, sql as sql11 } from "drizzle-orm";
25775
26020
 
25776
26021
  // ../integration-commoncrawl/src/constants.ts
25777
26022
  import os2 from "os";
@@ -26174,7 +26419,7 @@ function pruneCachedRelease(release, opts = {}) {
26174
26419
  }
26175
26420
 
26176
26421
  // ../api-routes/src/backlinks-filter.ts
26177
- import { and as and21, ne as ne3, notLike } from "drizzle-orm";
26422
+ import { and as and22, ne as ne3, notLike } from "drizzle-orm";
26178
26423
  var BACKLINK_FILTER_PATTERNS = [
26179
26424
  "*.google.com",
26180
26425
  "*.googleusercontent.com",
@@ -26197,7 +26442,7 @@ function backlinkCrawlerExclusionClause() {
26197
26442
  conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
26198
26443
  }
26199
26444
  }
26200
- const combined = and21(...conditions);
26445
+ const combined = and22(...conditions);
26201
26446
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
26202
26447
  return combined;
26203
26448
  }
@@ -26272,7 +26517,7 @@ function latestSummaryForProject(db, projectId, source, release) {
26272
26517
  eq27(backlinkSummaries.source, source)
26273
26518
  ];
26274
26519
  if (release) conditions.push(eq27(backlinkSummaries.release, release));
26275
- return db.select().from(backlinkSummaries).where(and22(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
26520
+ return db.select().from(backlinkSummaries).where(and23(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
26276
26521
  }
26277
26522
  function parseExcludeCrawlers(value) {
26278
26523
  if (!value) return false;
@@ -26280,12 +26525,12 @@ function parseExcludeCrawlers(value) {
26280
26525
  return lower === "1" || lower === "true" || lower === "yes";
26281
26526
  }
26282
26527
  function computeFilteredSummary(db, base) {
26283
- const baseDomainCondition = and22(
26528
+ const baseDomainCondition = and23(
26284
26529
  eq27(backlinkDomains.projectId, base.projectId),
26285
26530
  eq27(backlinkDomains.source, base.source),
26286
26531
  eq27(backlinkDomains.release, base.release)
26287
26532
  );
26288
- const filteredCondition = and22(baseDomainCondition, backlinkCrawlerExclusionClause());
26533
+ const filteredCondition = and23(baseDomainCondition, backlinkCrawlerExclusionClause());
26289
26534
  const unfilteredAgg = db.select({
26290
26535
  count: sql11`count(*)`,
26291
26536
  total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -26315,10 +26560,10 @@ function computeFilteredSummary(db, base) {
26315
26560
  };
26316
26561
  }
26317
26562
  function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
26318
- const summary = db.select().from(backlinkSummaries).where(and22(eq27(backlinkSummaries.projectId, projectId), eq27(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
26563
+ const summary = db.select().from(backlinkSummaries).where(and23(eq27(backlinkSummaries.projectId, projectId), eq27(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
26319
26564
  let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
26320
26565
  if (summary && excludeCrawlers) {
26321
- const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and22(
26566
+ const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and23(
26322
26567
  eq27(backlinkDomains.projectId, projectId),
26323
26568
  eq27(backlinkDomains.source, source),
26324
26569
  eq27(backlinkDomains.release, summary.release),
@@ -26501,12 +26746,12 @@ async function backlinksRoutes(app, opts) {
26501
26746
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
26502
26747
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
26503
26748
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
26504
- const baseDomainCondition = and22(
26749
+ const baseDomainCondition = and23(
26505
26750
  eq27(backlinkDomains.projectId, project.id),
26506
26751
  eq27(backlinkDomains.source, source),
26507
26752
  eq27(backlinkDomains.release, targetRelease)
26508
26753
  );
26509
- const domainCondition = excludeCrawlers ? and22(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
26754
+ const domainCondition = excludeCrawlers ? and23(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
26510
26755
  const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
26511
26756
  const rows = app.db.select({
26512
26757
  linkingDomain: backlinkDomains.linkingDomain,
@@ -26530,7 +26775,7 @@ async function backlinksRoutes(app, opts) {
26530
26775
  async (request, reply) => {
26531
26776
  const project = resolveProject(app.db, request.params.name);
26532
26777
  const source = parseSourceParam(request.query.source);
26533
- const rows = app.db.select().from(backlinkSummaries).where(and22(eq27(backlinkSummaries.projectId, project.id), eq27(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
26778
+ const rows = app.db.select().from(backlinkSummaries).where(and23(eq27(backlinkSummaries.projectId, project.id), eq27(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
26534
26779
  const response = rows.map((r) => ({
26535
26780
  release: r.release,
26536
26781
  totalLinkingDomains: r.totalLinkingDomains,
@@ -26586,7 +26831,7 @@ async function backlinksRoutes(app, opts) {
26586
26831
  // ../api-routes/src/traffic.ts
26587
26832
  import crypto25 from "crypto";
26588
26833
  import { Agent as UndiciAgent } from "undici";
26589
- import { and as and23, desc as desc15, eq as eq28, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
26834
+ import { and as and24, desc as desc15, eq as eq28, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
26590
26835
 
26591
26836
  // ../integration-cloud-run/src/auth.ts
26592
26837
  import crypto24 from "crypto";
@@ -30445,28 +30690,28 @@ async function runBackfillTask(options) {
30445
30690
  try {
30446
30691
  app.db.transaction((tx) => {
30447
30692
  tx.delete(crawlerEventsHourly).where(
30448
- and23(
30693
+ and24(
30449
30694
  eq28(crawlerEventsHourly.sourceId, sourceRow.id),
30450
30695
  gte4(crawlerEventsHourly.tsHour, windowStartIso),
30451
30696
  lte3(crawlerEventsHourly.tsHour, windowEndIso)
30452
30697
  )
30453
30698
  ).run();
30454
30699
  tx.delete(aiUserFetchEventsHourly).where(
30455
- and23(
30700
+ and24(
30456
30701
  eq28(aiUserFetchEventsHourly.sourceId, sourceRow.id),
30457
30702
  gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
30458
30703
  lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
30459
30704
  )
30460
30705
  ).run();
30461
30706
  tx.delete(aiReferralEventsHourly).where(
30462
- and23(
30707
+ and24(
30463
30708
  eq28(aiReferralEventsHourly.sourceId, sourceRow.id),
30464
30709
  gte4(aiReferralEventsHourly.tsHour, windowStartIso),
30465
30710
  lte3(aiReferralEventsHourly.tsHour, windowEndIso)
30466
30711
  )
30467
30712
  ).run();
30468
30713
  tx.delete(rawEventSamples).where(
30469
- and23(
30714
+ and24(
30470
30715
  eq28(rawEventSamples.sourceId, sourceRow.id),
30471
30716
  gte4(rawEventSamples.ts, windowStartIso),
30472
30717
  lte3(rawEventSamples.ts, windowEndIso)
@@ -30849,7 +31094,7 @@ async function trafficRoutes(app, opts) {
30849
31094
  row = tx.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
30850
31095
  }
30851
31096
  const existingSchedule = tx.select().from(schedules).where(
30852
- and23(
31097
+ and24(
30853
31098
  eq28(schedules.projectId, project.id),
30854
31099
  eq28(schedules.kind, SchedulableRunKinds["traffic-sync"])
30855
31100
  )
@@ -31571,7 +31816,7 @@ async function trafficRoutes(app, opts) {
31571
31816
  pathNormalized: crawlerEventsHourly.pathNormalized,
31572
31817
  hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
31573
31818
  }).from(crawlerEventsHourly).where(
31574
- and23(
31819
+ and24(
31575
31820
  eq28(crawlerEventsHourly.sourceId, row.id),
31576
31821
  gte4(crawlerEventsHourly.tsHour, since)
31577
31822
  )
@@ -31581,25 +31826,25 @@ async function trafficRoutes(app, opts) {
31581
31826
  );
31582
31827
  const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
31583
31828
  const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
31584
- and23(
31829
+ and24(
31585
31830
  eq28(aiUserFetchEventsHourly.sourceId, row.id),
31586
31831
  gte4(aiUserFetchEventsHourly.tsHour, since)
31587
31832
  )
31588
31833
  ).get();
31589
31834
  const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31590
- and23(
31835
+ and24(
31591
31836
  eq28(aiReferralEventsHourly.sourceId, row.id),
31592
31837
  gte4(aiReferralEventsHourly.tsHour, since)
31593
31838
  )
31594
31839
  ).get();
31595
31840
  const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
31596
- and23(
31841
+ and24(
31597
31842
  eq28(rawEventSamples.sourceId, row.id),
31598
31843
  gte4(rawEventSamples.ts, since)
31599
31844
  )
31600
31845
  ).get();
31601
31846
  const latestRun = app.db.select().from(runs).where(
31602
- and23(
31847
+ and24(
31603
31848
  eq28(runs.projectId, projectId),
31604
31849
  eq28(runs.kind, RunKinds["traffic-sync"]),
31605
31850
  eq28(runs.sourceId, row.id)
@@ -31633,7 +31878,7 @@ async function trafficRoutes(app, opts) {
31633
31878
  "`advanceToNow` must be `true`. There is no implicit reset."
31634
31879
  );
31635
31880
  }
31636
- const sourceRow = app.db.select().from(trafficSources).where(and23(eq28(trafficSources.projectId, project.id), eq28(trafficSources.id, request.params.id))).get();
31881
+ const sourceRow = app.db.select().from(trafficSources).where(and24(eq28(trafficSources.projectId, project.id), eq28(trafficSources.id, request.params.id))).get();
31637
31882
  if (!sourceRow) {
31638
31883
  throw notFound("traffic source", request.params.id);
31639
31884
  }
@@ -31738,7 +31983,7 @@ async function trafficRoutes(app, opts) {
31738
31983
  lte3(crawlerEventsHourly.tsHour, untilIso)
31739
31984
  ];
31740
31985
  if (sourceIdParam) crawlerFilters.push(eq28(crawlerEventsHourly.sourceId, sourceIdParam));
31741
- const crawlerWhere = and23(...crawlerFilters);
31986
+ const crawlerWhere = and24(...crawlerFilters);
31742
31987
  const pathTotals = app.db.select({
31743
31988
  pathNormalized: crawlerEventsHourly.pathNormalized,
31744
31989
  hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
@@ -31770,7 +32015,7 @@ async function trafficRoutes(app, opts) {
31770
32015
  lte3(aiUserFetchEventsHourly.tsHour, untilIso)
31771
32016
  ];
31772
32017
  if (sourceIdParam) userFetchFilters.push(eq28(aiUserFetchEventsHourly.sourceId, sourceIdParam));
31773
- const userFetchWhere = and23(...userFetchFilters);
32018
+ const userFetchWhere = and24(...userFetchFilters);
31774
32019
  const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
31775
32020
  aiUserFetchTotal = Number(total?.total ?? 0);
31776
32021
  const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
@@ -31795,7 +32040,7 @@ async function trafficRoutes(app, opts) {
31795
32040
  lte3(aiReferralEventsHourly.tsHour, untilIso)
31796
32041
  ];
31797
32042
  if (sourceIdParam) aiFilters.push(eq28(aiReferralEventsHourly.sourceId, sourceIdParam));
31798
- const aiWhere = and23(...aiFilters);
32043
+ const aiWhere = and24(...aiFilters);
31799
32044
  const total = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
31800
32045
  aiReferralTotal = Number(total?.total ?? 0);
31801
32046
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
@@ -32002,7 +32247,7 @@ function readInstalledManifest(skillDir) {
32002
32247
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
32003
32248
 
32004
32249
  // ../api-routes/src/doctor/checks/backlinks.ts
32005
- import { and as and24, eq as eq29 } from "drizzle-orm";
32250
+ import { and as and25, eq as eq29 } from "drizzle-orm";
32006
32251
  function skippedNoProject() {
32007
32252
  return {
32008
32253
  status: CheckStatuses.skipped,
@@ -32034,7 +32279,7 @@ var BACKLINKS_CHECKS = [
32034
32279
  details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
32035
32280
  };
32036
32281
  }
32037
- const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and24(
32282
+ const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and25(
32038
32283
  eq29(backlinkSummaries.projectId, ctx.project.id),
32039
32284
  eq29(backlinkSummaries.source, BacklinkSources.commoncrawl)
32040
32285
  )).limit(1).get() : false;
@@ -32559,7 +32804,7 @@ var ga4ConnectionCheck = {
32559
32804
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
32560
32805
 
32561
32806
  // ../api-routes/src/doctor/checks/gbp-auth.ts
32562
- import { and as and25, eq as eq32 } from "drizzle-orm";
32807
+ import { and as and26, eq as eq32 } from "drizzle-orm";
32563
32808
  var RECENT_SYNC_WARN_DAYS2 = 7;
32564
32809
  var RECENT_SYNC_FAIL_DAYS2 = 30;
32565
32810
  function skippedNoProject3() {
@@ -32792,7 +33037,7 @@ var recentSyncCheck = {
32792
33037
  title: "GBP recent sync",
32793
33038
  run: (ctx) => {
32794
33039
  if (!ctx.project) return skippedNoProject3();
32795
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and25(eq32(gbpLocations.projectId, ctx.project.id), eq32(gbpLocations.selected, true))).all();
33040
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(eq32(gbpLocations.projectId, ctx.project.id), eq32(gbpLocations.selected, true))).all();
32796
33041
  if (selected.length === 0) {
32797
33042
  return {
32798
33043
  status: CheckStatuses.skipped,
@@ -33394,7 +33639,7 @@ var RUNTIME_STATE_CHECKS = [
33394
33639
  ];
33395
33640
 
33396
33641
  // ../api-routes/src/doctor/checks/traffic-source.ts
33397
- import { and as and26, eq as eq34, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
33642
+ import { and as and27, eq as eq34, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
33398
33643
  var RECENT_DATA_WARN_DAYS = 7;
33399
33644
  var RECENT_DATA_FAIL_DAYS = 30;
33400
33645
  function skippedNoProject5() {
@@ -33408,7 +33653,7 @@ function skippedNoProject5() {
33408
33653
  function loadProbes(ctx) {
33409
33654
  if (!ctx.project) return [];
33410
33655
  const rows = ctx.db.select().from(trafficSources).where(
33411
- and26(
33656
+ and27(
33412
33657
  eq34(trafficSources.projectId, ctx.project.id),
33413
33658
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
33414
33659
  )
@@ -33489,7 +33734,7 @@ var recentDataCheck = {
33489
33734
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
33490
33735
  const recentCrawlers = Number(
33491
33736
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
33492
- and26(
33737
+ and27(
33493
33738
  eq34(crawlerEventsHourly.projectId, ctx.project.id),
33494
33739
  gte5(crawlerEventsHourly.tsHour, warnCutoff)
33495
33740
  )
@@ -33497,7 +33742,7 @@ var recentDataCheck = {
33497
33742
  );
33498
33743
  const recentReferrals = Number(
33499
33744
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
33500
- and26(
33745
+ and27(
33501
33746
  eq34(aiReferralEventsHourly.projectId, ctx.project.id),
33502
33747
  gte5(aiReferralEventsHourly.tsHour, warnCutoff)
33503
33748
  )
@@ -33513,7 +33758,7 @@ var recentDataCheck = {
33513
33758
  }
33514
33759
  const olderCrawlers = Number(
33515
33760
  ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
33516
- and26(
33761
+ and27(
33517
33762
  eq34(crawlerEventsHourly.projectId, ctx.project.id),
33518
33763
  gte5(crawlerEventsHourly.tsHour, failCutoff)
33519
33764
  )
@@ -33521,7 +33766,7 @@ var recentDataCheck = {
33521
33766
  );
33522
33767
  const olderReferrals = Number(
33523
33768
  ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
33524
- and26(
33769
+ and27(
33525
33770
  eq34(aiReferralEventsHourly.projectId, ctx.project.id),
33526
33771
  gte5(aiReferralEventsHourly.tsHour, failCutoff)
33527
33772
  )
@@ -33938,7 +34183,7 @@ async function doctorRoutes(app, opts) {
33938
34183
 
33939
34184
  // ../api-routes/src/discovery/routes.ts
33940
34185
  import crypto27 from "crypto";
33941
- import { and as and27, desc as desc16, eq as eq35, gte as gte6, inArray as inArray12, isNull, or as or5 } from "drizzle-orm";
34186
+ import { and as and28, desc as desc16, eq as eq35, gte as gte6, inArray as inArray12, isNull, or as or5 } from "drizzle-orm";
33942
34187
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
33943
34188
  async function discoveryRoutes(app, opts) {
33944
34189
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -33972,7 +34217,7 @@ async function discoveryRoutes(app, opts) {
33972
34217
  const now = (/* @__PURE__ */ new Date()).toISOString();
33973
34218
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
33974
34219
  const decision = app.db.transaction((tx) => {
33975
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and27(
34220
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and28(
33976
34221
  eq35(discoverySessions.projectId, project.id),
33977
34222
  eq35(discoverySessions.icpDescription, icpDescription),
33978
34223
  // Buyer is part of session identity: it changes the seed prompt's
@@ -34618,7 +34863,7 @@ function dedupeStrings(input) {
34618
34863
 
34619
34864
  // ../api-routes/src/technical-aeo.ts
34620
34865
  import crypto29 from "crypto";
34621
- import { and as and28, asc as asc5, count, desc as desc17, eq as eq37, inArray as inArray13 } from "drizzle-orm";
34866
+ import { and as and29, asc as asc5, count, desc as desc17, eq as eq37, inArray as inArray13 } from "drizzle-orm";
34622
34867
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
34623
34868
  function emptyScore(projectName) {
34624
34869
  return {
@@ -34650,7 +34895,7 @@ function parsePositiveInt(value, fallback, max) {
34650
34895
  async function technicalAeoRoutes(app, opts) {
34651
34896
  app.get("/projects/:name/technical-aeo", async (request) => {
34652
34897
  const project = resolveProject(app.db, request.params.name);
34653
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and28(
34898
+ const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and29(
34654
34899
  eq37(siteAuditSnapshots.projectId, project.id),
34655
34900
  eq37(runs.kind, RunKinds["site-audit"]),
34656
34901
  inArray13(runs.status, SURFACEABLE_STATUSES),
@@ -34685,7 +34930,7 @@ async function technicalAeoRoutes(app, opts) {
34685
34930
  });
34686
34931
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
34687
34932
  const project = resolveProject(app.db, request.params.name);
34688
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and28(
34933
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and29(
34689
34934
  eq37(siteAuditSnapshots.projectId, project.id),
34690
34935
  eq37(runs.kind, RunKinds["site-audit"]),
34691
34936
  inArray13(runs.status, SURFACEABLE_STATUSES),
@@ -34697,7 +34942,7 @@ async function technicalAeoRoutes(app, opts) {
34697
34942
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
34698
34943
  const conds = [eq37(siteAuditPages.runId, latest.runId)];
34699
34944
  if (statusFilter) conds.push(eq37(siteAuditPages.status, statusFilter));
34700
- const where = and28(...conds);
34945
+ const where = and29(...conds);
34701
34946
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
34702
34947
  const total = totalRow?.value ?? 0;
34703
34948
  const limit = parsePositiveInt(request.query.limit, 100, 500);
@@ -34721,7 +34966,7 @@ async function technicalAeoRoutes(app, opts) {
34721
34966
  auditedAt: siteAuditSnapshots.auditedAt,
34722
34967
  aggregateScore: siteAuditSnapshots.aggregateScore,
34723
34968
  pagesAudited: siteAuditSnapshots.pagesAudited
34724
- }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and28(
34969
+ }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and29(
34725
34970
  eq37(siteAuditSnapshots.projectId, project.id),
34726
34971
  eq37(runs.kind, RunKinds["site-audit"]),
34727
34972
  inArray13(runs.status, SURFACEABLE_STATUSES),
@@ -34735,7 +34980,7 @@ async function technicalAeoRoutes(app, opts) {
34735
34980
  if (!parsed.success) {
34736
34981
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
34737
34982
  }
34738
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and28(
34983
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and29(
34739
34984
  eq37(runs.projectId, project.id),
34740
34985
  eq37(runs.kind, RunKinds["site-audit"]),
34741
34986
  inArray13(runs.status, [RunStatuses.queued, RunStatuses.running])
@@ -35329,7 +35574,7 @@ var IntelligenceService = class {
35329
35574
  */
35330
35575
  analyzeAndPersist(runId, projectId) {
35331
35576
  const recentRuns = this.db.select().from(runs).where(
35332
- and29(
35577
+ and30(
35333
35578
  eq38(runs.projectId, projectId),
35334
35579
  or6(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
35335
35580
  // Defensive: RunCoordinator already skips probes before this is
@@ -35421,7 +35666,7 @@ var IntelligenceService = class {
35421
35666
  }
35422
35667
  const windowStart = runRow.startedAt ?? runRow.createdAt;
35423
35668
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
35424
- const selected = this.db.select().from(gbpLocations).where(and29(
35669
+ const selected = this.db.select().from(gbpLocations).where(and30(
35425
35670
  eq38(gbpLocations.projectId, projectId),
35426
35671
  eq38(gbpLocations.selected, true),
35427
35672
  gte7(gbpLocations.syncedAt, windowStart),
@@ -35458,12 +35703,12 @@ var IntelligenceService = class {
35458
35703
  }
35459
35704
  /** Build the per-location signal bundle the GBP analyzer consumes. */
35460
35705
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
35461
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and29(eq38(gbpDailyMetrics.projectId, projectId), eq38(gbpDailyMetrics.locationName, locationName))).all();
35462
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and29(eq38(gbpPlaceActions.projectId, projectId), eq38(gbpPlaceActions.locationName, locationName))).all();
35463
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and29(eq38(gbpLodgingSnapshots.projectId, projectId), eq38(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
35464
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and29(eq38(gbpLocations.projectId, projectId), eq38(gbpLocations.locationName, locationName))).get();
35706
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(eq38(gbpDailyMetrics.projectId, projectId), eq38(gbpDailyMetrics.locationName, locationName))).all();
35707
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(eq38(gbpPlaceActions.projectId, projectId), eq38(gbpPlaceActions.locationName, locationName))).all();
35708
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(eq38(gbpLodgingSnapshots.projectId, projectId), eq38(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
35709
+ const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(eq38(gbpLocations.projectId, projectId), eq38(gbpLocations.locationName, locationName))).get();
35465
35710
  const descriptionMissing = !(ownerRow?.description ?? "").trim();
35466
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and29(eq38(gbpPlaceDetails.projectId, projectId), eq38(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
35711
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(eq38(gbpPlaceDetails.projectId, projectId), eq38(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
35467
35712
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
35468
35713
  const summary = buildGbpSummary({
35469
35714
  locationName,
@@ -35499,7 +35744,7 @@ var IntelligenceService = class {
35499
35744
  /** Build the month-over-month keyword series for a location from the
35500
35745
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
35501
35746
  buildGbpKeywordTrend(projectId, locationName) {
35502
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and29(eq38(gbpKeywordMonthly.projectId, projectId), eq38(gbpKeywordMonthly.locationName, locationName))).all();
35747
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(eq38(gbpKeywordMonthly.projectId, projectId), eq38(gbpKeywordMonthly.locationName, locationName))).all();
35503
35748
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
35504
35749
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
35505
35750
  const recentMonth = months[0] ?? null;
@@ -35530,7 +35775,7 @@ var IntelligenceService = class {
35530
35775
  */
35531
35776
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
35532
35777
  const covered = new Set(coveredLocationNames);
35533
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and29(eq38(insights.projectId, projectId), eq38(insights.provider, GBP_INSIGHT_PROVIDER))).all();
35778
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(eq38(insights.projectId, projectId), eq38(insights.provider, GBP_INSIGHT_PROVIDER))).all();
35534
35779
  const staleIds = [];
35535
35780
  const dismissedSlots = /* @__PURE__ */ new Set();
35536
35781
  for (const row of existing) {
@@ -35632,7 +35877,7 @@ var IntelligenceService = class {
35632
35877
  sinceTimestamp = parsed;
35633
35878
  }
35634
35879
  const allRuns = this.db.select().from(runs).where(
35635
- and29(
35880
+ and30(
35636
35881
  eq38(runs.projectId, project.id),
35637
35882
  or6(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
35638
35883
  // Backfill must not replay probe runs as if they were real sweeps.
@@ -35809,7 +36054,7 @@ var IntelligenceService = class {
35809
36054
  );
35810
36055
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
35811
36056
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
35812
- and29(
36057
+ and30(
35813
36058
  eq38(runs.projectId, projectId),
35814
36059
  eq38(runs.kind, RunKinds["answer-visibility"]),
35815
36060
  or6(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
@@ -35832,7 +36077,7 @@ var IntelligenceService = class {
35832
36077
  const haveHistory = recentRunIds.length > 0;
35833
36078
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
35834
36079
  if (haveHistory) {
35835
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and29(eq38(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
36080
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(eq38(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
35836
36081
  const regressionGroups = /* @__PURE__ */ new Map();
35837
36082
  for (const row of priorRows) {
35838
36083
  if (!row.runId) continue;