@ainyc/canonry 4.15.0 → 4.17.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.
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-C32VL5BB.js";
8
+ } from "./chunk-6TWKC3DP.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -66,7 +66,7 @@ import {
66
66
  schedules,
67
67
  trafficSources,
68
68
  usageCounters
69
- } from "./chunk-7HBZCGRL.js";
69
+ } from "./chunk-PAZCY4FF.js";
70
70
  import {
71
71
  AGENT_MEMORY_VALUE_MAX_BYTES,
72
72
  AGENT_PROVIDER_IDS,
@@ -81,7 +81,9 @@ import {
81
81
  RunKinds,
82
82
  RunStatuses,
83
83
  RunTriggers,
84
+ SchedulableRunKinds,
84
85
  TrafficEventConfidences,
86
+ TrafficEventKinds,
85
87
  TrafficEvidenceKinds,
86
88
  TrafficSourceAuthModes,
87
89
  TrafficSourceStatuses,
@@ -143,6 +145,7 @@ import {
143
145
  runInProgress,
144
146
  runNotCancellable,
145
147
  runTriggerRequestSchema,
148
+ schedulableRunKindSchema,
146
149
  scheduleUpsertRequestSchema,
147
150
  serializeRunError,
148
151
  snapshotRequestSchema,
@@ -152,7 +155,7 @@ import {
152
155
  visibilityStateFromAnswerMentioned,
153
156
  windowCutoff,
154
157
  wordpressEnvSchema
155
- } from "./chunk-6QTH5NS5.js";
158
+ } from "./chunk-Q2OED5JQ.js";
156
159
 
157
160
  // src/telemetry.ts
158
161
  import crypto from "crypto";
@@ -1435,7 +1438,7 @@ function loadRunDetail(app, run) {
1435
1438
 
1436
1439
  // ../api-routes/src/apply.ts
1437
1440
  import crypto10 from "crypto";
1438
- import { eq as eq8 } from "drizzle-orm";
1441
+ import { and as and2, eq as eq8 } from "drizzle-orm";
1439
1442
 
1440
1443
  // ../api-routes/src/schedule-utils.ts
1441
1444
  var DAY_MAP = {
@@ -1840,8 +1843,9 @@ async function applyRoutes(app, opts) {
1840
1843
  entityType: "competitor",
1841
1844
  diff: { competitors: normalizedCompetitors }
1842
1845
  });
1846
+ const AV_KIND = SchedulableRunKinds["answer-visibility"];
1843
1847
  if (resolvedSchedule) {
1844
- const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
1848
+ const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1845
1849
  if (existingSched) {
1846
1850
  tx.update(schedules).set({
1847
1851
  cronExpr: resolvedSchedule.cronExpr,
@@ -1855,6 +1859,7 @@ async function applyRoutes(app, opts) {
1855
1859
  tx.insert(schedules).values({
1856
1860
  id: crypto10.randomUUID(),
1857
1861
  projectId,
1862
+ kind: AV_KIND,
1858
1863
  cronExpr: resolvedSchedule.cronExpr,
1859
1864
  preset: resolvedSchedule.preset,
1860
1865
  timezone: resolvedSchedule.timezone,
@@ -1866,9 +1871,9 @@ async function applyRoutes(app, opts) {
1866
1871
  }
1867
1872
  scheduleAction = "upsert";
1868
1873
  } else if (deleteSchedule) {
1869
- const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
1874
+ const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
1870
1875
  if (existingSched) {
1871
- tx.delete(schedules).where(eq8(schedules.projectId, projectId)).run();
1876
+ tx.delete(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
1872
1877
  scheduleAction = "delete";
1873
1878
  }
1874
1879
  }
@@ -1896,7 +1901,7 @@ async function applyRoutes(app, opts) {
1896
1901
  }
1897
1902
  });
1898
1903
  if (scheduleAction) {
1899
- opts?.onScheduleUpdated?.(scheduleAction, projectId);
1904
+ opts?.onScheduleUpdated?.(scheduleAction, projectId, SchedulableRunKinds["answer-visibility"]);
1900
1905
  }
1901
1906
  if (!hasNotifications) {
1902
1907
  opts?.onProjectUpserted?.(projectId, config.metadata.name);
@@ -2558,7 +2563,7 @@ function buildCategoryCounts(counts) {
2558
2563
  }
2559
2564
 
2560
2565
  // ../api-routes/src/intelligence.ts
2561
- import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
2566
+ import { eq as eq11, desc as desc4, and as and3 } from "drizzle-orm";
2562
2567
  function emptyHealthSnapshot(projectId) {
2563
2568
  return {
2564
2569
  id: `no-data:${projectId}`,
@@ -2609,7 +2614,7 @@ async function intelligenceRoutes(app) {
2609
2614
  if (request.query.runId) {
2610
2615
  conditions.push(eq11(insights.runId, request.query.runId));
2611
2616
  }
2612
- const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and2(...conditions)).orderBy(desc4(insights.createdAt)).all();
2617
+ const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and3(...conditions)).orderBy(desc4(insights.createdAt)).all();
2613
2618
  const showDismissed = request.query.dismissed === "true";
2614
2619
  const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
2615
2620
  return reply.send(result);
@@ -2649,7 +2654,7 @@ async function intelligenceRoutes(app) {
2649
2654
  }
2650
2655
 
2651
2656
  // ../api-routes/src/report.ts
2652
- import { and as and4, desc as desc6, eq as eq13, inArray as inArray4, or as or2 } from "drizzle-orm";
2657
+ import { and as and5, desc as desc6, eq as eq13, inArray as inArray4, or as or2 } from "drizzle-orm";
2653
2658
 
2654
2659
  // ../api-routes/src/report-renderer.ts
2655
2660
  var COLORS = {
@@ -4724,7 +4729,7 @@ function renderReportHtml(report, opts = {}) {
4724
4729
  }
4725
4730
 
4726
4731
  // ../api-routes/src/content-data.ts
4727
- import { and as and3, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
4732
+ import { and as and4, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
4728
4733
  var RECENT_RUNS_WINDOW = 5;
4729
4734
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
4730
4735
  const projectId = project.id;
@@ -4848,7 +4853,7 @@ function listCompetitorDomains(db, projectId) {
4848
4853
  }
4849
4854
  function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
4850
4855
  const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
4851
- and3(
4856
+ and4(
4852
4857
  eq12(runs.projectId, projectId),
4853
4858
  eq12(runs.kind, RunKinds["answer-visibility"]),
4854
4859
  // Queued/running/failed/cancelled runs may have partial or no
@@ -5192,7 +5197,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
5192
5197
  }
5193
5198
  function buildGaSection(db, projectId) {
5194
5199
  const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
5195
- and4(
5200
+ and5(
5196
5201
  eq13(gaTrafficWindowSummaries.projectId, projectId),
5197
5202
  eq13(gaTrafficWindowSummaries.windowKey, "30d")
5198
5203
  )
@@ -5375,7 +5380,7 @@ function buildIndexingHealth(db, projectId) {
5375
5380
  return null;
5376
5381
  }
5377
5382
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5378
- const visibilityRuns = db.select().from(runs).where(and4(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
5383
+ const visibilityRuns = db.select().from(runs).where(and5(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
5379
5384
  const totalQueries = queryLookup.byId.size;
5380
5385
  const points = [];
5381
5386
  for (const run of visibilityRuns) {
@@ -5421,14 +5426,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
5421
5426
  }
5422
5427
  function buildInsightList(db, projectId, locationFilter) {
5423
5428
  const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
5424
- and4(
5429
+ and5(
5425
5430
  eq13(runs.projectId, projectId),
5426
5431
  eq13(runs.kind, RunKinds["answer-visibility"]),
5427
5432
  or2(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
5428
5433
  )
5429
5434
  ).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
5430
5435
  if (recentRunIds.length === 0) return [];
5431
- const rows = db.select().from(insights).where(and4(eq13(insights.projectId, projectId), inArray4(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5436
+ const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray4(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5432
5437
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
5433
5438
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
5434
5439
  const recommendation = parseJsonColumn(r.recommendation, null);
@@ -6350,7 +6355,7 @@ function normalizeDomain2(domain) {
6350
6355
  }
6351
6356
 
6352
6357
  // ../api-routes/src/composites.ts
6353
- import { eq as eq15, and as and5, desc as desc7, sql as sql3, like, or as or3, inArray as inArray6 } from "drizzle-orm";
6358
+ import { eq as eq15, and as and6, desc as desc7, sql as sql3, like, or as or3, inArray as inArray6 } from "drizzle-orm";
6354
6359
  var TOP_INSIGHT_LIMIT = 5;
6355
6360
  var SEARCH_HIT_HARD_LIMIT = 50;
6356
6361
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -6453,7 +6458,7 @@ async function compositeRoutes(app) {
6453
6458
  rawResponse: querySnapshots.rawResponse,
6454
6459
  createdAt: querySnapshots.createdAt
6455
6460
  }).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
6456
- and5(
6461
+ and6(
6457
6462
  eq15(queries.projectId, project.id),
6458
6463
  or3(
6459
6464
  sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
@@ -6464,7 +6469,7 @@ async function compositeRoutes(app) {
6464
6469
  )
6465
6470
  ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
6466
6471
  const insightMatches = app.db.select().from(insights).where(
6467
- and5(
6472
+ and6(
6468
6473
  eq15(insights.projectId, project.id),
6469
6474
  or3(
6470
6475
  like(insights.title, pattern),
@@ -7036,6 +7041,12 @@ var locationQueryParameter = {
7036
7041
  description: "Filter by location label. Use an empty value to request locationless results.",
7037
7042
  schema: stringSchema
7038
7043
  };
7044
+ var scheduleKindQueryParameter = {
7045
+ name: "kind",
7046
+ in: "query",
7047
+ description: 'Schedulable run kind. Defaults to "answer-visibility" for backward compatibility.',
7048
+ schema: { type: "string", enum: ["answer-visibility", "traffic-sync"] }
7049
+ };
7039
7050
  var reportAudienceQueryParameter = {
7040
7051
  name: "audience",
7041
7052
  in: "query",
@@ -7893,7 +7904,7 @@ var routeCatalog = [
7893
7904
  path: "/api/v1/projects/{name}/schedule",
7894
7905
  summary: "Create or update a schedule",
7895
7906
  tags: ["schedules"],
7896
- parameters: [nameParameter],
7907
+ parameters: [nameParameter, scheduleKindQueryParameter],
7897
7908
  requestBody: {
7898
7909
  required: true,
7899
7910
  content: {
@@ -7901,11 +7912,13 @@ var routeCatalog = [
7901
7912
  schema: {
7902
7913
  type: "object",
7903
7914
  properties: {
7915
+ kind: { type: "string", enum: ["answer-visibility", "traffic-sync"] },
7904
7916
  preset: stringSchema,
7905
7917
  cron: stringSchema,
7906
7918
  timezone: stringSchema,
7907
7919
  providers: stringArraySchema,
7908
- enabled: booleanSchema
7920
+ enabled: booleanSchema,
7921
+ sourceId: stringSchema
7909
7922
  }
7910
7923
  }
7911
7924
  }
@@ -7913,7 +7926,8 @@ var routeCatalog = [
7913
7926
  },
7914
7927
  responses: {
7915
7928
  200: { description: "Schedule updated." },
7916
- 201: { description: "Schedule created." }
7929
+ 201: { description: "Schedule created." },
7930
+ 400: { description: "Invalid payload (e.g. sourceId missing for kind=traffic-sync, or providers set for kind=traffic-sync)." }
7917
7931
  }
7918
7932
  },
7919
7933
  {
@@ -7921,7 +7935,7 @@ var routeCatalog = [
7921
7935
  path: "/api/v1/projects/{name}/schedule",
7922
7936
  summary: "Get a schedule",
7923
7937
  tags: ["schedules"],
7924
- parameters: [nameParameter],
7938
+ parameters: [nameParameter, scheduleKindQueryParameter],
7925
7939
  responses: {
7926
7940
  200: { description: "Schedule returned." },
7927
7941
  404: { description: "Schedule not found." }
@@ -7932,7 +7946,7 @@ var routeCatalog = [
7932
7946
  path: "/api/v1/projects/{name}/schedule",
7933
7947
  summary: "Delete a schedule",
7934
7948
  tags: ["schedules"],
7935
- parameters: [nameParameter],
7949
+ parameters: [nameParameter, scheduleKindQueryParameter],
7936
7950
  responses: {
7937
7951
  204: { description: "Schedule deleted." },
7938
7952
  404: { description: "Schedule not found." }
@@ -9661,8 +9675,66 @@ var routeCatalog = [
9661
9675
  },
9662
9676
  responses: {
9663
9677
  200: { description: "Sync summary returned." },
9664
- 400: { description: "Invalid sync request, missing credentials, or upstream pull error." },
9665
- 404: { description: "Project or traffic source not found." }
9678
+ 400: { description: "Invalid sync request or missing credentials." },
9679
+ 404: { description: "Project or traffic source not found." },
9680
+ 502: { description: "Upstream Cloud Run pull or auth-token resolution failed." }
9681
+ }
9682
+ },
9683
+ {
9684
+ method: "get",
9685
+ path: "/api/v1/projects/{name}/traffic/sources",
9686
+ summary: "List non-archived traffic sources for a project",
9687
+ tags: ["traffic"],
9688
+ parameters: [nameParameter],
9689
+ responses: {
9690
+ 200: { description: "Source list returned." },
9691
+ 404: { description: "Project not found." }
9692
+ }
9693
+ },
9694
+ {
9695
+ method: "get",
9696
+ path: "/api/v1/projects/{name}/traffic/status",
9697
+ summary: "List non-archived traffic sources with last-24h totals and the latest sync run for each",
9698
+ description: "Single-call composite for the `canonry traffic status` view: same shape as `GET /traffic/sources/{id}` but returned as `{ sources: TrafficSourceDetailDto[] }` for every non-archived source. Lets agents and the dashboard avoid an N+1 fan-out.",
9699
+ tags: ["traffic"],
9700
+ parameters: [nameParameter],
9701
+ responses: {
9702
+ 200: { description: "Status returned." },
9703
+ 404: { description: "Project not found." }
9704
+ }
9705
+ },
9706
+ {
9707
+ method: "get",
9708
+ path: "/api/v1/projects/{name}/traffic/sources/{id}",
9709
+ summary: "Get a single traffic source with last-24h totals and the latest sync run",
9710
+ tags: ["traffic"],
9711
+ parameters: [
9712
+ nameParameter,
9713
+ { name: "id", in: "path", required: true, description: "Traffic source ID.", schema: stringSchema }
9714
+ ],
9715
+ responses: {
9716
+ 200: { description: "Source detail returned." },
9717
+ 404: { description: "Project or source not found." }
9718
+ }
9719
+ },
9720
+ {
9721
+ method: "get",
9722
+ path: "/api/v1/projects/{name}/traffic/events",
9723
+ summary: "List rolled-up crawler and AI-referral hits within a window",
9724
+ description: "Returns hourly rollup rows from `crawler_events_hourly` and `ai_referral_events_hourly`. Defaults to the last 24h. Totals reflect the full window; the `events` array is capped by `limit` (default 500, max 5000).",
9725
+ tags: ["traffic"],
9726
+ parameters: [
9727
+ nameParameter,
9728
+ { name: "since", in: "query", description: "ISO-8601 window start (defaults to 24h ago).", schema: stringSchema },
9729
+ { name: "until", in: "query", description: "ISO-8601 window end (defaults to now).", schema: stringSchema },
9730
+ { name: "kind", in: "query", description: 'Filter to "crawler", "ai-referral", or "all" (default).', schema: stringSchema },
9731
+ { name: "limit", in: "query", description: "Max rows per kind in the events array (default 500, max 5000).", schema: stringSchema },
9732
+ { name: "sourceId", in: "query", description: "Restrict to a single traffic source.", schema: stringSchema }
9733
+ ],
9734
+ responses: {
9735
+ 200: { description: "Events returned with windowed totals." },
9736
+ 400: { description: "Invalid query parameters." },
9737
+ 404: { description: "Project not found." }
9666
9738
  }
9667
9739
  }
9668
9740
  ];
@@ -10030,7 +10102,15 @@ async function telemetryRoutes(app, opts) {
10030
10102
 
10031
10103
  // ../api-routes/src/schedules.ts
10032
10104
  import crypto11 from "crypto";
10033
- import { eq as eq16 } from "drizzle-orm";
10105
+ import { and as and7, eq as eq16 } from "drizzle-orm";
10106
+ function parseKindParam(raw) {
10107
+ if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
10108
+ const parsed = schedulableRunKindSchema.safeParse(raw);
10109
+ if (!parsed.success) {
10110
+ throw validationError(`Invalid kind "${String(raw)}". Must be one of: ${Object.values(SchedulableRunKinds).join(", ")}`);
10111
+ }
10112
+ return parsed.data;
10113
+ }
10034
10114
  async function scheduleRoutes(app, opts) {
10035
10115
  app.put("/projects/:name/schedule", async (request, reply) => {
10036
10116
  const project = resolveProject(app.db, request.params.name);
@@ -10043,7 +10123,22 @@ async function scheduleRoutes(app, opts) {
10043
10123
  }))
10044
10124
  });
10045
10125
  }
10046
- const { preset, cron: cron2, timezone, providers, enabled } = parsedBody.data;
10126
+ const kind = parsedBody.data.kind ?? parseKindParam(request.query?.kind);
10127
+ const { preset, cron: cron2, timezone, providers, enabled, sourceId } = parsedBody.data;
10128
+ if (kind === SchedulableRunKinds["traffic-sync"]) {
10129
+ if (!sourceId) {
10130
+ throw validationError('"sourceId" is required when kind is "traffic-sync"');
10131
+ }
10132
+ const sourceRow = app.db.select().from(trafficSources).where(eq16(trafficSources.id, sourceId)).get();
10133
+ if (!sourceRow || sourceRow.projectId !== project.id) {
10134
+ throw notFound("Traffic source", sourceId);
10135
+ }
10136
+ if (providers && providers.length > 0) {
10137
+ throw validationError('"providers" is not valid for kind "traffic-sync"');
10138
+ }
10139
+ } else if (sourceId) {
10140
+ throw validationError(`"sourceId" is only valid when kind is "traffic-sync"`);
10141
+ }
10047
10142
  const validNames = opts.validProviderNames ?? [];
10048
10143
  if (validNames.length && providers?.length) {
10049
10144
  const invalid = providers.filter((p) => !validNames.includes(p));
@@ -10073,13 +10168,14 @@ async function scheduleRoutes(app, opts) {
10073
10168
  }
10074
10169
  const now = (/* @__PURE__ */ new Date()).toISOString();
10075
10170
  const enabledInt = enabled === false ? 0 : 1;
10076
- const existing = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
10171
+ const existing = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10077
10172
  if (existing) {
10078
10173
  app.db.update(schedules).set({
10079
10174
  cronExpr,
10080
10175
  preset: preset ?? null,
10081
10176
  timezone,
10082
- providers: JSON.stringify(providers),
10177
+ providers: JSON.stringify(providers ?? []),
10178
+ sourceId: sourceId ?? null,
10083
10179
  enabled: enabledInt,
10084
10180
  updatedAt: now
10085
10181
  }).where(eq16(schedules.id, existing.id)).run();
@@ -10087,11 +10183,13 @@ async function scheduleRoutes(app, opts) {
10087
10183
  app.db.insert(schedules).values({
10088
10184
  id: crypto11.randomUUID(),
10089
10185
  projectId: project.id,
10186
+ kind,
10090
10187
  cronExpr,
10091
10188
  preset: preset ?? null,
10092
10189
  timezone,
10093
10190
  enabled: enabledInt,
10094
- providers: JSON.stringify(providers),
10191
+ providers: JSON.stringify(providers ?? []),
10192
+ sourceId: sourceId ?? null,
10095
10193
  createdAt: now,
10096
10194
  updatedAt: now
10097
10195
  }).run();
@@ -10101,25 +10199,27 @@ async function scheduleRoutes(app, opts) {
10101
10199
  actor: "api",
10102
10200
  action: existing ? "schedule.updated" : "schedule.created",
10103
10201
  entityType: "schedule",
10104
- diff: { cronExpr, preset, timezone, providers }
10202
+ diff: { kind, cronExpr, preset, timezone, providers, sourceId }
10105
10203
  });
10106
- opts.onScheduleUpdated?.("upsert", project.id);
10107
- const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
10204
+ opts.onScheduleUpdated?.("upsert", project.id, kind);
10205
+ const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10108
10206
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
10109
10207
  });
10110
10208
  app.get("/projects/:name/schedule", async (request, reply) => {
10111
10209
  const project = resolveProject(app.db, request.params.name);
10112
- const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
10210
+ const kind = parseKindParam(request.query?.kind);
10211
+ const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10113
10212
  if (!schedule) {
10114
- throw notFound("Schedule", request.params.name);
10213
+ throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10115
10214
  }
10116
10215
  return reply.send(formatSchedule(schedule));
10117
10216
  });
10118
10217
  app.delete("/projects/:name/schedule", async (request, reply) => {
10119
10218
  const project = resolveProject(app.db, request.params.name);
10120
- const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
10219
+ const kind = parseKindParam(request.query?.kind);
10220
+ const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
10121
10221
  if (!schedule) {
10122
- throw notFound("Schedule", request.params.name);
10222
+ throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
10123
10223
  }
10124
10224
  app.db.delete(schedules).where(eq16(schedules.id, schedule.id)).run();
10125
10225
  writeAuditLog(app.db, {
@@ -10127,9 +10227,10 @@ async function scheduleRoutes(app, opts) {
10127
10227
  actor: "api",
10128
10228
  action: "schedule.deleted",
10129
10229
  entityType: "schedule",
10130
- entityId: schedule.id
10230
+ entityId: schedule.id,
10231
+ diff: { kind }
10131
10232
  });
10132
- opts.onScheduleUpdated?.("delete", project.id);
10233
+ opts.onScheduleUpdated?.("delete", project.id, kind);
10133
10234
  return reply.status(204).send();
10134
10235
  });
10135
10236
  }
@@ -10137,11 +10238,13 @@ function formatSchedule(row) {
10137
10238
  return {
10138
10239
  id: row.id,
10139
10240
  projectId: row.projectId,
10241
+ kind: row.kind,
10140
10242
  cronExpr: row.cronExpr,
10141
10243
  preset: row.preset,
10142
10244
  timezone: row.timezone,
10143
10245
  enabled: row.enabled === 1,
10144
10246
  providers: parseJsonColumn(row.providers, []),
10247
+ sourceId: row.sourceId,
10145
10248
  lastRunAt: row.lastRunAt,
10146
10249
  nextRunAt: row.nextRunAt,
10147
10250
  createdAt: row.createdAt,
@@ -10270,7 +10373,7 @@ function formatNotification(row) {
10270
10373
 
10271
10374
  // ../api-routes/src/google.ts
10272
10375
  import crypto14 from "crypto";
10273
- import { eq as eq18, and as and6, desc as desc8, sql as sql4 } from "drizzle-orm";
10376
+ import { eq as eq18, and as and8, desc as desc8, sql as sql4 } from "drizzle-orm";
10274
10377
 
10275
10378
  // ../integration-google/src/constants.ts
10276
10379
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -11485,7 +11588,7 @@ async function googleRoutes(app, opts) {
11485
11588
  if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
11486
11589
  if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
11487
11590
  if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
11488
- const rows = app.db.select().from(gscSearchData).where(and6(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
11591
+ const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
11489
11592
  return rows.map((r) => ({
11490
11593
  date: r.date,
11491
11594
  query: r.query,
@@ -11559,7 +11662,7 @@ async function googleRoutes(app, opts) {
11559
11662
  const { url, limit } = request.query;
11560
11663
  const conditions = [eq18(gscUrlInspections.projectId, project.id)];
11561
11664
  if (url) conditions.push(eq18(gscUrlInspections.url, url));
11562
- const rows = app.db.select().from(gscUrlInspections).where(and6(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
11665
+ const rows = app.db.select().from(gscUrlInspections).where(and8(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
11563
11666
  return rows.map((r) => ({
11564
11667
  id: r.id,
11565
11668
  url: r.url,
@@ -11911,7 +12014,7 @@ async function googleRoutes(app, opts) {
11911
12014
 
11912
12015
  // ../api-routes/src/bing.ts
11913
12016
  import crypto15 from "crypto";
11914
- import { eq as eq19, and as and7, desc as desc9 } from "drizzle-orm";
12017
+ import { eq as eq19, and as and9, desc as desc9 } from "drizzle-orm";
11915
12018
 
11916
12019
  // ../integration-bing/src/constants.ts
11917
12020
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -12325,7 +12428,7 @@ async function bingRoutes(app, opts) {
12325
12428
  requireConnectionStore();
12326
12429
  const project = resolveProject(app.db, request.params.name);
12327
12430
  const { url, limit } = request.query;
12328
- const whereClause = url ? and7(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
12431
+ const whereClause = url ? and9(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
12329
12432
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
12330
12433
  return filtered.map((r) => ({
12331
12434
  id: r.id,
@@ -12557,7 +12660,7 @@ async function bingRoutes(app, opts) {
12557
12660
  import fs from "fs";
12558
12661
  import path from "path";
12559
12662
  import os2 from "os";
12560
- import { eq as eq20, and as and8 } from "drizzle-orm";
12663
+ import { eq as eq20, and as and10 } from "drizzle-orm";
12561
12664
  function getScreenshotDir() {
12562
12665
  return path.join(os2.homedir(), ".canonry", "screenshots");
12563
12666
  }
@@ -12630,7 +12733,7 @@ async function cdpRoutes(app, opts) {
12630
12733
  async (request, reply) => {
12631
12734
  const project = resolveProject(app.db, request.params.name);
12632
12735
  const { runId } = request.params;
12633
- const run = app.db.select().from(runs).where(and8(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
12736
+ const run = app.db.select().from(runs).where(and10(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
12634
12737
  if (!run) {
12635
12738
  const err = notFound("Run", runId);
12636
12739
  return reply.code(err.statusCode).send(err.toJSON());
@@ -12727,7 +12830,7 @@ async function cdpRoutes(app, opts) {
12727
12830
 
12728
12831
  // ../api-routes/src/ga.ts
12729
12832
  import crypto16 from "crypto";
12730
- import { eq as eq21, desc as desc10, and as and9, sql as sql5 } from "drizzle-orm";
12833
+ import { eq as eq21, desc as desc10, and as and11, sql as sql5 } from "drizzle-orm";
12731
12834
  function gaLog(level, action, ctx) {
12732
12835
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
12733
12836
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -13022,7 +13125,7 @@ async function ga4Routes(app, opts) {
13022
13125
  app.db.transaction((tx) => {
13023
13126
  if (syncTraffic) {
13024
13127
  tx.delete(gaTrafficSnapshots).where(
13025
- and9(
13128
+ and11(
13026
13129
  eq21(gaTrafficSnapshots.projectId, project.id),
13027
13130
  sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
13028
13131
  sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
@@ -13046,7 +13149,7 @@ async function ga4Routes(app, opts) {
13046
13149
  }
13047
13150
  if (syncAi) {
13048
13151
  tx.delete(gaAiReferrals).where(
13049
- and9(
13152
+ and11(
13050
13153
  eq21(gaAiReferrals.projectId, project.id),
13051
13154
  sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
13052
13155
  sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
@@ -13072,7 +13175,7 @@ async function ga4Routes(app, opts) {
13072
13175
  }
13073
13176
  if (syncSocial) {
13074
13177
  tx.delete(gaSocialReferrals).where(
13075
- and9(
13178
+ and11(
13076
13179
  eq21(gaSocialReferrals.projectId, project.id),
13077
13180
  sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
13078
13181
  sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
@@ -13176,7 +13279,7 @@ async function ga4Routes(app, opts) {
13176
13279
  totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
13177
13280
  totalUsers: gaTrafficWindowSummaries.totalUsers
13178
13281
  }).from(gaTrafficWindowSummaries).where(
13179
- and9(
13282
+ and11(
13180
13283
  eq21(gaTrafficWindowSummaries.projectId, project.id),
13181
13284
  eq21(gaTrafficWindowSummaries.windowKey, window)
13182
13285
  )
@@ -13185,7 +13288,7 @@ async function ga4Routes(app, opts) {
13185
13288
  totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
13186
13289
  totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
13187
13290
  totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
13188
- }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get() : null;
13291
+ }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get() : null;
13189
13292
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
13190
13293
  totalSessions: gaTrafficSummaries.totalSessions,
13191
13294
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
@@ -13193,7 +13296,7 @@ async function ga4Routes(app, opts) {
13193
13296
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
13194
13297
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
13195
13298
  totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
13196
- }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get();
13299
+ }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get();
13197
13300
  const summaryMeta = app.db.select({
13198
13301
  periodStart: gaTrafficSummaries.periodStart,
13199
13302
  periodEnd: gaTrafficSummaries.periodEnd
@@ -13204,14 +13307,14 @@ async function ga4Routes(app, opts) {
13204
13307
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
13205
13308
  directSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
13206
13309
  users: sql5`SUM(${gaTrafficSnapshots.users})`
13207
- }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
13310
+ }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
13208
13311
  const aiReferralRows = app.db.select({
13209
13312
  source: gaAiReferrals.source,
13210
13313
  medium: gaAiReferrals.medium,
13211
13314
  sourceDimension: gaAiReferrals.sourceDimension,
13212
13315
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13213
13316
  users: sql5`SUM(${gaAiReferrals.users})`
13214
- }).from(gaAiReferrals).where(and9(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
13317
+ }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
13215
13318
  const aiReferralLandingPageRows = app.db.select({
13216
13319
  source: gaAiReferrals.source,
13217
13320
  medium: gaAiReferrals.medium,
@@ -13219,7 +13322,7 @@ async function ga4Routes(app, opts) {
13219
13322
  landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
13220
13323
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13221
13324
  users: sql5`SUM(${gaAiReferrals.users})`
13222
- }).from(gaAiReferrals).where(and9(...aiConditions)).groupBy(
13325
+ }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(
13223
13326
  gaAiReferrals.source,
13224
13327
  gaAiReferrals.medium,
13225
13328
  gaAiReferrals.sourceDimension,
@@ -13256,7 +13359,7 @@ async function ga4Routes(app, opts) {
13256
13359
  channelGroup: gaAiReferrals.channelGroup,
13257
13360
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
13258
13361
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
13259
- }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
13362
+ }).from(gaAiReferrals).where(and11(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
13260
13363
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
13261
13364
  let aiBySessionUsers = 0;
13262
13365
  for (const row of aiBySessionRows) {
@@ -13270,11 +13373,11 @@ async function ga4Routes(app, opts) {
13270
13373
  channelGroup: gaSocialReferrals.channelGroup,
13271
13374
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
13272
13375
  users: sql5`SUM(${gaSocialReferrals.users})`
13273
- }).from(gaSocialReferrals).where(and9(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
13376
+ }).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
13274
13377
  const socialTotals = app.db.select({
13275
13378
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
13276
13379
  users: sql5`SUM(${gaSocialReferrals.users})`
13277
- }).from(gaSocialReferrals).where(and9(...socialConditions)).get();
13380
+ }).from(gaSocialReferrals).where(and11(...socialConditions)).get();
13278
13381
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
13279
13382
  const total = summaryRow?.totalSessions ?? 0;
13280
13383
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
@@ -13365,7 +13468,7 @@ async function ga4Routes(app, opts) {
13365
13468
  sourceDimension: gaAiReferrals.sourceDimension,
13366
13469
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13367
13470
  users: sql5`SUM(${gaAiReferrals.users})`
13368
- }).from(gaAiReferrals).where(and9(...conditions)).groupBy(
13471
+ }).from(gaAiReferrals).where(and11(...conditions)).groupBy(
13369
13472
  gaAiReferrals.date,
13370
13473
  gaAiReferrals.source,
13371
13474
  gaAiReferrals.medium,
@@ -13387,7 +13490,7 @@ async function ga4Routes(app, opts) {
13387
13490
  channelGroup: gaSocialReferrals.channelGroup,
13388
13491
  sessions: gaSocialReferrals.sessions,
13389
13492
  users: gaSocialReferrals.users
13390
- }).from(gaSocialReferrals).where(and9(...conditions)).orderBy(gaSocialReferrals.date).all();
13493
+ }).from(gaSocialReferrals).where(and11(...conditions)).orderBy(gaSocialReferrals.date).all();
13391
13494
  return rows;
13392
13495
  });
13393
13496
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -13400,7 +13503,7 @@ async function ga4Routes(app, opts) {
13400
13503
  d.setDate(d.getDate() - n);
13401
13504
  return fmt(d);
13402
13505
  };
13403
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and9(
13506
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
13404
13507
  eq21(gaSocialReferrals.projectId, project.id),
13405
13508
  sql5`${gaSocialReferrals.date} >= ${from}`,
13406
13509
  sql5`${gaSocialReferrals.date} < ${to}`
@@ -13413,7 +13516,7 @@ async function ga4Routes(app, opts) {
13413
13516
  const sourceCurrent = app.db.select({
13414
13517
  source: gaSocialReferrals.source,
13415
13518
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
13416
- }).from(gaSocialReferrals).where(and9(
13519
+ }).from(gaSocialReferrals).where(and11(
13417
13520
  eq21(gaSocialReferrals.projectId, project.id),
13418
13521
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
13419
13522
  sql5`${gaSocialReferrals.date} < ${fmt(today)}`
@@ -13421,7 +13524,7 @@ async function ga4Routes(app, opts) {
13421
13524
  const sourcePrev = app.db.select({
13422
13525
  source: gaSocialReferrals.source,
13423
13526
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
13424
- }).from(gaSocialReferrals).where(and9(
13527
+ }).from(gaSocialReferrals).where(and11(
13425
13528
  eq21(gaSocialReferrals.projectId, project.id),
13426
13529
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
13427
13530
  sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
@@ -13463,16 +13566,16 @@ async function ga4Routes(app, opts) {
13463
13566
  return fmt(d);
13464
13567
  };
13465
13568
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
13466
- const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13467
- const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13468
- const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13469
- const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
13569
+ const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13570
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13571
+ const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
13572
+ const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13470
13573
  eq21(gaAiReferrals.projectId, project.id),
13471
13574
  sql5`${gaAiReferrals.date} >= ${from}`,
13472
13575
  sql5`${gaAiReferrals.date} < ${to}`,
13473
13576
  eq21(gaAiReferrals.sourceDimension, "session")
13474
13577
  )).get();
13475
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
13578
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
13476
13579
  const todayStr = fmt(today);
13477
13580
  const buildTrend = (sum) => {
13478
13581
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -13481,13 +13584,13 @@ async function ga4Routes(app, opts) {
13481
13584
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
13482
13585
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
13483
13586
  };
13484
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
13587
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13485
13588
  eq21(gaAiReferrals.projectId, project.id),
13486
13589
  sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
13487
13590
  sql5`${gaAiReferrals.date} < ${todayStr}`,
13488
13591
  eq21(gaAiReferrals.sourceDimension, "session")
13489
13592
  )).groupBy(gaAiReferrals.source).all();
13490
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
13593
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13491
13594
  eq21(gaAiReferrals.projectId, project.id),
13492
13595
  sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
13493
13596
  sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
@@ -13507,8 +13610,8 @@ async function ga4Routes(app, opts) {
13507
13610
  }
13508
13611
  return mover;
13509
13612
  };
13510
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
13511
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
13613
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
13614
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
13512
13615
  return {
13513
13616
  total: buildTrend(sumTotal),
13514
13617
  organic: buildTrend(sumOrganic),
@@ -13530,7 +13633,7 @@ async function ga4Routes(app, opts) {
13530
13633
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
13531
13634
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
13532
13635
  users: sql5`SUM(${gaTrafficSnapshots.users})`
13533
- }).from(gaTrafficSnapshots).where(and9(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
13636
+ }).from(gaTrafficSnapshots).where(and11(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
13534
13637
  return rows.map((r) => ({
13535
13638
  date: r.date,
13536
13639
  sessions: r.sessions ?? 0,
@@ -15183,7 +15286,7 @@ async function wordpressRoutes(app, opts) {
15183
15286
 
15184
15287
  // ../api-routes/src/backlinks.ts
15185
15288
  import crypto18 from "crypto";
15186
- import { and as and11, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
15289
+ import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
15187
15290
 
15188
15291
  // ../integration-commoncrawl/src/constants.ts
15189
15292
  import os3 from "os";
@@ -15580,7 +15683,7 @@ function pruneCachedRelease(release, opts = {}) {
15580
15683
  }
15581
15684
 
15582
15685
  // ../api-routes/src/backlinks-filter.ts
15583
- import { and as and10, ne, notLike } from "drizzle-orm";
15686
+ import { and as and12, ne, notLike } from "drizzle-orm";
15584
15687
  var BACKLINK_FILTER_PATTERNS = [
15585
15688
  "*.google.com",
15586
15689
  "*.googleusercontent.com",
@@ -15603,7 +15706,7 @@ function backlinkCrawlerExclusionClause() {
15603
15706
  conditions.push(ne(backlinkDomains.linkingDomain, pattern));
15604
15707
  }
15605
15708
  }
15606
- const combined = and10(...conditions);
15709
+ const combined = and12(...conditions);
15607
15710
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
15608
15711
  return combined;
15609
15712
  }
@@ -15664,7 +15767,7 @@ function mapRunRow(row) {
15664
15767
  };
15665
15768
  }
15666
15769
  function latestSummaryForProject(db, projectId, release) {
15667
- const condition = release ? and11(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
15770
+ const condition = release ? and13(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
15668
15771
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
15669
15772
  }
15670
15773
  function parseExcludeCrawlers(value) {
@@ -15673,11 +15776,11 @@ function parseExcludeCrawlers(value) {
15673
15776
  return lower === "1" || lower === "true" || lower === "yes";
15674
15777
  }
15675
15778
  function computeFilteredSummary(db, base) {
15676
- const baseDomainCondition = and11(
15779
+ const baseDomainCondition = and13(
15677
15780
  eq22(backlinkDomains.projectId, base.projectId),
15678
15781
  eq22(backlinkDomains.release, base.release)
15679
15782
  );
15680
- const filteredCondition = and11(baseDomainCondition, backlinkCrawlerExclusionClause());
15783
+ const filteredCondition = and13(baseDomainCondition, backlinkCrawlerExclusionClause());
15681
15784
  const unfilteredAgg = db.select({
15682
15785
  count: sql6`count(*)`,
15683
15786
  total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -15853,11 +15956,11 @@ async function backlinksRoutes(app, opts) {
15853
15956
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
15854
15957
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
15855
15958
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
15856
- const baseDomainCondition = and11(
15959
+ const baseDomainCondition = and13(
15857
15960
  eq22(backlinkDomains.projectId, project.id),
15858
15961
  eq22(backlinkDomains.release, targetRelease)
15859
15962
  );
15860
- const domainCondition = excludeCrawlers ? and11(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
15963
+ const domainCondition = excludeCrawlers ? and13(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
15861
15964
  const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
15862
15965
  const rows = app.db.select({
15863
15966
  linkingDomain: backlinkDomains.linkingDomain,
@@ -15893,7 +15996,7 @@ async function backlinksRoutes(app, opts) {
15893
15996
 
15894
15997
  // ../api-routes/src/traffic.ts
15895
15998
  import crypto20 from "crypto";
15896
- import { eq as eq23, sql as sql7 } from "drizzle-orm";
15999
+ import { and as and14, desc as desc12, eq as eq23, gte, lte, sql as sql7 } from "drizzle-orm";
15897
16000
 
15898
16001
  // ../integration-cloud-run/src/auth.ts
15899
16002
  import crypto19 from "crypto";
@@ -16500,10 +16603,11 @@ function incrementBucket(map, key, fields) {
16500
16603
  }
16501
16604
 
16502
16605
  // ../api-routes/src/traffic.ts
16503
- var DEFAULT_SYNC_WINDOW_MINUTES = 60;
16606
+ var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
16504
16607
  var DEFAULT_PAGE_SIZE2 = 1e3;
16505
16608
  var DEFAULT_MAX_PAGES2 = 5;
16506
16609
  var DEFAULT_SAMPLE_LIMIT2 = 100;
16610
+ var MAX_TRACKED_EVENT_IDS = 1e3;
16507
16611
  function parseSourceConfig(row) {
16508
16612
  return parseJsonColumn(row.configJson, {});
16509
16613
  }
@@ -16664,17 +16768,24 @@ async function trafficRoutes(app, opts) {
16664
16768
  kind: RunKinds["traffic-sync"],
16665
16769
  status: RunStatuses.running,
16666
16770
  trigger: RunTriggers.manual,
16771
+ sourceId: sourceRow.id,
16667
16772
  startedAt,
16668
16773
  createdAt: startedAt
16669
16774
  }).run();
16775
+ const markFailed = (msg) => {
16776
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
16777
+ app.db.transaction((tx) => {
16778
+ tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq23(runs.id, runId)).run();
16779
+ tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq23(trafficSources.id, sourceRow.id)).run();
16780
+ });
16781
+ };
16670
16782
  let accessToken;
16671
16783
  try {
16672
16784
  accessToken = await resolveAccessToken2(credential);
16673
16785
  } catch (e) {
16674
16786
  const msg = e instanceof Error ? e.message : String(e);
16675
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16676
- app.db.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(trafficSources.id, sourceRow.id)).run();
16677
- throw validationError(`Failed to resolve Cloud Run access token: ${msg}`);
16787
+ markFailed(msg);
16788
+ throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
16678
16789
  }
16679
16790
  let allEvents = [];
16680
16791
  try {
@@ -16690,11 +16801,23 @@ async function trafficRoutes(app, opts) {
16690
16801
  allEvents = page.events;
16691
16802
  } catch (e) {
16692
16803
  const msg = e instanceof Error ? e.message : String(e);
16693
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16694
- app.db.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(trafficSources.id, sourceRow.id)).run();
16695
- throw validationError(`Cloud Run pull failed: ${msg}`);
16696
- }
16697
- const report = buildTrafficProbeReport(allEvents, { sampleLimit });
16804
+ markFailed(msg);
16805
+ throw providerError(`Cloud Run pull failed: ${msg}`);
16806
+ }
16807
+ const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
16808
+ const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
16809
+ const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
16810
+ const previousIds = parseJsonColumn(sourceRow.lastEventIds, []);
16811
+ const merged = [];
16812
+ const mergedSet = /* @__PURE__ */ new Set();
16813
+ for (const id of [...newSorted, ...previousIds]) {
16814
+ if (mergedSet.has(id)) continue;
16815
+ mergedSet.add(id);
16816
+ merged.push(id);
16817
+ if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
16818
+ }
16819
+ const nextEventIds = merged;
16820
+ const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
16698
16821
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
16699
16822
  let crawlerBucketRows = 0;
16700
16823
  let aiReferralBucketRows = 0;
@@ -16800,6 +16923,7 @@ async function trafficRoutes(app, opts) {
16800
16923
  status: TrafficSourceStatuses.connected,
16801
16924
  lastSyncedAt: finishedAt,
16802
16925
  lastError: null,
16926
+ lastEventIds: JSON.stringify(nextEventIds),
16803
16927
  updatedAt: finishedAt
16804
16928
  }).where(eq23(trafficSources.id, sourceRow.id)).run();
16805
16929
  tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq23(runs.id, runId)).run();
@@ -16827,6 +16951,177 @@ async function trafficRoutes(app, opts) {
16827
16951
  };
16828
16952
  return response;
16829
16953
  });
16954
+ function buildSourceDetail(projectId, row, since) {
16955
+ const crawlerTotals = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
16956
+ and14(
16957
+ eq23(crawlerEventsHourly.sourceId, row.id),
16958
+ gte(crawlerEventsHourly.tsHour, since)
16959
+ )
16960
+ ).get();
16961
+ const aiTotals = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
16962
+ and14(
16963
+ eq23(aiReferralEventsHourly.sourceId, row.id),
16964
+ gte(aiReferralEventsHourly.tsHour, since)
16965
+ )
16966
+ ).get();
16967
+ const sampleTotals = app.db.select({ total: sql7`COUNT(*)` }).from(rawEventSamples).where(
16968
+ and14(
16969
+ eq23(rawEventSamples.sourceId, row.id),
16970
+ gte(rawEventSamples.ts, since)
16971
+ )
16972
+ ).get();
16973
+ const latestRun = app.db.select().from(runs).where(
16974
+ and14(
16975
+ eq23(runs.projectId, projectId),
16976
+ eq23(runs.kind, RunKinds["traffic-sync"]),
16977
+ eq23(runs.sourceId, row.id)
16978
+ )
16979
+ ).orderBy(desc12(runs.startedAt)).limit(1).get();
16980
+ return {
16981
+ ...rowToDto(row),
16982
+ totals24h: {
16983
+ crawlerHits: Number(crawlerTotals?.total ?? 0),
16984
+ aiReferralHits: Number(aiTotals?.total ?? 0),
16985
+ sampleCount: Number(sampleTotals?.total ?? 0)
16986
+ },
16987
+ latestRun: latestRun ? {
16988
+ runId: latestRun.id,
16989
+ status: latestRun.status,
16990
+ startedAt: latestRun.startedAt,
16991
+ finishedAt: latestRun.finishedAt ?? null,
16992
+ error: latestRun.error ?? null
16993
+ } : null
16994
+ };
16995
+ }
16996
+ app.get("/projects/:name/traffic/sources", async (request) => {
16997
+ const project = resolveProject(app.db, request.params.name);
16998
+ const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
16999
+ const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
17000
+ const response = { sources };
17001
+ return response;
17002
+ });
17003
+ app.get("/projects/:name/traffic/status", async (request) => {
17004
+ const project = resolveProject(app.db, request.params.name);
17005
+ const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
17006
+ const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
17007
+ const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
17008
+ const response = { sources };
17009
+ return response;
17010
+ });
17011
+ app.get(
17012
+ "/projects/:name/traffic/sources/:id",
17013
+ async (request) => {
17014
+ const project = resolveProject(app.db, request.params.name);
17015
+ const row = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
17016
+ if (!row || row.projectId !== project.id) {
17017
+ throw notFound("Traffic source", request.params.id);
17018
+ }
17019
+ const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
17020
+ return buildSourceDetail(project.id, row, since);
17021
+ }
17022
+ );
17023
+ app.get("/projects/:name/traffic/events", async (request) => {
17024
+ const project = resolveProject(app.db, request.params.name);
17025
+ const now = /* @__PURE__ */ new Date();
17026
+ const defaultSince = new Date(now.getTime() - 24 * 60 * 6e4);
17027
+ const sinceParam = request.query?.since;
17028
+ const untilParam = request.query?.until;
17029
+ const since = sinceParam ? new Date(sinceParam) : defaultSince;
17030
+ const until = untilParam ? new Date(untilParam) : now;
17031
+ if (Number.isNaN(since.getTime())) {
17032
+ throw validationError('"since" must be an ISO-8601 timestamp');
17033
+ }
17034
+ if (Number.isNaN(until.getTime())) {
17035
+ throw validationError('"until" must be an ISO-8601 timestamp');
17036
+ }
17037
+ if (since.getTime() > until.getTime()) {
17038
+ throw validationError('"since" must be \u2264 "until"');
17039
+ }
17040
+ const kindParam = request.query?.kind;
17041
+ let kind = "all";
17042
+ if (kindParam !== void 0) {
17043
+ if (kindParam === "all" || kindParam === TrafficEventKinds.crawler || kindParam === TrafficEventKinds["ai-referral"]) {
17044
+ kind = kindParam;
17045
+ } else {
17046
+ throw validationError(`"kind" must be one of: all, ${TrafficEventKinds.crawler}, ${TrafficEventKinds["ai-referral"]}`);
17047
+ }
17048
+ }
17049
+ const limitParam = request.query?.limit;
17050
+ const requestedLimit = limitParam ? parseInt(limitParam, 10) : 500;
17051
+ if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
17052
+ throw validationError('"limit" must be a positive integer');
17053
+ }
17054
+ const limit = Math.min(requestedLimit, 5e3);
17055
+ const sourceIdParam = request.query?.sourceId;
17056
+ const sinceIso = since.toISOString();
17057
+ const untilIso = until.toISOString();
17058
+ const events = [];
17059
+ let crawlerTotal = 0;
17060
+ let aiReferralTotal = 0;
17061
+ if (kind === "all" || kind === TrafficEventKinds.crawler) {
17062
+ const crawlerFilters = [
17063
+ eq23(crawlerEventsHourly.projectId, project.id),
17064
+ gte(crawlerEventsHourly.tsHour, sinceIso),
17065
+ lte(crawlerEventsHourly.tsHour, untilIso)
17066
+ ];
17067
+ if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
17068
+ const crawlerWhere = and14(...crawlerFilters);
17069
+ const total = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
17070
+ crawlerTotal = Number(total?.total ?? 0);
17071
+ const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
17072
+ for (const r of rows) {
17073
+ events.push({
17074
+ kind: TrafficEventKinds.crawler,
17075
+ sourceId: r.sourceId,
17076
+ tsHour: r.tsHour,
17077
+ botId: r.botId,
17078
+ operator: r.operator,
17079
+ verificationStatus: r.verificationStatus,
17080
+ pathNormalized: r.pathNormalized,
17081
+ status: r.status,
17082
+ hits: r.hits
17083
+ });
17084
+ }
17085
+ }
17086
+ if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
17087
+ const aiFilters = [
17088
+ eq23(aiReferralEventsHourly.projectId, project.id),
17089
+ gte(aiReferralEventsHourly.tsHour, sinceIso),
17090
+ lte(aiReferralEventsHourly.tsHour, untilIso)
17091
+ ];
17092
+ if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
17093
+ const aiWhere = and14(...aiFilters);
17094
+ const total = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
17095
+ aiReferralTotal = Number(total?.total ?? 0);
17096
+ const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
17097
+ for (const r of rows) {
17098
+ events.push({
17099
+ kind: TrafficEventKinds["ai-referral"],
17100
+ sourceId: r.sourceId,
17101
+ tsHour: r.tsHour,
17102
+ product: r.product,
17103
+ operator: r.operator,
17104
+ sourceDomain: r.sourceDomain,
17105
+ evidenceType: r.evidenceType,
17106
+ landingPathNormalized: r.landingPathNormalized,
17107
+ status: r.status,
17108
+ hits: r.sessionsOrHits
17109
+ });
17110
+ }
17111
+ }
17112
+ events.sort((a, b) => a.tsHour < b.tsHour ? 1 : a.tsHour > b.tsHour ? -1 : 0);
17113
+ const trimmed = events.slice(0, limit);
17114
+ const response = {
17115
+ windowStart: sinceIso,
17116
+ windowEnd: untilIso,
17117
+ totals: {
17118
+ crawlerHits: crawlerTotal,
17119
+ aiReferralHits: aiReferralTotal
17120
+ },
17121
+ events: trimmed
17122
+ };
17123
+ return response;
17124
+ });
16830
17125
  }
16831
17126
 
16832
17127
  // ../api-routes/src/doctor/checks/bing-auth.ts
@@ -20198,7 +20493,7 @@ import crypto22 from "crypto";
20198
20493
  import fs7 from "fs";
20199
20494
  import path9 from "path";
20200
20495
  import os5 from "os";
20201
- import { and as and12, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
20496
+ import { and as and15, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
20202
20497
 
20203
20498
  // src/run-telemetry.ts
20204
20499
  import crypto21 from "crypto";
@@ -20577,7 +20872,7 @@ var JobRunner = class {
20577
20872
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
20578
20873
  }
20579
20874
  if (existingRun.status === "queued") {
20580
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and12(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
20875
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and15(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
20581
20876
  }
20582
20877
  this.throwIfRunCancelled(runId);
20583
20878
  const project = this.db.select().from(projects).where(eq24(projects.id, projectId)).get();
@@ -20936,7 +21231,7 @@ function buildPhases(input) {
20936
21231
 
20937
21232
  // src/gsc-sync.ts
20938
21233
  import crypto23 from "crypto";
20939
- import { eq as eq25, and as and13, sql as sql9 } from "drizzle-orm";
21234
+ import { eq as eq25, and as and16, sql as sql9 } from "drizzle-orm";
20940
21235
  var log2 = createLogger("GscSync");
20941
21236
  function formatDate3(d) {
20942
21237
  return d.toISOString().split("T")[0];
@@ -20988,7 +21283,7 @@ async function executeGscSync(db, runId, projectId, opts) {
20988
21283
  });
20989
21284
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
20990
21285
  db.delete(gscSearchData).where(
20991
- and13(
21286
+ and16(
20992
21287
  eq25(gscSearchData.projectId, projectId),
20993
21288
  sql9`${gscSearchData.date} >= ${startDate}`,
20994
21289
  sql9`${gscSearchData.date} <= ${endDate}`
@@ -21077,7 +21372,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21077
21372
  }
21078
21373
  }
21079
21374
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
21080
- db.delete(gscCoverageSnapshots).where(and13(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
21375
+ db.delete(gscCoverageSnapshots).where(and16(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
21081
21376
  db.insert(gscCoverageSnapshots).values({
21082
21377
  id: crypto23.randomUUID(),
21083
21378
  projectId,
@@ -21100,7 +21395,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21100
21395
 
21101
21396
  // src/gsc-inspect-sitemap.ts
21102
21397
  import crypto24 from "crypto";
21103
- import { eq as eq26, and as and14 } from "drizzle-orm";
21398
+ import { eq as eq26, and as and17 } from "drizzle-orm";
21104
21399
 
21105
21400
  // src/sitemap-parser.ts
21106
21401
  var log3 = createLogger("SitemapParser");
@@ -21316,7 +21611,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21316
21611
  }
21317
21612
  }
21318
21613
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
21319
- db.delete(gscCoverageSnapshots).where(and14(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
21614
+ db.delete(gscCoverageSnapshots).where(and17(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
21320
21615
  db.insert(gscCoverageSnapshots).values({
21321
21616
  id: crypto24.randomUUID(),
21322
21617
  projectId,
@@ -21340,7 +21635,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21340
21635
 
21341
21636
  // src/bing-inspect-sitemap.ts
21342
21637
  import crypto25 from "crypto";
21343
- import { eq as eq27, desc as desc12 } from "drizzle-orm";
21638
+ import { eq as eq27, desc as desc13 } from "drizzle-orm";
21344
21639
  var log5 = createLogger("BingInspectSitemap");
21345
21640
  function parseBingDate2(value) {
21346
21641
  if (!value) return null;
@@ -21461,7 +21756,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21461
21756
  await new Promise((r) => setTimeout(r, 1e3));
21462
21757
  }
21463
21758
  }
21464
- const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
21759
+ const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc13(bingUrlInspections.inspectedAt)).all();
21465
21760
  const latestByUrl = /* @__PURE__ */ new Map();
21466
21761
  const definitiveByUrl = /* @__PURE__ */ new Map();
21467
21762
  for (const row of allInspections) {
@@ -21527,7 +21822,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21527
21822
  // src/commoncrawl-sync.ts
21528
21823
  import crypto26 from "crypto";
21529
21824
  import path10 from "path";
21530
- import { and as and15, eq as eq28, sql as sql10 } from "drizzle-orm";
21825
+ import { and as and18, eq as eq28, sql as sql10 } from "drizzle-orm";
21531
21826
  var log6 = createLogger("CommonCrawlSync");
21532
21827
  var INSERT_CHUNK_SIZE = 1e4;
21533
21828
  function defaultDeps() {
@@ -21718,7 +22013,7 @@ function computeSummary(rows) {
21718
22013
  // src/backlink-extract.ts
21719
22014
  import crypto27 from "crypto";
21720
22015
  import fs8 from "fs";
21721
- import { and as and16, desc as desc13, eq as eq29 } from "drizzle-orm";
22016
+ import { and as and19, desc as desc14, eq as eq29 } from "drizzle-orm";
21722
22017
  var log7 = createLogger("BacklinkExtract");
21723
22018
  function defaultDeps2() {
21724
22019
  return {
@@ -21736,7 +22031,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
21736
22031
  if (!project) {
21737
22032
  throw new Error(`Project not found: ${projectId}`);
21738
22033
  }
21739
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc13(ccReleaseSyncs.createdAt)).limit(1).get();
22034
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc14(ccReleaseSyncs.createdAt)).limit(1).get();
21740
22035
  if (!sync) {
21741
22036
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
21742
22037
  }
@@ -21764,7 +22059,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
21764
22059
  const targetDomain = project.canonicalDomain;
21765
22060
  db.transaction((tx) => {
21766
22061
  tx.delete(backlinkDomains).where(
21767
- and16(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
22062
+ and19(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
21768
22063
  ).run();
21769
22064
  if (rows.length > 0) {
21770
22065
  const values = rows.map((r) => ({
@@ -21886,8 +22181,11 @@ var ProviderRegistry = class {
21886
22181
 
21887
22182
  // src/scheduler.ts
21888
22183
  import cron from "node-cron";
21889
- import { eq as eq30 } from "drizzle-orm";
22184
+ import { and as and20, eq as eq30 } from "drizzle-orm";
21890
22185
  var log8 = createLogger("Scheduler");
22186
+ function taskKey(projectId, kind) {
22187
+ return `${projectId}::${kind}`;
22188
+ }
21891
22189
  var Scheduler = class {
21892
22190
  db;
21893
22191
  callbacks;
@@ -21903,78 +22201,110 @@ var Scheduler = class {
21903
22201
  const missedRunAt = schedule.nextRunAt;
21904
22202
  this.registerCronTask(schedule);
21905
22203
  if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
21906
- log8.info("run.catch-up", { projectId: schedule.projectId, missedRunAt });
21907
- this.triggerRun(schedule.id, schedule.projectId);
22204
+ log8.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
22205
+ this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
21908
22206
  }
21909
22207
  }
21910
22208
  log8.info("started", { scheduleCount: allSchedules.length });
21911
22209
  }
21912
22210
  /** Stop all cron tasks for graceful shutdown. */
21913
22211
  stop() {
21914
- for (const [projectId, task] of this.tasks) {
21915
- this.stopTask(projectId, task, "Stopped");
22212
+ for (const [key, task] of this.tasks) {
22213
+ this.stopTask(key, task, "Stopped");
21916
22214
  }
21917
22215
  this.tasks.clear();
21918
22216
  }
21919
- /** Add or update a cron registration at runtime (called when schedule API is used). */
21920
- upsert(projectId) {
21921
- const existing = this.tasks.get(projectId);
22217
+ /**
22218
+ * Add or update a cron registration at runtime (called when schedule API
22219
+ * is used). Keyed by `(projectId, kind)` so a project's traffic-sync and
22220
+ * answer-visibility schedules can coexist independently.
22221
+ */
22222
+ upsert(projectId, kind) {
22223
+ const key = taskKey(projectId, kind);
22224
+ const existing = this.tasks.get(key);
21922
22225
  if (existing) {
21923
- this.stopTask(projectId, existing, "Stopped");
21924
- this.tasks.delete(projectId);
22226
+ this.stopTask(key, existing, "Stopped");
22227
+ this.tasks.delete(key);
21925
22228
  }
21926
- const schedule = this.db.select().from(schedules).where(eq30(schedules.projectId, projectId)).get();
22229
+ const schedule = this.db.select().from(schedules).where(and20(eq30(schedules.projectId, projectId), eq30(schedules.kind, kind))).get();
21927
22230
  if (schedule && schedule.enabled === 1) {
21928
22231
  this.registerCronTask(schedule);
21929
22232
  }
21930
22233
  }
21931
- /** Remove a cron registration (called when schedule is deleted). */
21932
- remove(projectId) {
21933
- const existing = this.tasks.get(projectId);
22234
+ /** Remove a single cron registration (kind-scoped). */
22235
+ remove(projectId, kind) {
22236
+ const key = taskKey(projectId, kind);
22237
+ const existing = this.tasks.get(key);
21934
22238
  if (existing) {
21935
- this.stopTask(projectId, existing, "Removed");
21936
- this.tasks.delete(projectId);
22239
+ this.stopTask(key, existing, "Removed");
22240
+ this.tasks.delete(key);
21937
22241
  }
21938
22242
  }
21939
- stopTask(projectId, task, verb) {
22243
+ /** Remove ALL cron registrations for a project (used on project delete). */
22244
+ removeAllForProject(projectId) {
22245
+ for (const kind of Object.values(SchedulableRunKinds)) {
22246
+ this.remove(projectId, kind);
22247
+ }
22248
+ }
22249
+ stopTask(key, task, verb) {
21940
22250
  task.stop();
21941
22251
  task.destroy();
21942
- log8.info(`task.${verb.toLowerCase()}`, { projectId });
22252
+ log8.info(`task.${verb.toLowerCase()}`, { key });
21943
22253
  }
21944
22254
  registerCronTask(schedule) {
21945
22255
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
22256
+ const kind = schedule.kind;
21946
22257
  if (!cron.validate(cronExpr)) {
21947
- log8.error("cron.invalid", { projectId, cronExpr });
22258
+ log8.error("cron.invalid", { projectId, kind, cronExpr });
21948
22259
  return;
21949
22260
  }
21950
22261
  const task = cron.schedule(cronExpr, () => {
21951
- this.triggerRun(scheduleId, projectId);
22262
+ this.triggerRun(scheduleId, projectId, kind);
21952
22263
  }, {
21953
22264
  timezone
21954
22265
  });
21955
- this.tasks.set(projectId, task);
22266
+ this.tasks.set(taskKey(projectId, kind), task);
21956
22267
  this.db.update(schedules).set({
21957
22268
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
21958
22269
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21959
22270
  }).where(eq30(schedules.id, scheduleId)).run();
21960
22271
  const label = schedule.preset ?? cronExpr;
21961
- log8.info("cron.registered", { projectId, schedule: label, timezone });
22272
+ log8.info("cron.registered", { projectId, kind, schedule: label, timezone });
21962
22273
  }
21963
- triggerRun(scheduleId, projectId) {
22274
+ triggerRun(scheduleId, projectId, kind) {
21964
22275
  try {
21965
22276
  const now = (/* @__PURE__ */ new Date()).toISOString();
21966
22277
  const currentSchedule = this.db.select().from(schedules).where(eq30(schedules.id, scheduleId)).get();
21967
22278
  if (!currentSchedule || currentSchedule.enabled !== 1) {
21968
- log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
21969
- this.remove(projectId);
22279
+ log8.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
22280
+ this.remove(projectId, kind);
21970
22281
  return;
21971
22282
  }
21972
- const task = this.tasks.get(projectId);
22283
+ const task = this.tasks.get(taskKey(projectId, kind));
21973
22284
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
21974
22285
  const project = this.db.select().from(projects).where(eq30(projects.id, projectId)).get();
21975
22286
  if (!project) {
21976
- log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
21977
- this.remove(projectId);
22287
+ log8.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
22288
+ this.remove(projectId, kind);
22289
+ return;
22290
+ }
22291
+ if (kind === SchedulableRunKinds["traffic-sync"]) {
22292
+ const sourceId = currentSchedule.sourceId;
22293
+ if (!sourceId) {
22294
+ log8.warn("traffic-sync.missing-source", { scheduleId, projectId });
22295
+ return;
22296
+ }
22297
+ if (!this.callbacks.onTrafficSyncRequested) {
22298
+ log8.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
22299
+ return;
22300
+ }
22301
+ this.db.update(schedules).set({
22302
+ lastRunAt: now,
22303
+ nextRunAt,
22304
+ updatedAt: now
22305
+ }).where(eq30(schedules.id, currentSchedule.id)).run();
22306
+ log8.info("traffic-sync.triggered", { projectName: project.name, sourceId });
22307
+ this.callbacks.onTrafficSyncRequested(project.name, sourceId);
21978
22308
  return;
21979
22309
  }
21980
22310
  const projectLocations = parseJsonColumn(project.locations, []);
@@ -22014,13 +22344,13 @@ var Scheduler = class {
22014
22344
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
22015
22345
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
22016
22346
  } catch (err) {
22017
- log8.error("trigger.error", { scheduleId, projectId, error: err instanceof Error ? err.message : String(err) });
22347
+ log8.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
22018
22348
  }
22019
22349
  }
22020
22350
  };
22021
22351
 
22022
22352
  // src/notifier.ts
22023
- import { eq as eq31, desc as desc14, and as and17, or as or4 } from "drizzle-orm";
22353
+ import { eq as eq31, desc as desc15, and as and21, or as or4 } from "drizzle-orm";
22024
22354
  import crypto28 from "crypto";
22025
22355
  var log9 = createLogger("Notifier");
22026
22356
  var Notifier = class {
@@ -22126,11 +22456,11 @@ var Notifier = class {
22126
22456
  }
22127
22457
  computeTransitions(runId, projectId) {
22128
22458
  const recentRuns = this.db.select().from(runs).where(
22129
- and17(
22459
+ and21(
22130
22460
  eq31(runs.projectId, projectId),
22131
22461
  or4(eq31(runs.status, "completed"), eq31(runs.status, "partial"))
22132
22462
  )
22133
- ).orderBy(desc14(runs.createdAt)).limit(2).all();
22463
+ ).orderBy(desc15(runs.createdAt)).limit(2).all();
22134
22464
  if (recentRuns.length < 2) return [];
22135
22465
  const currentRunId = recentRuns[0].id;
22136
22466
  const previousRunId = recentRuns[1].id;
@@ -22612,7 +22942,7 @@ function resolveSessionProviderAndModel(config, opts) {
22612
22942
 
22613
22943
  // src/agent/memory-store.ts
22614
22944
  import crypto29 from "crypto";
22615
- import { and as and18, desc as desc15, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
22945
+ import { and as and22, desc as desc16, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
22616
22946
  var COMPACTION_KEY_PREFIX = "compaction:";
22617
22947
  var COMPACTION_NOTES_PER_SESSION = 3;
22618
22948
  function rowToDto2(row) {
@@ -22626,7 +22956,7 @@ function rowToDto2(row) {
22626
22956
  };
22627
22957
  }
22628
22958
  function listMemoryEntries(db, projectId, opts = {}) {
22629
- const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc15(agentMemory.updatedAt));
22959
+ const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc16(agentMemory.updatedAt));
22630
22960
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
22631
22961
  return rows.map(rowToDto2);
22632
22962
  }
@@ -22657,12 +22987,12 @@ function upsertMemoryEntry(db, args) {
22657
22987
  updatedAt: now
22658
22988
  }
22659
22989
  }).run();
22660
- const row = db.select().from(agentMemory).where(and18(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
22990
+ const row = db.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
22661
22991
  if (!row) throw new Error("memory upsert produced no row");
22662
22992
  return rowToDto2(row);
22663
22993
  }
22664
22994
  function deleteMemoryEntry(db, projectId, key) {
22665
- const result = db.delete(agentMemory).where(and18(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
22995
+ const result = db.delete(agentMemory).where(and22(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
22666
22996
  const changes = result.changes ?? 0;
22667
22997
  return changes > 0;
22668
22998
  }
@@ -22691,16 +23021,16 @@ function writeCompactionNote(db, args) {
22691
23021
  }).run();
22692
23022
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
22693
23023
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
22694
- and18(
23024
+ and22(
22695
23025
  eq32(agentMemory.projectId, args.projectId),
22696
23026
  like2(agentMemory.key, `${sessionPrefix}%`)
22697
23027
  )
22698
- ).orderBy(desc15(agentMemory.updatedAt)).all();
23028
+ ).orderBy(desc16(agentMemory.updatedAt)).all();
22699
23029
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
22700
23030
  if (stale.length > 0) {
22701
23031
  tx.delete(agentMemory).where(sql11`${agentMemory.id} IN (${sql11.join(stale.map((s) => sql11`${s}`), sql11`, `)})`).run();
22702
23032
  }
22703
- const row = tx.select().from(agentMemory).where(and18(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
23033
+ const row = tx.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
22704
23034
  if (row) inserted = rowToDto2(row);
22705
23035
  });
22706
23036
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -24367,6 +24697,11 @@ async function createServer(opts) {
24367
24697
  jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
24368
24698
  app.log.error({ runId, err }, "Scheduled job runner failed");
24369
24699
  });
24700
+ },
24701
+ onTrafficSyncRequested: (projectName, sourceId) => {
24702
+ aeroClient.trafficSync(projectName, sourceId).catch((err) => {
24703
+ app.log.error({ projectName, sourceId, err: err instanceof Error ? err.message : String(err) }, "Scheduled traffic sync failed");
24704
+ });
24370
24705
  }
24371
24706
  });
24372
24707
  const providerSummary = API_ADAPTERS.map((adapter) => ({
@@ -24898,12 +25233,12 @@ async function createServer(opts) {
24898
25233
  return null;
24899
25234
  }
24900
25235
  },
24901
- onScheduleUpdated: (action, projectId) => {
24902
- if (action === "upsert") scheduler.upsert(projectId);
24903
- if (action === "delete") scheduler.remove(projectId);
25236
+ onScheduleUpdated: (action, projectId, kind) => {
25237
+ if (action === "upsert") scheduler.upsert(projectId, kind);
25238
+ if (action === "delete") scheduler.remove(projectId, kind);
24904
25239
  },
24905
25240
  onProjectDeleted: (projectId) => {
24906
- scheduler.remove(projectId);
25241
+ scheduler.removeAllForProject(projectId);
24907
25242
  },
24908
25243
  getTelemetryStatus: () => {
24909
25244
  const enabled = isTelemetryEnabled();