@ainyc/canonry 4.17.1 → 4.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-6TWKC3DP.js";
8
+ } from "./chunk-P3SFTXHG.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-PAZCY4FF.js";
69
+ } from "./chunk-BN2VQDZ2.js";
70
70
  import {
71
71
  AGENT_MEMORY_VALUE_MAX_BYTES,
72
72
  AGENT_PROVIDER_IDS,
@@ -88,6 +88,7 @@ import {
88
88
  TrafficSourceAuthModes,
89
89
  TrafficSourceStatuses,
90
90
  TrafficSourceTypes,
91
+ VerificationStatuses,
91
92
  absolutizeProjectUrl,
92
93
  actionConfidenceLabel,
93
94
  agentBusy,
@@ -106,6 +107,8 @@ import {
106
107
  dedupeReportActions,
107
108
  dedupeReportOpportunities,
108
109
  deliveryFailed,
110
+ deltaPercent,
111
+ deltaTone,
109
112
  determineAnswerMentioned,
110
113
  effectiveDomains,
111
114
  emptyCitationVisibility,
@@ -113,6 +116,7 @@ import {
113
116
  findDuplicateLocationLabels,
114
117
  formatDate,
115
118
  formatDateRange,
119
+ formatDeltaCopy,
116
120
  formatIsoDate,
117
121
  formatNumber,
118
122
  formatRatio,
@@ -155,7 +159,7 @@ import {
155
159
  visibilityStateFromAnswerMentioned,
156
160
  windowCutoff,
157
161
  wordpressEnvSchema
158
- } from "./chunk-Q2OED5JQ.js";
162
+ } from "./chunk-SBZTDECX.js";
159
163
 
160
164
  // src/telemetry.ts
161
165
  import crypto from "crypto";
@@ -314,7 +318,7 @@ import crypto31 from "crypto";
314
318
  import fs12 from "fs";
315
319
  import path14 from "path";
316
320
  import { fileURLToPath as fileURLToPath2 } from "url";
317
- import { eq as eq35 } from "drizzle-orm";
321
+ import { eq as eq36 } from "drizzle-orm";
318
322
  import Fastify from "fastify";
319
323
 
320
324
  // ../api-routes/src/auth.ts
@@ -2654,7 +2658,7 @@ async function intelligenceRoutes(app) {
2654
2658
  }
2655
2659
 
2656
2660
  // ../api-routes/src/report.ts
2657
- import { and as and5, desc as desc6, eq as eq13, inArray as inArray4, or as or2 } from "drizzle-orm";
2661
+ import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray4, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
2658
2662
 
2659
2663
  // ../api-routes/src/report-renderer.ts
2660
2664
  var COLORS = {
@@ -4268,11 +4272,151 @@ function renderAiReferrals(report) {
4268
4272
  </div>`
4269
4273
  );
4270
4274
  }
4275
+ function serverActivityHeading(audience, hasData) {
4276
+ const isClient = audience === "client";
4277
+ return {
4278
+ id: "server-activity",
4279
+ eyebrow: isClient ? "AI engine attention" : "Section 10",
4280
+ title: "AI Visibility \u2014 Server-Side",
4281
+ intro: isClient ? hasData ? "What AI engines actually do in your server logs over the last 7 days \u2014 the other half of citations." : "Live telemetry from your server logs." : "What AI engines actually do in your server logs \u2014 direct evidence, complementary to citations (which measure what they say)."
4282
+ };
4283
+ }
4284
+ function renderServerActivity(report, audience) {
4285
+ const sa = report.serverActivity;
4286
+ const isClient = audience === "client";
4287
+ if (!sa) {
4288
+ if (isClient) return "";
4289
+ return section(
4290
+ serverActivityHeading("agency", false),
4291
+ renderEmpty("Connect a server-side traffic source to surface what AI engines do directly in your server logs \u2014 distinct from GA4 click-throughs.")
4292
+ );
4293
+ }
4294
+ if (!sa.hasData) {
4295
+ return section(
4296
+ serverActivityHeading(audience, false),
4297
+ renderEmpty(isClient ? "Your server-side traffic source is connected. Numbers will appear after the next sync." : "Source connected \u2014 collecting your first data. Numbers will appear after the next sync.")
4298
+ );
4299
+ }
4300
+ const formatDelta = (d, suffix) => {
4301
+ const copy = formatDeltaCopy(d, suffix);
4302
+ if (!copy) return "";
4303
+ return `<span class="tone-${deltaTone(d.deltaPct)}">${escapeHtml(copy)}</span>`;
4304
+ };
4305
+ if (isClient) {
4306
+ const clientOperators = sa.byOperator.filter((o) => o.verifiedHits > 0 || o.referralArrivals > 0).slice(0, 5);
4307
+ const clientOperatorRows = clientOperators.map((o) => `
4308
+ <tr>
4309
+ <td>${escapeHtml(o.operator)}</td>
4310
+ <td class="numeric">${formatNumber(o.verifiedHits)}</td>
4311
+ <td class="numeric">${formatNumber(o.referralArrivals)}</td>
4312
+ </tr>`).join("");
4313
+ return section(
4314
+ serverActivityHeading("client", true),
4315
+ `<div class="metric-grid">
4316
+ <div class="metric">
4317
+ <div class="label">AI bots visited your site</div>
4318
+ <div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
4319
+ <div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "crawls")}</div>
4320
+ </div>
4321
+ <div class="metric">
4322
+ <div class="label">People clicked through from AI</div>
4323
+ <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
4324
+ <div class="subtitle">${formatDelta(sa.referralArrivals, "arrivals")}</div>
4325
+ </div>
4326
+ </div>
4327
+ ${clientOperatorRows ? `<div class="chart-card"><h3>By AI tool</h3>
4328
+ <table class="report-table">
4329
+ <thead><tr><th>AI tool</th><th class="numeric">Bot visits (7d)</th><th class="numeric">Click-throughs</th></tr></thead>
4330
+ <tbody>${clientOperatorRows}</tbody>
4331
+ </table>
4332
+ <p class="meta">Verified visits only. We confirm each bot via reverse-DNS so the numbers above can't be inflated by anyone faking a user agent.</p>
4333
+ </div>` : ""}`
4334
+ );
4335
+ }
4336
+ const operatorRows = sa.byOperator.map((o) => {
4337
+ const deltaText = o.deltaPct === null ? "\u2014" : `${o.deltaPct > 0 ? "+" : ""}${o.deltaPct}%`;
4338
+ const toneClass = o.deltaPct === null ? "" : `tone-${deltaTone(o.deltaPct)}`;
4339
+ return `
4340
+ <tr>
4341
+ <td>${escapeHtml(o.operator)}</td>
4342
+ <td class="numeric">${formatNumber(o.verifiedHits)}</td>
4343
+ <td class="numeric meta">${formatNumber(o.unverifiedHits)}</td>
4344
+ <td class="numeric">${formatNumber(o.referralArrivals)}</td>
4345
+ <td class="numeric ${toneClass}">${deltaText}</td>
4346
+ </tr>`;
4347
+ }).join("");
4348
+ const pathRows = sa.topCrawledPaths.map((p) => `
4349
+ <tr>
4350
+ <td class="page-cell">${formatLandingPageHtml(p.path)}</td>
4351
+ <td class="numeric">${formatNumber(p.verifiedHits)}</td>
4352
+ <td class="numeric">${p.distinctOperators}</td>
4353
+ </tr>`).join("");
4354
+ const referralProductRows = sa.referralProducts.map((p) => `
4355
+ <tr>
4356
+ <td>${escapeHtml(p.product)}</td>
4357
+ <td class="numeric">${formatNumber(p.arrivals)}</td>
4358
+ <td class="numeric">${p.distinctLandingPaths}</td>
4359
+ </tr>`).join("");
4360
+ const referralLandingRows = sa.topReferralLandingPaths.map((p) => `
4361
+ <tr>
4362
+ <td class="page-cell">${formatLandingPageHtml(p.path)}</td>
4363
+ <td class="numeric">${formatNumber(p.arrivals)}</td>
4364
+ <td class="numeric">${p.distinctProducts}</td>
4365
+ </tr>`).join("");
4366
+ const trendChart = sa.dailyTrend.length > 0 ? renderLineChart(
4367
+ sa.dailyTrend.map((d) => ({ x: d.date, y: d.verifiedCrawlerHits, label: d.date.slice(5) })),
4368
+ COLORS.series[1],
4369
+ "Verified crawler hits over time (last 14 days)"
4370
+ ) : "";
4371
+ return section(
4372
+ serverActivityHeading("agency", true),
4373
+ `<div class="metric-grid">
4374
+ <div class="metric">
4375
+ <div class="label">Verified crawler hits (7d)</div>
4376
+ <div class="value">${formatNumber(sa.verifiedCrawlerHits.current)}</div>
4377
+ <div class="subtitle">${formatDelta(sa.verifiedCrawlerHits, "hits")}</div>
4378
+ </div>
4379
+ <div class="metric">
4380
+ <div class="label">AI-referral arrivals (7d)</div>
4381
+ <div class="value">${formatNumber(sa.referralArrivals.current)}</div>
4382
+ <div class="subtitle">${formatDelta(sa.referralArrivals, "arrivals")}</div>
4383
+ </div>
4384
+ </div>
4385
+ ${trendChart}
4386
+ ${operatorRows ? `<div class="chart-card"><h3>Per AI operator</h3>
4387
+ <p class="meta">Verified means rDNS-confirmed. Unverified bots claim the user-agent but couldn't be verified \u2014 could be the real bot or an imitator.</p>
4388
+ <table class="report-table">
4389
+ <thead><tr><th>Operator</th><th class="numeric">Verified hits</th><th class="numeric">Unverified</th><th class="numeric">Referral arrivals</th><th class="numeric">7d delta</th></tr></thead>
4390
+ <tbody>${operatorRows}</tbody>
4391
+ </table>
4392
+ </div>` : ""}
4393
+ ${pathRows ? `<div class="chart-card"><h3>Top crawled paths</h3>
4394
+ <p class="meta">Pages AI bots fetched most often (verified only, last 7d).</p>
4395
+ <table class="report-table">
4396
+ <thead><tr><th>Path</th><th class="numeric">Verified hits</th><th class="numeric">Distinct operators</th></tr></thead>
4397
+ <tbody>${pathRows}</tbody>
4398
+ </table>
4399
+ </div>` : ""}
4400
+ ${referralProductRows ? `<div class="chart-card"><h3>Click-throughs by AI product</h3>
4401
+ <p class="meta">Where humans landed coming from each AI product (chatgpt.com, claude.ai, \u2026).</p>
4402
+ <table class="report-table">
4403
+ <thead><tr><th>Product</th><th class="numeric">Arrivals</th><th class="numeric">Distinct landing paths</th></tr></thead>
4404
+ <tbody>${referralProductRows}</tbody>
4405
+ </table>
4406
+ </div>` : ""}
4407
+ ${referralLandingRows ? `<div class="chart-card"><h3>Top AI-referral landing paths</h3>
4408
+ <table class="report-table">
4409
+ <thead><tr><th>Path</th><th class="numeric">Arrivals</th><th class="numeric">Distinct products</th></tr></thead>
4410
+ <tbody>${referralLandingRows}</tbody>
4411
+ </table>
4412
+ </div>` : ""}`
4413
+ );
4414
+ }
4271
4415
  function renderIndexingHealth(report) {
4272
4416
  const ih = report.indexingHealth;
4273
4417
  if (!ih) {
4274
4418
  return section(
4275
- { id: "indexing-health", eyebrow: "Section 10", title: "Indexing Health" },
4419
+ { id: "indexing-health", eyebrow: "Section 11", title: "Indexing Health" },
4276
4420
  renderEmpty("Connect Google Search Console or Bing Webmaster Tools and run a sitemap inspection.")
4277
4421
  );
4278
4422
  }
@@ -4294,7 +4438,7 @@ function renderIndexingHealth(report) {
4294
4438
  }).join("");
4295
4439
  const legend = segments.map((s) => `<span><span class="legend-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)}: ${s.count}</span>`).join("");
4296
4440
  return section(
4297
- { id: "indexing-health", eyebrow: "Section 10", title: "Indexing Health", intro: `Pages absent from ${ih.provider === "google" ? "Google" : "Bing"} are harder for AI engines to retrieve.` },
4441
+ { id: "indexing-health", eyebrow: "Section 11", title: "Indexing Health", intro: `Pages absent from ${ih.provider === "google" ? "Google" : "Bing"} are harder for AI engines to retrieve.` },
4298
4442
  `<div class="metric-grid">
4299
4443
  <div class="metric"><div class="label">Indexed</div><div class="value tone-positive">${formatNumber(ih.indexed)}</div></div>
4300
4444
  <div class="metric"><div class="label">Total inspected</div><div class="value">${formatNumber(ih.total)}</div></div>
@@ -4311,13 +4455,13 @@ function renderCitationsTrend(report) {
4311
4455
  const trend = report.citationsTrend;
4312
4456
  if (trend.length === 0) {
4313
4457
  return section(
4314
- { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time" },
4458
+ { id: "citations-trend", eyebrow: "Section 12", title: "Citations Over Time" },
4315
4459
  renderEmpty("Run multiple checks to see a trend.")
4316
4460
  );
4317
4461
  }
4318
4462
  if (isTrendBaseline(trend)) {
4319
4463
  return section(
4320
- { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time" },
4464
+ { id: "citations-trend", eyebrow: "Section 12", title: "Citations Over Time" },
4321
4465
  renderEmpty(`Building baseline (${trend.length} of ${MIN_TREND_POINTS} checks completed). Trend will appear once more checks are recorded.`)
4322
4466
  );
4323
4467
  }
@@ -4334,7 +4478,7 @@ function renderCitationsTrend(report) {
4334
4478
  <td>${t.providerRates.map((r) => `${escapeHtml(r.provider)}: ${r.citationRate}%`).join(" \xB7 ")}</td>
4335
4479
  </tr>`).join("");
4336
4480
  return section(
4337
- { id: "citations-trend", eyebrow: "Section 11", title: "Citations Over Time", intro: "Citation coverage across recent checks." },
4481
+ { id: "citations-trend", eyebrow: "Section 12", title: "Citations Over Time", intro: "Citation coverage across recent checks." },
4338
4482
  `${chart}
4339
4483
  <div class="chart-card"><h3>Check-by-check breakdown</h3>
4340
4484
  <table class="report-table">
@@ -4348,7 +4492,7 @@ function renderInsights(report) {
4348
4492
  const list = report.insights;
4349
4493
  if (list.length === 0) {
4350
4494
  return section(
4351
- { id: "insights", eyebrow: "Section 12", title: "Insights & Alerts" },
4495
+ { id: "insights", eyebrow: "Section 13", title: "Insights & Alerts" },
4352
4496
  renderEmpty("No insights yet \u2014 run a check to generate alerts.")
4353
4497
  );
4354
4498
  }
@@ -4365,7 +4509,7 @@ function renderInsights(report) {
4365
4509
  </tr>`;
4366
4510
  }).join("");
4367
4511
  return section(
4368
- { id: "insights", eyebrow: "Section 12", title: "Insights & Alerts", intro: "Regressions, gains, and recurring alerts ordered by severity." },
4512
+ { id: "insights", eyebrow: "Section 13", title: "Insights & Alerts", intro: "Regressions, gains, and recurring alerts ordered by severity." },
4369
4513
  `<table class="report-table insights-table">
4370
4514
  <thead><tr>
4371
4515
  <th class="col-severity">Severity</th>
@@ -4407,7 +4551,7 @@ function renderOpportunities(report) {
4407
4551
  return section(
4408
4552
  {
4409
4553
  id: "content-opportunities",
4410
- eyebrow: "Section 13",
4554
+ eyebrow: "Section 14",
4411
4555
  title: "Content Opportunities",
4412
4556
  intro: "Queries where content work has the clearest path to more AI citations. Opportunity score is 0\u2013100, higher = stronger."
4413
4557
  },
@@ -4433,7 +4577,7 @@ function renderContentGaps(report) {
4433
4577
  return section(
4434
4578
  {
4435
4579
  id: "content-gaps",
4436
- eyebrow: "Section 14",
4580
+ eyebrow: "Section 15",
4437
4581
  title: "Content Gaps",
4438
4582
  intro: "Tracked queries where competitors are cited and the client is missing."
4439
4583
  },
@@ -4447,7 +4591,7 @@ function renderRecommendedNextSteps(report) {
4447
4591
  const steps = report.recommendedNextSteps;
4448
4592
  if (steps.length === 0) {
4449
4593
  return section(
4450
- { id: "recommended-next-steps", eyebrow: "Section 15", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4594
+ { id: "recommended-next-steps", eyebrow: "Section 16", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4451
4595
  renderEmpty("No outstanding actions.")
4452
4596
  );
4453
4597
  }
@@ -4458,7 +4602,7 @@ function renderRecommendedNextSteps(report) {
4458
4602
  <span class="rationale">${escapeHtml(s.rationale)}</span>
4459
4603
  </div>`).join("");
4460
4604
  return section(
4461
- { id: "recommended-next-steps", eyebrow: "Section 15", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4605
+ { id: "recommended-next-steps", eyebrow: "Section 16", title: "Recommended Next Steps", intro: "Action items bucketed by timing." },
4462
4606
  `<div class="steps">${items}</div>`
4463
4607
  );
4464
4608
  }
@@ -4683,6 +4827,10 @@ function renderReportHtml(report, opts = {}) {
4683
4827
  const sections = audience === "client" ? [
4684
4828
  renderClientSummary(report),
4685
4829
  renderWhatsChanged(report, "client"),
4830
+ // Server-side AI visibility runs between WhatsChanged and the action
4831
+ // plan in BOTH the SPA and HTML so clients see the same ordered set
4832
+ // of sections in either surface (per the report-parity rule).
4833
+ renderServerActivity(report, "client"),
4686
4834
  renderAudienceActionPlan(report, "client"),
4687
4835
  renderClientEvidenceSummary(report)
4688
4836
  ].join("\n") : [
@@ -4697,6 +4845,7 @@ function renderReportHtml(report, opts = {}) {
4697
4845
  renderGa(report),
4698
4846
  renderSocial(report),
4699
4847
  renderAiReferrals(report),
4848
+ renderServerActivity(report, "agency"),
4700
4849
  renderIndexingHealth(report),
4701
4850
  renderCitationsTrend(report),
4702
4851
  renderInsights(report),
@@ -5082,6 +5231,9 @@ var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
5082
5231
  var TOP_CAMPAIGN_LIMIT = 10;
5083
5232
  var INSIGHT_LOOKBACK_RUNS = 5;
5084
5233
  var REPORT_WINDOW_DAYS = 30;
5234
+ var SERVER_ACTIVITY_HEADLINE_DAYS = 7;
5235
+ var SERVER_ACTIVITY_TREND_DAYS = 14;
5236
+ var SERVER_ACTIVITY_TOP_PATHS_LIMIT = 10;
5085
5237
  function windowStartDate(endDate, windowDays) {
5086
5238
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(endDate);
5087
5239
  if (!m) return endDate;
@@ -5350,6 +5502,209 @@ function buildAiReferrals(db, projectId) {
5350
5502
  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);
5351
5503
  return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
5352
5504
  }
5505
+ function buildServerActivity(db, projectId) {
5506
+ const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
5507
+ and5(
5508
+ eq13(trafficSources.projectId, projectId),
5509
+ ne(trafficSources.status, TrafficSourceStatuses.archived)
5510
+ )
5511
+ ).all();
5512
+ if (sourceRows.length === 0) return null;
5513
+ const now = /* @__PURE__ */ new Date();
5514
+ const headlineEnd = now.toISOString();
5515
+ const headlineStartMs = now.getTime() - SERVER_ACTIVITY_HEADLINE_DAYS * 24 * 60 * 6e4;
5516
+ const priorStartMs = headlineStartMs - SERVER_ACTIVITY_HEADLINE_DAYS * 24 * 60 * 6e4;
5517
+ const trendStartMs = now.getTime() - SERVER_ACTIVITY_TREND_DAYS * 24 * 60 * 6e4;
5518
+ const headlineStart = new Date(headlineStartMs).toISOString();
5519
+ const priorStart = new Date(priorStartMs).toISOString();
5520
+ const trendStart = new Date(trendStartMs).toISOString();
5521
+ const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5522
+ db.select({ total: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
5523
+ and5(
5524
+ eq13(crawlerEventsHourly.projectId, projectId),
5525
+ eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5526
+ gte(crawlerEventsHourly.tsHour, windowStartIso),
5527
+ exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
5528
+ )
5529
+ ).get()?.total ?? 0
5530
+ );
5531
+ const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
5532
+ db.select({ total: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
5533
+ and5(
5534
+ eq13(aiReferralEventsHourly.projectId, projectId),
5535
+ gte(aiReferralEventsHourly.tsHour, windowStartIso),
5536
+ exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
5537
+ )
5538
+ ).get()?.total ?? 0
5539
+ );
5540
+ const verifiedCurrent = sumVerifiedCrawlers(headlineStart, headlineEnd);
5541
+ const verifiedPrior = sumVerifiedCrawlers(priorStart, headlineStart, true);
5542
+ const referralCurrent = sumReferrals(headlineStart, headlineEnd);
5543
+ const referralPrior = sumReferrals(priorStart, headlineStart, true);
5544
+ const crawlerByOperatorRows = db.select({
5545
+ operator: crawlerEventsHourly.operator,
5546
+ verificationStatus: crawlerEventsHourly.verificationStatus,
5547
+ hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5548
+ }).from(crawlerEventsHourly).where(
5549
+ and5(
5550
+ eq13(crawlerEventsHourly.projectId, projectId),
5551
+ gte(crawlerEventsHourly.tsHour, headlineStart),
5552
+ lte(crawlerEventsHourly.tsHour, headlineEnd)
5553
+ )
5554
+ ).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
5555
+ const crawlerByOperatorPriorRows = db.select({
5556
+ operator: crawlerEventsHourly.operator,
5557
+ hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5558
+ }).from(crawlerEventsHourly).where(
5559
+ and5(
5560
+ eq13(crawlerEventsHourly.projectId, projectId),
5561
+ eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5562
+ gte(crawlerEventsHourly.tsHour, priorStart),
5563
+ lt(crawlerEventsHourly.tsHour, headlineStart)
5564
+ )
5565
+ ).groupBy(crawlerEventsHourly.operator).all();
5566
+ const referralByOperatorRows = db.select({
5567
+ operator: aiReferralEventsHourly.operator,
5568
+ hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5569
+ }).from(aiReferralEventsHourly).where(
5570
+ and5(
5571
+ eq13(aiReferralEventsHourly.projectId, projectId),
5572
+ gte(aiReferralEventsHourly.tsHour, headlineStart),
5573
+ lte(aiReferralEventsHourly.tsHour, headlineEnd)
5574
+ )
5575
+ ).groupBy(aiReferralEventsHourly.operator).all();
5576
+ const operatorAgg = /* @__PURE__ */ new Map();
5577
+ const ensureOp = (op) => {
5578
+ let entry = operatorAgg.get(op);
5579
+ if (!entry) {
5580
+ entry = { verified: 0, unverified: 0, referrals: 0, prior: 0 };
5581
+ operatorAgg.set(op, entry);
5582
+ }
5583
+ return entry;
5584
+ };
5585
+ for (const r of crawlerByOperatorRows) {
5586
+ const entry = ensureOp(r.operator);
5587
+ if (r.verificationStatus === VerificationStatuses.verified) entry.verified += Number(r.hits);
5588
+ else entry.unverified += Number(r.hits);
5589
+ }
5590
+ for (const r of crawlerByOperatorPriorRows) {
5591
+ ensureOp(r.operator).prior += Number(r.hits);
5592
+ }
5593
+ for (const r of referralByOperatorRows) {
5594
+ ensureOp(r.operator).referrals += Number(r.hits);
5595
+ }
5596
+ const byOperator = [...operatorAgg.entries()].map(([operator, v]) => ({
5597
+ operator,
5598
+ verifiedHits: v.verified,
5599
+ unverifiedHits: v.unverified,
5600
+ referralArrivals: v.referrals,
5601
+ deltaPct: deltaPercent(v.verified, v.prior)
5602
+ })).sort(
5603
+ (a, b) => b.verifiedHits - a.verifiedHits || b.referralArrivals - a.referralArrivals
5604
+ );
5605
+ const topPathsRows = db.select({
5606
+ path: crawlerEventsHourly.pathNormalized,
5607
+ hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
5608
+ operators: sql3`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
5609
+ }).from(crawlerEventsHourly).where(
5610
+ and5(
5611
+ eq13(crawlerEventsHourly.projectId, projectId),
5612
+ eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5613
+ gte(crawlerEventsHourly.tsHour, headlineStart),
5614
+ lte(crawlerEventsHourly.tsHour, headlineEnd)
5615
+ )
5616
+ ).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql3`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
5617
+ const topCrawledPaths = topPathsRows.map((r) => ({
5618
+ path: r.path,
5619
+ verifiedHits: Number(r.hits),
5620
+ distinctOperators: Number(r.operators)
5621
+ }));
5622
+ const referralProductsRows = db.select({
5623
+ product: aiReferralEventsHourly.product,
5624
+ arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5625
+ landingPaths: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
5626
+ }).from(aiReferralEventsHourly).where(
5627
+ and5(
5628
+ eq13(aiReferralEventsHourly.projectId, projectId),
5629
+ gte(aiReferralEventsHourly.tsHour, headlineStart),
5630
+ lte(aiReferralEventsHourly.tsHour, headlineEnd)
5631
+ )
5632
+ ).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
5633
+ const referralProducts = referralProductsRows.map((r) => ({
5634
+ product: r.product,
5635
+ arrivals: Number(r.arrivals),
5636
+ distinctLandingPaths: Number(r.landingPaths)
5637
+ }));
5638
+ const topReferralRows = db.select({
5639
+ path: aiReferralEventsHourly.landingPathNormalized,
5640
+ arrivals: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
5641
+ products: sql3`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
5642
+ }).from(aiReferralEventsHourly).where(
5643
+ and5(
5644
+ eq13(aiReferralEventsHourly.projectId, projectId),
5645
+ gte(aiReferralEventsHourly.tsHour, headlineStart),
5646
+ lte(aiReferralEventsHourly.tsHour, headlineEnd)
5647
+ )
5648
+ ).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql3`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
5649
+ const topReferralLandingPaths = topReferralRows.map((r) => ({
5650
+ path: r.path,
5651
+ arrivals: Number(r.arrivals),
5652
+ distinctProducts: Number(r.products)
5653
+ }));
5654
+ const crawlerTrendRows = db.select({
5655
+ date: sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
5656
+ hits: sql3`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
5657
+ }).from(crawlerEventsHourly).where(
5658
+ and5(
5659
+ eq13(crawlerEventsHourly.projectId, projectId),
5660
+ eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
5661
+ gte(crawlerEventsHourly.tsHour, trendStart),
5662
+ lte(crawlerEventsHourly.tsHour, headlineEnd)
5663
+ )
5664
+ ).groupBy(sql3`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
5665
+ const referralTrendRows = db.select({
5666
+ date: sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
5667
+ hits: sql3`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
5668
+ }).from(aiReferralEventsHourly).where(
5669
+ and5(
5670
+ eq13(aiReferralEventsHourly.projectId, projectId),
5671
+ gte(aiReferralEventsHourly.tsHour, trendStart),
5672
+ lte(aiReferralEventsHourly.tsHour, headlineEnd)
5673
+ )
5674
+ ).groupBy(sql3`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
5675
+ const dailyTrendMap = /* @__PURE__ */ new Map();
5676
+ for (const r of crawlerTrendRows) {
5677
+ const e = dailyTrendMap.get(r.date) ?? { verifiedCrawlerHits: 0, referralArrivals: 0 };
5678
+ e.verifiedCrawlerHits += Number(r.hits);
5679
+ dailyTrendMap.set(r.date, e);
5680
+ }
5681
+ for (const r of referralTrendRows) {
5682
+ const e = dailyTrendMap.get(r.date) ?? { verifiedCrawlerHits: 0, referralArrivals: 0 };
5683
+ e.referralArrivals += Number(r.hits);
5684
+ dailyTrendMap.set(r.date, e);
5685
+ }
5686
+ const dailyTrend = [...dailyTrendMap.entries()].map(([date, v]) => ({ date, ...v })).sort((a, b) => a.date.localeCompare(b.date));
5687
+ return {
5688
+ windowStart: headlineStart,
5689
+ windowEnd: headlineEnd,
5690
+ hasData: verifiedCurrent + referralCurrent + verifiedPrior + referralPrior > 0 || byOperator.length > 0 || topCrawledPaths.length > 0 || referralProducts.length > 0,
5691
+ verifiedCrawlerHits: {
5692
+ current: verifiedCurrent,
5693
+ prior: verifiedPrior,
5694
+ deltaPct: deltaPercent(verifiedCurrent, verifiedPrior)
5695
+ },
5696
+ referralArrivals: {
5697
+ current: referralCurrent,
5698
+ prior: referralPrior,
5699
+ deltaPct: deltaPercent(referralCurrent, referralPrior)
5700
+ },
5701
+ byOperator,
5702
+ topCrawledPaths,
5703
+ referralProducts,
5704
+ dailyTrend,
5705
+ topReferralLandingPaths
5706
+ };
5707
+ }
5353
5708
  function buildIndexingHealth(db, projectId) {
5354
5709
  const gsc = db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, projectId)).orderBy(desc6(gscCoverageSnapshots.date)).limit(1).get();
5355
5710
  if (gsc) {
@@ -6009,6 +6364,7 @@ function buildProjectReport(db, projectName) {
6009
6364
  const gaSection = buildGaSection(db, project.id);
6010
6365
  const socialSection = buildSocialReferrals(db, project.id);
6011
6366
  const aiReferralsSection = buildAiReferrals(db, project.id);
6367
+ const serverActivitySection = buildServerActivity(db, project.id);
6012
6368
  const indexingHealthSection = buildIndexingHealth(db, project.id);
6013
6369
  const citationsTrend = buildCitationsTrend(db, project.id, queryLookup, latestRunLocation);
6014
6370
  const insightList = buildInsightList(db, project.id, latestRunLocation);
@@ -6152,6 +6508,7 @@ function buildProjectReport(db, projectName) {
6152
6508
  ga: gaSection,
6153
6509
  socialReferrals: socialSection,
6154
6510
  aiReferrals: aiReferralsSection,
6511
+ serverActivity: serverActivitySection,
6155
6512
  indexingHealth: indexingHealthSection,
6156
6513
  citationsTrend,
6157
6514
  whatsChanged,
@@ -6355,7 +6712,7 @@ function normalizeDomain2(domain) {
6355
6712
  }
6356
6713
 
6357
6714
  // ../api-routes/src/composites.ts
6358
- import { eq as eq15, and as and6, desc as desc7, sql as sql3, like, or as or3, inArray as inArray6 } from "drizzle-orm";
6715
+ import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray6 } from "drizzle-orm";
6359
6716
  var TOP_INSIGHT_LIMIT = 5;
6360
6717
  var SEARCH_HIT_HARD_LIMIT = 50;
6361
6718
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -6461,9 +6818,9 @@ async function compositeRoutes(app) {
6461
6818
  and6(
6462
6819
  eq15(queries.projectId, project.id),
6463
6820
  or3(
6464
- sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
6465
- sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
6466
- sql3`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
6821
+ sql4`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
6822
+ sql4`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
6823
+ sql4`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
6467
6824
  like(queries.query, pattern)
6468
6825
  )
6469
6826
  )
@@ -6474,8 +6831,8 @@ async function compositeRoutes(app) {
6474
6831
  or3(
6475
6832
  like(insights.title, pattern),
6476
6833
  like(insights.query, pattern),
6477
- sql3`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
6478
- sql3`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
6834
+ sql4`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
6835
+ sql4`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
6479
6836
  )
6480
6837
  )
6481
6838
  ).orderBy(desc7(insights.createdAt)).limit(limit + 1).all();
@@ -10373,7 +10730,7 @@ function formatNotification(row) {
10373
10730
 
10374
10731
  // ../api-routes/src/google.ts
10375
10732
  import crypto14 from "crypto";
10376
- import { eq as eq18, and as and8, desc as desc8, sql as sql4 } from "drizzle-orm";
10733
+ import { eq as eq18, and as and8, desc as desc8, sql as sql5 } from "drizzle-orm";
10377
10734
 
10378
10735
  // ../integration-google/src/constants.ts
10379
10736
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -11583,11 +11940,11 @@ async function googleRoutes(app, opts) {
11583
11940
  const { startDate, endDate, query, page, limit } = request.query;
11584
11941
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
11585
11942
  const conditions = [eq18(gscSearchData.projectId, project.id)];
11586
- if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
11587
- else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
11588
- if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
11589
- if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
11590
- if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
11943
+ if (startDate) conditions.push(sql5`${gscSearchData.date} >= ${startDate}`);
11944
+ else if (cutoffDate) conditions.push(sql5`${gscSearchData.date} >= ${cutoffDate}`);
11945
+ if (endDate) conditions.push(sql5`${gscSearchData.date} <= ${endDate}`);
11946
+ if (query) conditions.push(sql5`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
11947
+ if (page) conditions.push(sql5`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
11591
11948
  const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
11592
11949
  return rows.map((r) => ({
11593
11950
  date: r.date,
@@ -12830,7 +13187,7 @@ async function cdpRoutes(app, opts) {
12830
13187
 
12831
13188
  // ../api-routes/src/ga.ts
12832
13189
  import crypto16 from "crypto";
12833
- import { eq as eq21, desc as desc10, and as and11, sql as sql5 } from "drizzle-orm";
13190
+ import { eq as eq21, desc as desc10, and as and11, sql as sql6 } from "drizzle-orm";
12834
13191
  function gaLog(level, action, ctx) {
12835
13192
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
12836
13193
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -13127,8 +13484,8 @@ async function ga4Routes(app, opts) {
13127
13484
  tx.delete(gaTrafficSnapshots).where(
13128
13485
  and11(
13129
13486
  eq21(gaTrafficSnapshots.projectId, project.id),
13130
- sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
13131
- sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
13487
+ sql6`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
13488
+ sql6`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
13132
13489
  )
13133
13490
  ).run();
13134
13491
  for (const row of rows) {
@@ -13151,8 +13508,8 @@ async function ga4Routes(app, opts) {
13151
13508
  tx.delete(gaAiReferrals).where(
13152
13509
  and11(
13153
13510
  eq21(gaAiReferrals.projectId, project.id),
13154
- sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
13155
- sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
13511
+ sql6`${gaAiReferrals.date} >= ${summary.periodStart}`,
13512
+ sql6`${gaAiReferrals.date} <= ${summary.periodEnd}`
13156
13513
  )
13157
13514
  ).run();
13158
13515
  for (const row of aiReferrals) {
@@ -13177,8 +13534,8 @@ async function ga4Routes(app, opts) {
13177
13534
  tx.delete(gaSocialReferrals).where(
13178
13535
  and11(
13179
13536
  eq21(gaSocialReferrals.projectId, project.id),
13180
- sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
13181
- sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
13537
+ sql6`${gaSocialReferrals.date} >= ${summary.periodStart}`,
13538
+ sql6`${gaSocialReferrals.date} <= ${summary.periodEnd}`
13182
13539
  )
13183
13540
  ).run();
13184
13541
  for (const row of socialReferrals) {
@@ -13268,11 +13625,11 @@ async function ga4Routes(app, opts) {
13268
13625
  const cutoff = windowCutoff(window);
13269
13626
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
13270
13627
  const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
13271
- if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
13628
+ if (cutoffDate) snapshotConditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
13272
13629
  const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
13273
- if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
13630
+ if (cutoffDate) aiConditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
13274
13631
  const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
13275
- if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
13632
+ if (cutoffDate) socialConditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
13276
13633
  const windowSummaryRow = cutoffDate ? app.db.select({
13277
13634
  totalSessions: gaTrafficWindowSummaries.totalSessions,
13278
13635
  totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
@@ -13285,9 +13642,9 @@ async function ga4Routes(app, opts) {
13285
13642
  )
13286
13643
  ).get() : null;
13287
13644
  const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
13288
- totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
13289
- totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
13290
- totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
13645
+ totalSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
13646
+ totalOrganicSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
13647
+ totalUsers: sql6`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
13291
13648
  }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get() : null;
13292
13649
  const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
13293
13650
  totalSessions: gaTrafficSummaries.totalSessions,
@@ -13295,38 +13652,38 @@ async function ga4Routes(app, opts) {
13295
13652
  totalUsers: gaTrafficSummaries.totalUsers
13296
13653
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
13297
13654
  const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
13298
- totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
13655
+ totalDirectSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
13299
13656
  }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get();
13300
13657
  const summaryMeta = app.db.select({
13301
13658
  periodStart: gaTrafficSummaries.periodStart,
13302
13659
  periodEnd: gaTrafficSummaries.periodEnd
13303
13660
  }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
13304
13661
  const rows = app.db.select({
13305
- landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
13306
- sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
13307
- organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
13308
- directSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
13309
- users: sql5`SUM(${gaTrafficSnapshots.users})`
13310
- }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
13662
+ landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
13663
+ sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
13664
+ organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
13665
+ directSessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
13666
+ users: sql6`SUM(${gaTrafficSnapshots.users})`
13667
+ }).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
13311
13668
  const aiReferralRows = app.db.select({
13312
13669
  source: gaAiReferrals.source,
13313
13670
  medium: gaAiReferrals.medium,
13314
13671
  sourceDimension: gaAiReferrals.sourceDimension,
13315
- sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13316
- users: sql5`SUM(${gaAiReferrals.users})`
13672
+ sessions: sql6`SUM(${gaAiReferrals.sessions})`,
13673
+ users: sql6`SUM(${gaAiReferrals.users})`
13317
13674
  }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
13318
13675
  const aiReferralLandingPageRows = app.db.select({
13319
13676
  source: gaAiReferrals.source,
13320
13677
  medium: gaAiReferrals.medium,
13321
13678
  sourceDimension: gaAiReferrals.sourceDimension,
13322
- landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
13323
- sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13324
- users: sql5`SUM(${gaAiReferrals.users})`
13679
+ landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
13680
+ sessions: sql6`SUM(${gaAiReferrals.sessions})`,
13681
+ users: sql6`SUM(${gaAiReferrals.users})`
13325
13682
  }).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(
13326
13683
  gaAiReferrals.source,
13327
13684
  gaAiReferrals.medium,
13328
13685
  gaAiReferrals.sourceDimension,
13329
- sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
13686
+ sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
13330
13687
  ).all();
13331
13688
  const aiReferrals = pickWinningDimension(
13332
13689
  aiReferralRows,
@@ -13337,10 +13694,10 @@ async function ga4Routes(app, opts) {
13337
13694
  (r) => `${r.source}\0${r.medium}\0${r.landingPage}`
13338
13695
  );
13339
13696
  const aiDeduped = app.db.select({
13340
- sessions: sql5`COALESCE(SUM(max_sessions), 0)`,
13341
- users: sql5`COALESCE(SUM(max_users), 0)`
13697
+ sessions: sql6`COALESCE(SUM(max_sessions), 0)`,
13698
+ users: sql6`COALESCE(SUM(max_users), 0)`
13342
13699
  }).from(
13343
- sql5`(
13700
+ sql6`(
13344
13701
  SELECT date, source, medium,
13345
13702
  MAX(dimension_sessions) AS max_sessions,
13346
13703
  MAX(dimension_users) AS max_users
@@ -13349,7 +13706,7 @@ async function ga4Routes(app, opts) {
13349
13706
  SUM(sessions) AS dimension_sessions,
13350
13707
  SUM(users) AS dimension_users
13351
13708
  FROM ga_ai_referrals
13352
- WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
13709
+ WHERE project_id = ${project.id}${cutoffDate ? sql6` AND date >= ${cutoffDate}` : sql6``}
13353
13710
  GROUP BY date, source, medium, source_dimension
13354
13711
  )
13355
13712
  GROUP BY date, source, medium
@@ -13357,8 +13714,8 @@ async function ga4Routes(app, opts) {
13357
13714
  ).get();
13358
13715
  const aiBySessionRows = app.db.select({
13359
13716
  channelGroup: gaAiReferrals.channelGroup,
13360
- sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
13361
- users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
13717
+ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
13718
+ users: sql6`COALESCE(SUM(${gaAiReferrals.users}), 0)`
13362
13719
  }).from(gaAiReferrals).where(and11(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
13363
13720
  const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
13364
13721
  let aiBySessionUsers = 0;
@@ -13371,12 +13728,12 @@ async function ga4Routes(app, opts) {
13371
13728
  source: gaSocialReferrals.source,
13372
13729
  medium: gaSocialReferrals.medium,
13373
13730
  channelGroup: gaSocialReferrals.channelGroup,
13374
- sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
13375
- users: sql5`SUM(${gaSocialReferrals.users})`
13376
- }).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
13731
+ sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
13732
+ users: sql6`SUM(${gaSocialReferrals.users})`
13733
+ }).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql6`SUM(${gaSocialReferrals.sessions}) DESC`).all();
13377
13734
  const socialTotals = app.db.select({
13378
- sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
13379
- users: sql5`SUM(${gaSocialReferrals.users})`
13735
+ sessions: sql6`SUM(${gaSocialReferrals.sessions})`,
13736
+ users: sql6`SUM(${gaSocialReferrals.users})`
13380
13737
  }).from(gaSocialReferrals).where(and11(...socialConditions)).get();
13381
13738
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
13382
13739
  const total = summaryRow?.totalSessions ?? 0;
@@ -13459,21 +13816,21 @@ async function ga4Routes(app, opts) {
13459
13816
  requireGa4Connection(opts, project.name, project.canonicalDomain);
13460
13817
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
13461
13818
  const conditions = [eq21(gaAiReferrals.projectId, project.id)];
13462
- if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
13819
+ if (cutoffDate) conditions.push(sql6`${gaAiReferrals.date} >= ${cutoffDate}`);
13463
13820
  const rows = app.db.select({
13464
13821
  date: gaAiReferrals.date,
13465
13822
  source: gaAiReferrals.source,
13466
13823
  medium: gaAiReferrals.medium,
13467
- landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
13824
+ landingPage: sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
13468
13825
  sourceDimension: gaAiReferrals.sourceDimension,
13469
- sessions: sql5`SUM(${gaAiReferrals.sessions})`,
13470
- users: sql5`SUM(${gaAiReferrals.users})`
13826
+ sessions: sql6`SUM(${gaAiReferrals.sessions})`,
13827
+ users: sql6`SUM(${gaAiReferrals.users})`
13471
13828
  }).from(gaAiReferrals).where(and11(...conditions)).groupBy(
13472
13829
  gaAiReferrals.date,
13473
13830
  gaAiReferrals.source,
13474
13831
  gaAiReferrals.medium,
13475
13832
  gaAiReferrals.sourceDimension,
13476
- sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
13833
+ sql6`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
13477
13834
  ).orderBy(gaAiReferrals.date).all();
13478
13835
  return rows;
13479
13836
  });
@@ -13482,7 +13839,7 @@ async function ga4Routes(app, opts) {
13482
13839
  requireGa4Connection(opts, project.name, project.canonicalDomain);
13483
13840
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
13484
13841
  const conditions = [eq21(gaSocialReferrals.projectId, project.id)];
13485
- if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
13842
+ if (cutoffDate) conditions.push(sql6`${gaSocialReferrals.date} >= ${cutoffDate}`);
13486
13843
  const rows = app.db.select({
13487
13844
  date: gaSocialReferrals.date,
13488
13845
  source: gaSocialReferrals.source,
@@ -13503,10 +13860,10 @@ async function ga4Routes(app, opts) {
13503
13860
  d.setDate(d.getDate() - n);
13504
13861
  return fmt(d);
13505
13862
  };
13506
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
13863
+ const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
13507
13864
  eq21(gaSocialReferrals.projectId, project.id),
13508
- sql5`${gaSocialReferrals.date} >= ${from}`,
13509
- sql5`${gaSocialReferrals.date} < ${to}`
13865
+ sql6`${gaSocialReferrals.date} >= ${from}`,
13866
+ sql6`${gaSocialReferrals.date} < ${to}`
13510
13867
  )).get();
13511
13868
  const current7d = sumSocial(daysAgo2(7), fmt(today));
13512
13869
  const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
@@ -13515,19 +13872,19 @@ async function ga4Routes(app, opts) {
13515
13872
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
13516
13873
  const sourceCurrent = app.db.select({
13517
13874
  source: gaSocialReferrals.source,
13518
- sessions: sql5`SUM(${gaSocialReferrals.sessions})`
13875
+ sessions: sql6`SUM(${gaSocialReferrals.sessions})`
13519
13876
  }).from(gaSocialReferrals).where(and11(
13520
13877
  eq21(gaSocialReferrals.projectId, project.id),
13521
- sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
13522
- sql5`${gaSocialReferrals.date} < ${fmt(today)}`
13878
+ sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
13879
+ sql6`${gaSocialReferrals.date} < ${fmt(today)}`
13523
13880
  )).groupBy(gaSocialReferrals.source).all();
13524
13881
  const sourcePrev = app.db.select({
13525
13882
  source: gaSocialReferrals.source,
13526
- sessions: sql5`SUM(${gaSocialReferrals.sessions})`
13883
+ sessions: sql6`SUM(${gaSocialReferrals.sessions})`
13527
13884
  }).from(gaSocialReferrals).where(and11(
13528
13885
  eq21(gaSocialReferrals.projectId, project.id),
13529
- sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
13530
- sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
13886
+ sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
13887
+ sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`
13531
13888
  )).groupBy(gaSocialReferrals.source).all();
13532
13889
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
13533
13890
  let biggestMover = null;
@@ -13566,16 +13923,16 @@ async function ga4Routes(app, opts) {
13566
13923
  return fmt(d);
13567
13924
  };
13568
13925
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
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(
13926
+ const sumTotal = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
13927
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
13928
+ const sumDirect = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql6`${gaTrafficSnapshots.date} >= ${from}`, sql6`${gaTrafficSnapshots.date} < ${to}`)).get();
13929
+ const sumAi = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13573
13930
  eq21(gaAiReferrals.projectId, project.id),
13574
- sql5`${gaAiReferrals.date} >= ${from}`,
13575
- sql5`${gaAiReferrals.date} < ${to}`,
13931
+ sql6`${gaAiReferrals.date} >= ${from}`,
13932
+ sql6`${gaAiReferrals.date} < ${to}`,
13576
13933
  eq21(gaAiReferrals.sourceDimension, "session")
13577
13934
  )).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();
13935
+ const sumSocial = (from, to) => app.db.select({ sessions: sql6`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${from}`, sql6`${gaSocialReferrals.date} < ${to}`)).get();
13579
13936
  const todayStr = fmt(today);
13580
13937
  const buildTrend = (sum) => {
13581
13938
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -13584,16 +13941,16 @@ async function ga4Routes(app, opts) {
13584
13941
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
13585
13942
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
13586
13943
  };
13587
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13944
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13588
13945
  eq21(gaAiReferrals.projectId, project.id),
13589
- sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
13590
- sql5`${gaAiReferrals.date} < ${todayStr}`,
13946
+ sql6`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
13947
+ sql6`${gaAiReferrals.date} < ${todayStr}`,
13591
13948
  eq21(gaAiReferrals.sourceDimension, "session")
13592
13949
  )).groupBy(gaAiReferrals.source).all();
13593
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13950
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql6`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
13594
13951
  eq21(gaAiReferrals.projectId, project.id),
13595
- sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
13596
- sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
13952
+ sql6`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
13953
+ sql6`${gaAiReferrals.date} < ${daysAgo2(7)}`,
13597
13954
  eq21(gaAiReferrals.sourceDimension, "session")
13598
13955
  )).groupBy(gaAiReferrals.source).all();
13599
13956
  const findBiggestMover = (current, prev) => {
@@ -13610,8 +13967,8 @@ async function ga4Routes(app, opts) {
13610
13967
  }
13611
13968
  return mover;
13612
13969
  };
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();
13970
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql6`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
13971
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql6`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql6`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql6`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
13615
13972
  return {
13616
13973
  total: buildTrend(sumTotal),
13617
13974
  organic: buildTrend(sumOrganic),
@@ -13627,12 +13984,12 @@ async function ga4Routes(app, opts) {
13627
13984
  requireGa4Connection(opts, project.name, project.canonicalDomain);
13628
13985
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
13629
13986
  const conditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
13630
- if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
13987
+ if (cutoffDate) conditions.push(sql6`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
13631
13988
  const rows = app.db.select({
13632
13989
  date: gaTrafficSnapshots.date,
13633
- sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
13634
- organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
13635
- users: sql5`SUM(${gaTrafficSnapshots.users})`
13990
+ sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
13991
+ organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
13992
+ users: sql6`SUM(${gaTrafficSnapshots.users})`
13636
13993
  }).from(gaTrafficSnapshots).where(and11(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
13637
13994
  return rows.map((r) => ({
13638
13995
  date: r.date,
@@ -13645,11 +14002,11 @@ async function ga4Routes(app, opts) {
13645
14002
  const project = resolveProject(app.db, request.params.name);
13646
14003
  requireGa4Connection(opts, project.name, project.canonicalDomain);
13647
14004
  const trafficPages = app.db.select({
13648
- landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
13649
- sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
13650
- organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
13651
- users: sql5`SUM(${gaTrafficSnapshots.users})`
13652
- }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
14005
+ landingPage: sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
14006
+ sessions: sql6`SUM(${gaTrafficSnapshots.sessions})`,
14007
+ organicSessions: sql6`SUM(${gaTrafficSnapshots.organicSessions})`,
14008
+ users: sql6`SUM(${gaTrafficSnapshots.users})`
14009
+ }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql6`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql6`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
13653
14010
  return {
13654
14011
  pages: trafficPages.map((r) => ({
13655
14012
  landingPage: r.landingPage,
@@ -15286,7 +15643,7 @@ async function wordpressRoutes(app, opts) {
15286
15643
 
15287
15644
  // ../api-routes/src/backlinks.ts
15288
15645
  import crypto18 from "crypto";
15289
- import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
15646
+ import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql7 } from "drizzle-orm";
15290
15647
 
15291
15648
  // ../integration-commoncrawl/src/constants.ts
15292
15649
  import os3 from "os";
@@ -15570,7 +15927,7 @@ async function queryBacklinks(opts) {
15570
15927
  const reversed = opts.targets.map(reverseDomain);
15571
15928
  const targetList = reversed.map(quote).join(", ");
15572
15929
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
15573
- const sql12 = `
15930
+ const sql14 = `
15574
15931
  WITH vertices AS (
15575
15932
  SELECT * FROM read_csv(
15576
15933
  ${quote(opts.vertexPath)},
@@ -15606,7 +15963,7 @@ async function queryBacklinks(opts) {
15606
15963
  const conn = await instance.connect();
15607
15964
  let rows;
15608
15965
  try {
15609
- const reader = await conn.runAndReadAll(sql12);
15966
+ const reader = await conn.runAndReadAll(sql14);
15610
15967
  rows = reader.getRowObjects();
15611
15968
  } finally {
15612
15969
  conn.disconnectSync?.();
@@ -15683,7 +16040,7 @@ function pruneCachedRelease(release, opts = {}) {
15683
16040
  }
15684
16041
 
15685
16042
  // ../api-routes/src/backlinks-filter.ts
15686
- import { and as and12, ne, notLike } from "drizzle-orm";
16043
+ import { and as and12, ne as ne2, notLike } from "drizzle-orm";
15687
16044
  var BACKLINK_FILTER_PATTERNS = [
15688
16045
  "*.google.com",
15689
16046
  "*.googleusercontent.com",
@@ -15700,10 +16057,10 @@ function backlinkCrawlerExclusionClause() {
15700
16057
  for (const pattern of BACKLINK_FILTER_PATTERNS) {
15701
16058
  if (pattern.startsWith("*.")) {
15702
16059
  const suffix = pattern.slice(2);
15703
- conditions.push(ne(backlinkDomains.linkingDomain, suffix));
16060
+ conditions.push(ne2(backlinkDomains.linkingDomain, suffix));
15704
16061
  conditions.push(notLike(backlinkDomains.linkingDomain, `%.${suffix}`));
15705
16062
  } else {
15706
- conditions.push(ne(backlinkDomains.linkingDomain, pattern));
16063
+ conditions.push(ne2(backlinkDomains.linkingDomain, pattern));
15707
16064
  }
15708
16065
  }
15709
16066
  const combined = and12(...conditions);
@@ -15782,12 +16139,12 @@ function computeFilteredSummary(db, base) {
15782
16139
  );
15783
16140
  const filteredCondition = and13(baseDomainCondition, backlinkCrawlerExclusionClause());
15784
16141
  const unfilteredAgg = db.select({
15785
- count: sql6`count(*)`,
15786
- total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16142
+ count: sql7`count(*)`,
16143
+ total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
15787
16144
  }).from(backlinkDomains).where(baseDomainCondition).get();
15788
16145
  const filteredAgg = db.select({
15789
- count: sql6`count(*)`,
15790
- total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
16146
+ count: sql7`count(*)`,
16147
+ total: sql7`coalesce(sum(${backlinkDomains.numHosts}), 0)`
15791
16148
  }).from(backlinkDomains).where(filteredCondition).get();
15792
16149
  const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(10).all();
15793
16150
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
@@ -15961,7 +16318,7 @@ async function backlinksRoutes(app, opts) {
15961
16318
  eq22(backlinkDomains.release, targetRelease)
15962
16319
  );
15963
16320
  const domainCondition = excludeCrawlers ? and13(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
15964
- const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
16321
+ const totalRow = app.db.select({ count: sql7`count(*)` }).from(backlinkDomains).where(domainCondition).get();
15965
16322
  const rows = app.db.select({
15966
16323
  linkingDomain: backlinkDomains.linkingDomain,
15967
16324
  numHosts: backlinkDomains.numHosts
@@ -15996,7 +16353,7 @@ async function backlinksRoutes(app, opts) {
15996
16353
 
15997
16354
  // ../api-routes/src/traffic.ts
15998
16355
  import crypto20 from "crypto";
15999
- import { and as and14, desc as desc12, eq as eq23, gte, lte, sql as sql7 } from "drizzle-orm";
16356
+ import { and as and14, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql8 } from "drizzle-orm";
16000
16357
 
16001
16358
  // ../integration-cloud-run/src/auth.ts
16002
16359
  import crypto19 from "crypto";
@@ -16761,6 +17118,7 @@ async function trafficRoutes(app, opts) {
16761
17118
  Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
16762
17119
  );
16763
17120
  const startedAt = windowEnd.toISOString();
17121
+ const syncStartedAtMs = windowEnd.getTime();
16764
17122
  const runId = crypto20.randomUUID();
16765
17123
  app.db.insert(runs).values({
16766
17124
  id: runId,
@@ -16772,19 +17130,32 @@ async function trafficRoutes(app, opts) {
16772
17130
  startedAt,
16773
17131
  createdAt: startedAt
16774
17132
  }).run();
16775
- const markFailed = (msg) => {
17133
+ const markFailed = (msg, errorCode) => {
16776
17134
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
16777
17135
  app.db.transaction((tx) => {
16778
17136
  tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq23(runs.id, runId)).run();
16779
17137
  tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq23(trafficSources.id, sourceRow.id)).run();
16780
17138
  });
17139
+ try {
17140
+ opts.onTrafficSynced?.({
17141
+ status: "failed",
17142
+ sourceType: sourceRow.sourceType,
17143
+ sourceId: sourceRow.id,
17144
+ pulledEvents: 0,
17145
+ crawlerHits: 0,
17146
+ aiReferralHits: 0,
17147
+ durationMs: Date.now() - syncStartedAtMs,
17148
+ errorCode
17149
+ });
17150
+ } catch {
17151
+ }
16781
17152
  };
16782
17153
  let accessToken;
16783
17154
  try {
16784
17155
  accessToken = await resolveAccessToken2(credential);
16785
17156
  } catch (e) {
16786
17157
  const msg = e instanceof Error ? e.message : String(e);
16787
- markFailed(msg);
17158
+ markFailed(msg, "PROVIDER_AUTH");
16788
17159
  throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
16789
17160
  }
16790
17161
  let allEvents = [];
@@ -16801,7 +17172,7 @@ async function trafficRoutes(app, opts) {
16801
17172
  allEvents = page.events;
16802
17173
  } catch (e) {
16803
17174
  const msg = e instanceof Error ? e.message : String(e);
16804
- markFailed(msg);
17175
+ markFailed(msg, "PROVIDER_PULL");
16805
17176
  throw providerError(`Cloud Run pull failed: ${msg}`);
16806
17177
  }
16807
17178
  const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
@@ -16849,7 +17220,7 @@ async function trafficRoutes(app, opts) {
16849
17220
  crawlerEventsHourly.status
16850
17221
  ],
16851
17222
  set: {
16852
- hits: sql7`${crawlerEventsHourly.hits} + ${bucket.hits}`,
17223
+ hits: sql8`${crawlerEventsHourly.hits} + ${bucket.hits}`,
16853
17224
  sampledUserAgent: bucket.sampledUserAgent,
16854
17225
  updatedAt: finishedAt
16855
17226
  }
@@ -16884,7 +17255,7 @@ async function trafficRoutes(app, opts) {
16884
17255
  aiReferralEventsHourly.status
16885
17256
  ],
16886
17257
  set: {
16887
- sessionsOrHits: sql7`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
17258
+ sessionsOrHits: sql8`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
16888
17259
  updatedAt: finishedAt
16889
17260
  }
16890
17261
  }).run();
@@ -16935,6 +17306,18 @@ async function trafficRoutes(app, opts) {
16935
17306
  entityType: "traffic_source",
16936
17307
  entityId: sourceRow.id
16937
17308
  });
17309
+ try {
17310
+ opts.onTrafficSynced?.({
17311
+ status: "completed",
17312
+ sourceType: sourceRow.sourceType,
17313
+ sourceId: sourceRow.id,
17314
+ pulledEvents: report.totals.normalizedEvents,
17315
+ crawlerHits: report.totals.crawlerHits,
17316
+ aiReferralHits: report.totals.aiReferralHits,
17317
+ durationMs: Date.now() - syncStartedAtMs
17318
+ });
17319
+ } catch {
17320
+ }
16938
17321
  const response = {
16939
17322
  sourceId: sourceRow.id,
16940
17323
  runId,
@@ -16952,22 +17335,22 @@ async function trafficRoutes(app, opts) {
16952
17335
  return response;
16953
17336
  });
16954
17337
  function buildSourceDetail(projectId, row, since) {
16955
- const crawlerTotals = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
17338
+ const crawlerTotals = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
16956
17339
  and14(
16957
17340
  eq23(crawlerEventsHourly.sourceId, row.id),
16958
- gte(crawlerEventsHourly.tsHour, since)
17341
+ gte2(crawlerEventsHourly.tsHour, since)
16959
17342
  )
16960
17343
  ).get();
16961
- const aiTotals = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
17344
+ const aiTotals = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
16962
17345
  and14(
16963
17346
  eq23(aiReferralEventsHourly.sourceId, row.id),
16964
- gte(aiReferralEventsHourly.tsHour, since)
17347
+ gte2(aiReferralEventsHourly.tsHour, since)
16965
17348
  )
16966
17349
  ).get();
16967
- const sampleTotals = app.db.select({ total: sql7`COUNT(*)` }).from(rawEventSamples).where(
17350
+ const sampleTotals = app.db.select({ total: sql8`COUNT(*)` }).from(rawEventSamples).where(
16968
17351
  and14(
16969
17352
  eq23(rawEventSamples.sourceId, row.id),
16970
- gte(rawEventSamples.ts, since)
17353
+ gte2(rawEventSamples.ts, since)
16971
17354
  )
16972
17355
  ).get();
16973
17356
  const latestRun = app.db.select().from(runs).where(
@@ -17061,12 +17444,12 @@ async function trafficRoutes(app, opts) {
17061
17444
  if (kind === "all" || kind === TrafficEventKinds.crawler) {
17062
17445
  const crawlerFilters = [
17063
17446
  eq23(crawlerEventsHourly.projectId, project.id),
17064
- gte(crawlerEventsHourly.tsHour, sinceIso),
17065
- lte(crawlerEventsHourly.tsHour, untilIso)
17447
+ gte2(crawlerEventsHourly.tsHour, sinceIso),
17448
+ lte2(crawlerEventsHourly.tsHour, untilIso)
17066
17449
  ];
17067
17450
  if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
17068
17451
  const crawlerWhere = and14(...crawlerFilters);
17069
- const total = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
17452
+ const total = app.db.select({ total: sql8`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
17070
17453
  crawlerTotal = Number(total?.total ?? 0);
17071
17454
  const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
17072
17455
  for (const r of rows) {
@@ -17086,12 +17469,12 @@ async function trafficRoutes(app, opts) {
17086
17469
  if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
17087
17470
  const aiFilters = [
17088
17471
  eq23(aiReferralEventsHourly.projectId, project.id),
17089
- gte(aiReferralEventsHourly.tsHour, sinceIso),
17090
- lte(aiReferralEventsHourly.tsHour, untilIso)
17472
+ gte2(aiReferralEventsHourly.tsHour, sinceIso),
17473
+ lte2(aiReferralEventsHourly.tsHour, untilIso)
17091
17474
  ];
17092
17475
  if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
17093
17476
  const aiWhere = and14(...aiFilters);
17094
- const total = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
17477
+ const total = app.db.select({ total: sql8`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
17095
17478
  aiReferralTotal = Number(total?.total ?? 0);
17096
17479
  const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
17097
17480
  for (const r of rows) {
@@ -17744,12 +18127,308 @@ var providersConfiguredCheck = {
17744
18127
  };
17745
18128
  var PROVIDERS_CHECKS = [providersConfiguredCheck];
17746
18129
 
18130
+ // ../api-routes/src/doctor/checks/traffic-source.ts
18131
+ import { and as and15, eq as eq24, gte as gte3, ne as ne3, sql as sql9 } from "drizzle-orm";
18132
+ var RECENT_DATA_WARN_DAYS = 7;
18133
+ var RECENT_DATA_FAIL_DAYS = 30;
18134
+ function skippedNoProject2() {
18135
+ return {
18136
+ status: CheckStatuses.skipped,
18137
+ code: "traffic.no-project",
18138
+ summary: "Project context required for traffic source checks.",
18139
+ remediation: "Run `canonry doctor --project <name>` to scope this check to a project."
18140
+ };
18141
+ }
18142
+ function loadProbes(ctx) {
18143
+ if (!ctx.project) return [];
18144
+ const rows = ctx.db.select().from(trafficSources).where(
18145
+ and15(
18146
+ eq24(trafficSources.projectId, ctx.project.id),
18147
+ ne3(trafficSources.status, TrafficSourceStatuses.archived)
18148
+ )
18149
+ ).all();
18150
+ return rows.map((r) => ({
18151
+ id: r.id,
18152
+ projectId: r.projectId,
18153
+ projectName: ctx.project.name,
18154
+ sourceType: r.sourceType,
18155
+ displayName: r.displayName,
18156
+ status: r.status,
18157
+ lastSyncedAt: r.lastSyncedAt,
18158
+ lastError: r.lastError,
18159
+ configJson: r.configJson
18160
+ }));
18161
+ }
18162
+ var sourceConnectedCheck = {
18163
+ id: "traffic.source.connected",
18164
+ category: CheckCategories.integrations,
18165
+ scope: CheckScopes.project,
18166
+ title: "Traffic source connected",
18167
+ run: (ctx) => {
18168
+ if (!ctx.project) return skippedNoProject2();
18169
+ const sources = loadProbes(ctx);
18170
+ if (sources.length === 0) {
18171
+ return {
18172
+ status: CheckStatuses.skipped,
18173
+ code: "traffic.source.none",
18174
+ summary: "No server-side traffic source connected \u2014 server-log AI visibility data unavailable for this project.",
18175
+ remediation: "Connect a traffic source via `canonry traffic connect <type> <project>` to surface crawler hits and AI-referral arrivals from your server logs.",
18176
+ details: { sourceCount: 0 }
18177
+ };
18178
+ }
18179
+ const errored = sources.filter((s) => s.status === "error");
18180
+ if (errored.length > 0 && errored.length === sources.length) {
18181
+ return {
18182
+ status: CheckStatuses.fail,
18183
+ code: "traffic.source.all-errored",
18184
+ summary: `All ${sources.length} traffic source(s) are in error state. No data is being ingested.`,
18185
+ remediation: errored[0].lastError ? `Latest error: "${errored[0].lastError}". Re-connect the source or run \`canonry traffic sync <project> --source <id>\` to retry.` : "Run `canonry traffic sources <project>` to inspect the failing source(s) and re-connect.",
18186
+ details: { sourceCount: sources.length, erroredIds: errored.map((s) => s.id) }
18187
+ };
18188
+ }
18189
+ if (errored.length > 0) {
18190
+ return {
18191
+ status: CheckStatuses.warn,
18192
+ code: "traffic.source.partially-errored",
18193
+ summary: `${errored.length} of ${sources.length} traffic source(s) are in error state.`,
18194
+ remediation: "Run `canonry traffic sources <project>` to inspect the failing sources individually.",
18195
+ details: { sourceCount: sources.length, erroredIds: errored.map((s) => s.id) }
18196
+ };
18197
+ }
18198
+ return {
18199
+ status: CheckStatuses.ok,
18200
+ code: "traffic.source.connected",
18201
+ summary: `${sources.length} traffic source(s) connected: ${sources.map((s) => s.displayName).join(", ")}.`,
18202
+ details: { sourceCount: sources.length, sourceTypes: [...new Set(sources.map((s) => s.sourceType))] }
18203
+ };
18204
+ }
18205
+ };
18206
+ var recentDataCheck = {
18207
+ id: "traffic.source.recent-data",
18208
+ category: CheckCategories.integrations,
18209
+ scope: CheckScopes.project,
18210
+ title: "Traffic source recent data",
18211
+ run: (ctx) => {
18212
+ if (!ctx.project) return skippedNoProject2();
18213
+ const sources = loadProbes(ctx);
18214
+ if (sources.length === 0) {
18215
+ return {
18216
+ status: CheckStatuses.skipped,
18217
+ code: "traffic.recent-data.no-source",
18218
+ summary: "No traffic source connected \u2014 recent-data check skipped."
18219
+ };
18220
+ }
18221
+ const now = /* @__PURE__ */ new Date();
18222
+ const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
18223
+ const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
18224
+ const recentCrawlers = Number(
18225
+ ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
18226
+ and15(
18227
+ eq24(crawlerEventsHourly.projectId, ctx.project.id),
18228
+ gte3(crawlerEventsHourly.tsHour, warnCutoff)
18229
+ )
18230
+ ).get()?.total ?? 0
18231
+ );
18232
+ const recentReferrals = Number(
18233
+ ctx.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
18234
+ and15(
18235
+ eq24(aiReferralEventsHourly.projectId, ctx.project.id),
18236
+ gte3(aiReferralEventsHourly.tsHour, warnCutoff)
18237
+ )
18238
+ ).get()?.total ?? 0
18239
+ );
18240
+ if (recentCrawlers > 0 || recentReferrals > 0) {
18241
+ return {
18242
+ status: CheckStatuses.ok,
18243
+ code: "traffic.recent-data.fresh",
18244
+ summary: `${recentCrawlers} crawler hit(s) and ${recentReferrals} AI-referral arrival(s) in the last ${RECENT_DATA_WARN_DAYS} days.`,
18245
+ details: { crawlerHits: recentCrawlers, referralArrivals: recentReferrals, windowDays: RECENT_DATA_WARN_DAYS }
18246
+ };
18247
+ }
18248
+ const olderCrawlers = Number(
18249
+ ctx.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
18250
+ and15(
18251
+ eq24(crawlerEventsHourly.projectId, ctx.project.id),
18252
+ gte3(crawlerEventsHourly.tsHour, failCutoff)
18253
+ )
18254
+ ).get()?.total ?? 0
18255
+ );
18256
+ const lastSyncedAt = sources.map((s) => s.lastSyncedAt).filter(Boolean).sort().at(-1) ?? null;
18257
+ if (olderCrawlers > 0 || lastSyncedAt) {
18258
+ return {
18259
+ status: CheckStatuses.warn,
18260
+ code: "traffic.recent-data.stale",
18261
+ summary: `No crawler hits or AI-referral arrivals in the last ${RECENT_DATA_WARN_DAYS} days, though older data exists.`,
18262
+ remediation: lastSyncedAt ? `Last sync: ${lastSyncedAt}. Run \`canonry traffic sync <project>\` to refresh, or check the source connection.` : "Run `canonry traffic sync <project>` to pull recent events.",
18263
+ details: { lastSyncedAt, sourceCount: sources.length }
18264
+ };
18265
+ }
18266
+ return {
18267
+ status: CheckStatuses.fail,
18268
+ code: "traffic.recent-data.empty",
18269
+ summary: `No traffic data in the last ${RECENT_DATA_FAIL_DAYS} days. The source is connected but isn't ingesting.`,
18270
+ remediation: "Verify the source's configuration with `canonry traffic sources <project>` and run a manual sync to confirm credentials + scopes are still valid.",
18271
+ details: { sourceCount: sources.length }
18272
+ };
18273
+ }
18274
+ };
18275
+ async function runValidator(source, validator, fallbackId, fallbackLabel) {
18276
+ if (!validator) {
18277
+ return {
18278
+ source,
18279
+ output: {
18280
+ status: CheckStatuses.skipped,
18281
+ code: `traffic.${fallbackId}.no-validator`,
18282
+ summary: `No ${fallbackLabel} validator registered for source type "${source.sourceType}".`
18283
+ }
18284
+ };
18285
+ }
18286
+ try {
18287
+ const result = await validator(source);
18288
+ if (!result) {
18289
+ return {
18290
+ source,
18291
+ output: {
18292
+ status: CheckStatuses.skipped,
18293
+ code: `traffic.${fallbackId}.unsupported`,
18294
+ summary: `Validator for "${source.sourceType}" does not implement ${fallbackLabel} validation.`
18295
+ }
18296
+ };
18297
+ }
18298
+ return { source, output: result };
18299
+ } catch (e) {
18300
+ const msg = e instanceof Error ? e.message : String(e);
18301
+ return {
18302
+ source,
18303
+ output: {
18304
+ status: CheckStatuses.fail,
18305
+ code: `traffic.${fallbackId}.validator-error`,
18306
+ summary: `${fallbackLabel} validator threw: ${msg}.`,
18307
+ remediation: "Check the source configuration and credentials, then re-run the doctor."
18308
+ }
18309
+ };
18310
+ }
18311
+ }
18312
+ function summarizePerSourceResults(fallbackId, fallbackLabel, results) {
18313
+ const failed = results.filter((r) => r.output.status === CheckStatuses.fail);
18314
+ const warned = results.filter((r) => r.output.status === CheckStatuses.warn);
18315
+ const skipped = results.filter((r) => r.output.status === CheckStatuses.skipped);
18316
+ const ok = results.filter((r) => r.output.status === CheckStatuses.ok);
18317
+ const detail = {
18318
+ sources: results.map((r) => ({
18319
+ id: r.source.id,
18320
+ sourceType: r.source.sourceType,
18321
+ displayName: r.source.displayName,
18322
+ status: r.output.status,
18323
+ code: r.output.code,
18324
+ summary: r.output.summary
18325
+ }))
18326
+ };
18327
+ if (failed.length > 0) {
18328
+ return {
18329
+ status: CheckStatuses.fail,
18330
+ code: `traffic.${fallbackId}.failed`,
18331
+ summary: `${failed.length} of ${results.length} source(s) failed ${fallbackLabel} validation: ${failed.map((r) => `${r.source.displayName} (${r.output.summary})`).join("; ")}.`,
18332
+ remediation: failed[0].output.remediation ?? `Inspect the failing source(s) \u2014 see details.sources for per-source codes.`,
18333
+ details: detail
18334
+ };
18335
+ }
18336
+ if (warned.length > 0) {
18337
+ return {
18338
+ status: CheckStatuses.warn,
18339
+ code: `traffic.${fallbackId}.warned`,
18340
+ summary: `${warned.length} of ${results.length} source(s) raised warnings during ${fallbackLabel} validation.`,
18341
+ remediation: warned[0].output.remediation ?? `Review the warning(s) \u2014 see details.sources.`,
18342
+ details: detail
18343
+ };
18344
+ }
18345
+ if (ok.length > 0) {
18346
+ return {
18347
+ status: CheckStatuses.ok,
18348
+ code: `traffic.${fallbackId}.ok`,
18349
+ summary: `${ok.length} source(s) passed ${fallbackLabel} validation${skipped.length > 0 ? ` (${skipped.length} skipped)` : ""}.`,
18350
+ details: detail
18351
+ };
18352
+ }
18353
+ return {
18354
+ status: CheckStatuses.skipped,
18355
+ code: `traffic.${fallbackId}.all-skipped`,
18356
+ summary: `No source-type validator was available for any of the ${results.length} connected source(s).`,
18357
+ details: detail
18358
+ };
18359
+ }
18360
+ var credentialsCheck = {
18361
+ id: "traffic.source.credentials",
18362
+ category: CheckCategories.auth,
18363
+ scope: CheckScopes.project,
18364
+ title: "Traffic source credentials",
18365
+ run: async (ctx) => {
18366
+ if (!ctx.project) return skippedNoProject2();
18367
+ const sources = loadProbes(ctx);
18368
+ if (sources.length === 0) {
18369
+ return {
18370
+ status: CheckStatuses.skipped,
18371
+ code: "traffic.credentials.no-source",
18372
+ summary: "No traffic source connected \u2014 credentials check skipped."
18373
+ };
18374
+ }
18375
+ const validators = ctx.trafficSourceValidators ?? {};
18376
+ const results = await Promise.all(
18377
+ sources.map(
18378
+ (s) => runValidator(
18379
+ s,
18380
+ validators[s.sourceType]?.validateCredentials?.bind(validators[s.sourceType]),
18381
+ "credentials",
18382
+ "credentials"
18383
+ )
18384
+ )
18385
+ );
18386
+ return summarizePerSourceResults("credentials", "credentials", results);
18387
+ }
18388
+ };
18389
+ var scopesCheck2 = {
18390
+ id: "traffic.source.scopes",
18391
+ category: CheckCategories.auth,
18392
+ scope: CheckScopes.project,
18393
+ title: "Traffic source scopes",
18394
+ run: async (ctx) => {
18395
+ if (!ctx.project) return skippedNoProject2();
18396
+ const sources = loadProbes(ctx);
18397
+ if (sources.length === 0) {
18398
+ return {
18399
+ status: CheckStatuses.skipped,
18400
+ code: "traffic.scopes.no-source",
18401
+ summary: "No traffic source connected \u2014 scopes check skipped."
18402
+ };
18403
+ }
18404
+ const validators = ctx.trafficSourceValidators ?? {};
18405
+ const results = await Promise.all(
18406
+ sources.map(
18407
+ (s) => runValidator(
18408
+ s,
18409
+ validators[s.sourceType]?.validateScopes?.bind(validators[s.sourceType]),
18410
+ "scopes",
18411
+ "scopes"
18412
+ )
18413
+ )
18414
+ );
18415
+ return summarizePerSourceResults("scopes", "scopes", results);
18416
+ }
18417
+ };
18418
+ var TRAFFIC_SOURCE_CHECKS = [
18419
+ sourceConnectedCheck,
18420
+ recentDataCheck,
18421
+ credentialsCheck,
18422
+ scopesCheck2
18423
+ ];
18424
+
17747
18425
  // ../api-routes/src/doctor/registry.ts
17748
18426
  var ALL_CHECKS = [
17749
18427
  ...GOOGLE_AUTH_CHECKS,
17750
18428
  ...BING_AUTH_CHECKS,
17751
18429
  ...GA_AUTH_CHECKS,
17752
- ...PROVIDERS_CHECKS
18430
+ ...PROVIDERS_CHECKS,
18431
+ ...TRAFFIC_SOURCE_CHECKS
17753
18432
  ];
17754
18433
  var CHECK_BY_ID = Object.fromEntries(
17755
18434
  ALL_CHECKS.map((check) => [check.id, check])
@@ -17836,7 +18515,8 @@ async function doctorRoutes(app, opts) {
17836
18515
  ga4CredentialStore: opts.ga4CredentialStore,
17837
18516
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
17838
18517
  redirectUri,
17839
- providerSummary: opts.providerSummary
18518
+ providerSummary: opts.providerSummary,
18519
+ trafficSourceValidators: opts.trafficSourceValidators
17840
18520
  };
17841
18521
  return runChecks(ctx, ALL_CHECKS, { checkIds });
17842
18522
  });
@@ -17856,7 +18536,8 @@ async function doctorRoutes(app, opts) {
17856
18536
  ga4CredentialStore: opts.ga4CredentialStore,
17857
18537
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
17858
18538
  redirectUri,
17859
- providerSummary: opts.providerSummary
18539
+ providerSummary: opts.providerSummary,
18540
+ trafficSourceValidators: opts.trafficSourceValidators
17860
18541
  };
17861
18542
  return runChecks(ctx, ALL_CHECKS, { checkIds });
17862
18543
  });
@@ -17978,7 +18659,8 @@ async function apiRoutes(app, opts) {
17978
18659
  await api.register(trafficRoutes, {
17979
18660
  cloudRunCredentialStore: opts.cloudRunCredentialStore,
17980
18661
  pullCloudRunEvents: opts.pullCloudRunEvents,
17981
- resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken
18662
+ resolveCloudRunAccessToken: opts.resolveCloudRunAccessToken,
18663
+ onTrafficSynced: opts.onTrafficSynced
17982
18664
  });
17983
18665
  await api.register(backlinksRoutes, {
17984
18666
  getBacklinksStatus: opts.getBacklinksStatus,
@@ -17995,13 +18677,57 @@ async function apiRoutes(app, opts) {
17995
18677
  ga4CredentialStore: opts.ga4CredentialStore,
17996
18678
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
17997
18679
  publicUrl: opts.publicUrl,
17998
- providerSummary: opts.providerSummary
18680
+ providerSummary: opts.providerSummary,
18681
+ trafficSourceValidators: buildTrafficSourceValidators(opts)
17999
18682
  });
18000
18683
  if (opts.registerAuthenticatedRoutes) {
18001
18684
  await opts.registerAuthenticatedRoutes(api);
18002
18685
  }
18003
18686
  }, { prefix: opts.routePrefix ?? "/api/v1" });
18004
18687
  }
18688
+ function buildTrafficSourceValidators(opts) {
18689
+ const validators = {};
18690
+ if (opts.cloudRunCredentialStore) {
18691
+ const store = opts.cloudRunCredentialStore;
18692
+ const resolveToken = opts.resolveCloudRunAccessToken ?? defaultResolveAccessToken;
18693
+ validators["cloud-run"] = {
18694
+ validateCredentials: async (source) => {
18695
+ const record = store.getConnection(source.projectName);
18696
+ if (!record) {
18697
+ return {
18698
+ status: CheckStatuses.fail,
18699
+ code: "traffic.credentials.missing",
18700
+ summary: `No Cloud Run credential found in ~/.canonry/config.yaml for project "${source.projectName}".`,
18701
+ remediation: "Re-run `canonry traffic connect cloud-run <project> --gcp-project <id> --service-account-key <path>`."
18702
+ };
18703
+ }
18704
+ try {
18705
+ await resolveToken(record);
18706
+ return {
18707
+ status: CheckStatuses.ok,
18708
+ code: "traffic.credentials.resolved",
18709
+ summary: `Cloud Run access token resolves for "${source.displayName}" (project ${record.gcpProjectId}).`
18710
+ };
18711
+ } catch (e) {
18712
+ const msg = e instanceof Error ? e.message : String(e);
18713
+ return {
18714
+ status: CheckStatuses.fail,
18715
+ code: "traffic.credentials.resolve-failed",
18716
+ summary: `Failed to resolve Cloud Run access token: ${msg}.`,
18717
+ remediation: "Verify the service-account key in ~/.canonry/config.yaml is unexpired and well-formed. Re-connect the source if needed."
18718
+ };
18719
+ }
18720
+ },
18721
+ // Cloud Run scopes are implicit in the service-account key — Cloud
18722
+ // Logging viewer is the only required scope today, and it's enforced
18723
+ // at the IAM layer rather than baked into the token. We surface a
18724
+ // skipped result so the framework is uniform without producing a
18725
+ // false signal.
18726
+ validateScopes: () => null
18727
+ };
18728
+ }
18729
+ return Object.keys(validators).length > 0 ? validators : void 0;
18730
+ }
18005
18731
 
18006
18732
  // src/server.ts
18007
18733
  import os6 from "os";
@@ -20493,7 +21219,7 @@ import crypto22 from "crypto";
20493
21219
  import fs7 from "fs";
20494
21220
  import path9 from "path";
20495
21221
  import os5 from "os";
20496
- import { and as and15, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
21222
+ import { and as and16, eq as eq25, inArray as inArray7, sql as sql10 } from "drizzle-orm";
20497
21223
 
20498
21224
  // src/run-telemetry.ts
20499
21225
  import crypto21 from "crypto";
@@ -20838,7 +21564,7 @@ var JobRunner = class {
20838
21564
  if (stale.length === 0) return;
20839
21565
  const now = (/* @__PURE__ */ new Date()).toISOString();
20840
21566
  for (const run of stale) {
20841
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq24(runs.id, run.id)).run();
21567
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq25(runs.id, run.id)).run();
20842
21568
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
20843
21569
  }
20844
21570
  }
@@ -20872,10 +21598,10 @@ var JobRunner = class {
20872
21598
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
20873
21599
  }
20874
21600
  if (existingRun.status === "queued") {
20875
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and15(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
21601
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and16(eq25(runs.id, runId), eq25(runs.status, "queued"))).run();
20876
21602
  }
20877
21603
  this.throwIfRunCancelled(runId);
20878
- const project = this.db.select().from(projects).where(eq24(projects.id, projectId)).get();
21604
+ const project = this.db.select().from(projects).where(eq25(projects.id, projectId)).get();
20879
21605
  if (!project) {
20880
21606
  throw new Error(`Project ${projectId} not found`);
20881
21607
  }
@@ -20896,8 +21622,8 @@ var JobRunner = class {
20896
21622
  throw new Error("No providers configured. Add at least one provider API key.");
20897
21623
  }
20898
21624
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
20899
- projectQueries = this.db.select().from(queries).where(eq24(queries.projectId, projectId)).all();
20900
- const projectCompetitors = this.db.select().from(competitors).where(eq24(competitors.projectId, projectId)).all();
21625
+ projectQueries = this.db.select().from(queries).where(eq25(queries.projectId, projectId)).all();
21626
+ const projectCompetitors = this.db.select().from(competitors).where(eq25(competitors.projectId, projectId)).all();
20901
21627
  const competitorDomains = projectCompetitors.map((c) => c.domain);
20902
21628
  const allDomains = effectiveDomains({
20903
21629
  canonicalDomain: project.canonicalDomain,
@@ -20915,7 +21641,7 @@ var JobRunner = class {
20915
21641
  const todayPeriod = getCurrentUsageDay();
20916
21642
  for (const p of activeProviders) {
20917
21643
  const providerScope = `${projectId}:${p.adapter.name}`;
20918
- const providerUsage = this.db.select().from(usageCounters).where(eq24(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
21644
+ const providerUsage = this.db.select().from(usageCounters).where(eq25(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
20919
21645
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
20920
21646
  if (providerUsage + queriesPerProvider > limit) {
20921
21647
  throw new Error(
@@ -21058,12 +21784,12 @@ var JobRunner = class {
21058
21784
  const someFailed = providerErrors.size > 0;
21059
21785
  if (allFailed) {
21060
21786
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
21061
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq24(runs.id, runId)).run();
21787
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq25(runs.id, runId)).run();
21062
21788
  } else if (someFailed) {
21063
21789
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
21064
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq24(runs.id, runId)).run();
21790
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq25(runs.id, runId)).run();
21065
21791
  } else {
21066
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
21792
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
21067
21793
  }
21068
21794
  this.flushProviderUsage(projectId, providerDispatchCounts);
21069
21795
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -21109,7 +21835,7 @@ var JobRunner = class {
21109
21835
  status: "failed",
21110
21836
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
21111
21837
  error: errorMessage
21112
- }).where(eq24(runs.id, runId)).run();
21838
+ }).where(eq25(runs.id, runId)).run();
21113
21839
  this.flushProviderUsage(projectId, providerDispatchCounts);
21114
21840
  const abortReason = classifyRunAbortReason(errorMessage);
21115
21841
  const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
@@ -21162,7 +21888,7 @@ var JobRunner = class {
21162
21888
  updatedAt: now
21163
21889
  }).onConflictDoUpdate({
21164
21890
  target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
21165
- set: { count: sql8`${usageCounters.count} + ${count}`, updatedAt: now }
21891
+ set: { count: sql10`${usageCounters.count} + ${count}`, updatedAt: now }
21166
21892
  }).run();
21167
21893
  }
21168
21894
  flushProviderUsage(projectId, providerDispatchCounts) {
@@ -21177,7 +21903,7 @@ var JobRunner = class {
21177
21903
  finishedAt: runs.finishedAt,
21178
21904
  error: runs.error,
21179
21905
  trigger: runs.trigger
21180
- }).from(runs).where(eq24(runs.id, runId)).get();
21906
+ }).from(runs).where(eq25(runs.id, runId)).get();
21181
21907
  }
21182
21908
  isRunCancelled(runId) {
21183
21909
  return this.getRunState(runId)?.status === "cancelled";
@@ -21193,7 +21919,7 @@ var JobRunner = class {
21193
21919
  this.db.update(runs).set({
21194
21920
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
21195
21921
  error: currentRun.error ?? "Cancelled by user"
21196
- }).where(eq24(runs.id, runId)).run();
21922
+ }).where(eq25(runs.id, runId)).run();
21197
21923
  }
21198
21924
  trackEvent(
21199
21925
  "run.completed",
@@ -21231,7 +21957,7 @@ function buildPhases(input) {
21231
21957
 
21232
21958
  // src/gsc-sync.ts
21233
21959
  import crypto23 from "crypto";
21234
- import { eq as eq25, and as and16, sql as sql9 } from "drizzle-orm";
21960
+ import { eq as eq26, and as and17, sql as sql11 } from "drizzle-orm";
21235
21961
  var log2 = createLogger("GscSync");
21236
21962
  function formatDate3(d) {
21237
21963
  return d.toISOString().split("T")[0];
@@ -21243,13 +21969,13 @@ function daysAgo(n) {
21243
21969
  }
21244
21970
  async function executeGscSync(db, runId, projectId, opts) {
21245
21971
  const now = (/* @__PURE__ */ new Date()).toISOString();
21246
- db.update(runs).set({ status: "running", startedAt: now }).where(eq25(runs.id, runId)).run();
21972
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq26(runs.id, runId)).run();
21247
21973
  try {
21248
21974
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
21249
21975
  if (!googleClientId || !googleClientSecret) {
21250
21976
  throw new Error("Google OAuth is not configured in the local Canonry config");
21251
21977
  }
21252
- const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
21978
+ const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
21253
21979
  if (!project) {
21254
21980
  throw new Error(`Project not found: ${projectId}`);
21255
21981
  }
@@ -21283,10 +22009,10 @@ async function executeGscSync(db, runId, projectId, opts) {
21283
22009
  });
21284
22010
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
21285
22011
  db.delete(gscSearchData).where(
21286
- and16(
21287
- eq25(gscSearchData.projectId, projectId),
21288
- sql9`${gscSearchData.date} >= ${startDate}`,
21289
- sql9`${gscSearchData.date} <= ${endDate}`
22012
+ and17(
22013
+ eq26(gscSearchData.projectId, projectId),
22014
+ sql11`${gscSearchData.date} >= ${startDate}`,
22015
+ sql11`${gscSearchData.date} <= ${endDate}`
21290
22016
  )
21291
22017
  ).run();
21292
22018
  const batchSize = 500;
@@ -21351,7 +22077,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21351
22077
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
21352
22078
  }
21353
22079
  }
21354
- const allInspections = db.select().from(gscUrlInspections).where(eq25(gscUrlInspections.projectId, projectId)).all();
22080
+ const allInspections = db.select().from(gscUrlInspections).where(eq26(gscUrlInspections.projectId, projectId)).all();
21355
22081
  const latestByUrl = /* @__PURE__ */ new Map();
21356
22082
  for (const row of allInspections) {
21357
22083
  const existing = latestByUrl.get(row.url);
@@ -21372,7 +22098,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21372
22098
  }
21373
22099
  }
21374
22100
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
21375
- db.delete(gscCoverageSnapshots).where(and16(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
22101
+ db.delete(gscCoverageSnapshots).where(and17(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
21376
22102
  db.insert(gscCoverageSnapshots).values({
21377
22103
  id: crypto23.randomUUID(),
21378
22104
  projectId,
@@ -21383,11 +22109,11 @@ async function executeGscSync(db, runId, projectId, opts) {
21383
22109
  reasonBreakdown: JSON.stringify(reasonCounts),
21384
22110
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
21385
22111
  }).run();
21386
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
22112
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
21387
22113
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
21388
22114
  } catch (err) {
21389
22115
  const errorMsg = err instanceof Error ? err.message : String(err);
21390
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
22116
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
21391
22117
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
21392
22118
  throw err;
21393
22119
  }
@@ -21395,7 +22121,7 @@ async function executeGscSync(db, runId, projectId, opts) {
21395
22121
 
21396
22122
  // src/gsc-inspect-sitemap.ts
21397
22123
  import crypto24 from "crypto";
21398
- import { eq as eq26, and as and17 } from "drizzle-orm";
22124
+ import { eq as eq27, and as and18 } from "drizzle-orm";
21399
22125
 
21400
22126
  // src/sitemap-parser.ts
21401
22127
  var log3 = createLogger("SitemapParser");
@@ -21516,13 +22242,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
21516
22242
  var log4 = createLogger("InspectSitemap");
21517
22243
  async function executeInspectSitemap(db, runId, projectId, opts) {
21518
22244
  const now = (/* @__PURE__ */ new Date()).toISOString();
21519
- db.update(runs).set({ status: "running", startedAt: now }).where(eq26(runs.id, runId)).run();
22245
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq27(runs.id, runId)).run();
21520
22246
  try {
21521
22247
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
21522
22248
  if (!googleClientId || !googleClientSecret) {
21523
22249
  throw new Error("Google OAuth is not configured in the local Canonry config");
21524
22250
  }
21525
- const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
22251
+ const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
21526
22252
  if (!project) {
21527
22253
  throw new Error(`Project not found: ${projectId}`);
21528
22254
  }
@@ -21590,7 +22316,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21590
22316
  await new Promise((r) => setTimeout(r, 1e3));
21591
22317
  }
21592
22318
  }
21593
- const allInspections = db.select().from(gscUrlInspections).where(eq26(gscUrlInspections.projectId, projectId)).all();
22319
+ const allInspections = db.select().from(gscUrlInspections).where(eq27(gscUrlInspections.projectId, projectId)).all();
21594
22320
  const latestByUrl = /* @__PURE__ */ new Map();
21595
22321
  for (const row of allInspections) {
21596
22322
  const existing = latestByUrl.get(row.url);
@@ -21611,7 +22337,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21611
22337
  }
21612
22338
  }
21613
22339
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
21614
- db.delete(gscCoverageSnapshots).where(and17(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
22340
+ db.delete(gscCoverageSnapshots).where(and18(eq27(gscCoverageSnapshots.projectId, projectId), eq27(gscCoverageSnapshots.date, snapshotDate))).run();
21615
22341
  db.insert(gscCoverageSnapshots).values({
21616
22342
  id: crypto24.randomUUID(),
21617
22343
  projectId,
@@ -21623,11 +22349,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21623
22349
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
21624
22350
  }).run();
21625
22351
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
21626
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
22352
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
21627
22353
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
21628
22354
  } catch (err) {
21629
22355
  const errorMsg = err instanceof Error ? err.message : String(err);
21630
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
22356
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
21631
22357
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
21632
22358
  throw err;
21633
22359
  }
@@ -21635,7 +22361,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
21635
22361
 
21636
22362
  // src/bing-inspect-sitemap.ts
21637
22363
  import crypto25 from "crypto";
21638
- import { eq as eq27, desc as desc13 } from "drizzle-orm";
22364
+ import { eq as eq28, desc as desc13 } from "drizzle-orm";
21639
22365
  var log5 = createLogger("BingInspectSitemap");
21640
22366
  function parseBingDate2(value) {
21641
22367
  if (!value) return null;
@@ -21653,9 +22379,9 @@ function isBlockingIssueType2(issueType) {
21653
22379
  }
21654
22380
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
21655
22381
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21656
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq27(runs.id, runId)).run();
22382
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq28(runs.id, runId)).run();
21657
22383
  try {
21658
- const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
22384
+ const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
21659
22385
  if (!project) {
21660
22386
  throw new Error(`Project not found: ${projectId}`);
21661
22387
  }
@@ -21673,7 +22399,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21673
22399
  if (sitemapUrls.length === 0) {
21674
22400
  throw new Error("No URLs found in sitemap");
21675
22401
  }
21676
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).all();
22402
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq28(bingUrlInspections.projectId, projectId)).all();
21677
22403
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
21678
22404
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
21679
22405
  log5.info("sitemap.diff", {
@@ -21756,7 +22482,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21756
22482
  await new Promise((r) => setTimeout(r, 1e3));
21757
22483
  }
21758
22484
  }
21759
- const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc13(bingUrlInspections.inspectedAt)).all();
22485
+ const allInspections = db.select().from(bingUrlInspections).where(eq28(bingUrlInspections.projectId, projectId)).orderBy(desc13(bingUrlInspections.inspectedAt)).all();
21760
22486
  const latestByUrl = /* @__PURE__ */ new Map();
21761
22487
  const definitiveByUrl = /* @__PURE__ */ new Map();
21762
22488
  for (const row of allInspections) {
@@ -21799,7 +22525,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21799
22525
  }
21800
22526
  }).run();
21801
22527
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
21802
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
22528
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq28(runs.id, runId)).run();
21803
22529
  log5.info("inspect.completed", {
21804
22530
  runId,
21805
22531
  projectId,
@@ -21813,7 +22539,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21813
22539
  });
21814
22540
  } catch (err) {
21815
22541
  const errorMsg = err instanceof Error ? err.message : String(err);
21816
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
22542
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq28(runs.id, runId)).run();
21817
22543
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
21818
22544
  throw err;
21819
22545
  }
@@ -21822,7 +22548,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
21822
22548
  // src/commoncrawl-sync.ts
21823
22549
  import crypto26 from "crypto";
21824
22550
  import path10 from "path";
21825
- import { and as and18, eq as eq28, sql as sql10 } from "drizzle-orm";
22551
+ import { and as and19, eq as eq29, sql as sql12 } from "drizzle-orm";
21826
22552
  var log6 = createLogger("CommonCrawlSync");
21827
22553
  var INSERT_CHUNK_SIZE = 1e4;
21828
22554
  function defaultDeps() {
@@ -21848,7 +22574,7 @@ async function executeReleaseSync(db, syncId, opts) {
21848
22574
  phaseDetail: "downloading vertices + edges",
21849
22575
  updatedAt: downloadStartedAt,
21850
22576
  error: null
21851
- }).where(eq28(ccReleaseSyncs.id, syncId)).run();
22577
+ }).where(eq29(ccReleaseSyncs.id, syncId)).run();
21852
22578
  const paths = ccReleasePaths(release);
21853
22579
  const releaseCacheDir = path10.join(deps.cacheDir, release);
21854
22580
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -21871,7 +22597,7 @@ async function executeReleaseSync(db, syncId, opts) {
21871
22597
  vertexSha256: vertex.sha256,
21872
22598
  edgesSha256: edges.sha256,
21873
22599
  updatedAt: downloadFinishedAt
21874
- }).where(eq28(ccReleaseSyncs.id, syncId)).run();
22600
+ }).where(eq29(ccReleaseSyncs.id, syncId)).run();
21875
22601
  const allProjects = db.select().from(projects).all();
21876
22602
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
21877
22603
  let rows = [];
@@ -21887,8 +22613,8 @@ async function executeReleaseSync(db, syncId, opts) {
21887
22613
  }
21888
22614
  const queriedAt = deps.now().toISOString();
21889
22615
  db.transaction((tx) => {
21890
- tx.delete(backlinkDomains).where(eq28(backlinkDomains.releaseSyncId, syncId)).run();
21891
- tx.delete(backlinkSummaries).where(eq28(backlinkSummaries.releaseSyncId, syncId)).run();
22616
+ tx.delete(backlinkDomains).where(eq29(backlinkDomains.releaseSyncId, syncId)).run();
22617
+ tx.delete(backlinkSummaries).where(eq29(backlinkSummaries.releaseSyncId, syncId)).run();
21892
22618
  const expanded = [];
21893
22619
  for (const r of rows) {
21894
22620
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -21947,7 +22673,7 @@ async function executeReleaseSync(db, syncId, opts) {
21947
22673
  domainsDiscovered: rows.length,
21948
22674
  updatedAt: finishedAt,
21949
22675
  error: null
21950
- }).where(eq28(ccReleaseSyncs.id, syncId)).run();
22676
+ }).where(eq29(ccReleaseSyncs.id, syncId)).run();
21951
22677
  log6.info("sync.completed", {
21952
22678
  syncId,
21953
22679
  release,
@@ -21977,7 +22703,7 @@ async function executeReleaseSync(db, syncId, opts) {
21977
22703
  error: errorMsg,
21978
22704
  phaseDetail: null,
21979
22705
  updatedAt: finishedAt
21980
- }).where(eq28(ccReleaseSyncs.id, syncId)).run();
22706
+ }).where(eq29(ccReleaseSyncs.id, syncId)).run();
21981
22707
  log6.error("sync.failed", { syncId, release, error: errorMsg });
21982
22708
  throw err;
21983
22709
  }
@@ -22013,7 +22739,7 @@ function computeSummary(rows) {
22013
22739
  // src/backlink-extract.ts
22014
22740
  import crypto27 from "crypto";
22015
22741
  import fs8 from "fs";
22016
- import { and as and19, desc as desc14, eq as eq29 } from "drizzle-orm";
22742
+ import { and as and20, desc as desc14, eq as eq30 } from "drizzle-orm";
22017
22743
  var log7 = createLogger("BacklinkExtract");
22018
22744
  function defaultDeps2() {
22019
22745
  return {
@@ -22025,13 +22751,13 @@ function defaultDeps2() {
22025
22751
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
22026
22752
  const deps = { ...defaultDeps2(), ...opts.deps };
22027
22753
  const startedAt = deps.now().toISOString();
22028
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq29(runs.id, runId)).run();
22754
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq30(runs.id, runId)).run();
22029
22755
  try {
22030
- const project = db.select().from(projects).where(eq29(projects.id, projectId)).get();
22756
+ const project = db.select().from(projects).where(eq30(projects.id, projectId)).get();
22031
22757
  if (!project) {
22032
22758
  throw new Error(`Project not found: ${projectId}`);
22033
22759
  }
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();
22760
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc14(ccReleaseSyncs.createdAt)).limit(1).get();
22035
22761
  if (!sync) {
22036
22762
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
22037
22763
  }
@@ -22059,7 +22785,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
22059
22785
  const targetDomain = project.canonicalDomain;
22060
22786
  db.transaction((tx) => {
22061
22787
  tx.delete(backlinkDomains).where(
22062
- and19(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
22788
+ and20(eq30(backlinkDomains.projectId, projectId), eq30(backlinkDomains.release, release))
22063
22789
  ).run();
22064
22790
  if (rows.length > 0) {
22065
22791
  const values = rows.map((r) => ({
@@ -22099,7 +22825,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
22099
22825
  }).run();
22100
22826
  });
22101
22827
  const finishedAt = deps.now().toISOString();
22102
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
22828
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq30(runs.id, runId)).run();
22103
22829
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
22104
22830
  } catch (err) {
22105
22831
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -22108,7 +22834,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
22108
22834
  status: RunStatuses.failed,
22109
22835
  error: errorMsg,
22110
22836
  finishedAt
22111
- }).where(eq29(runs.id, runId)).run();
22837
+ }).where(eq30(runs.id, runId)).run();
22112
22838
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
22113
22839
  throw err;
22114
22840
  }
@@ -22181,7 +22907,7 @@ var ProviderRegistry = class {
22181
22907
 
22182
22908
  // src/scheduler.ts
22183
22909
  import cron from "node-cron";
22184
- import { and as and20, eq as eq30 } from "drizzle-orm";
22910
+ import { and as and21, eq as eq31 } from "drizzle-orm";
22185
22911
  var log8 = createLogger("Scheduler");
22186
22912
  function taskKey(projectId, kind) {
22187
22913
  return `${projectId}::${kind}`;
@@ -22196,7 +22922,7 @@ var Scheduler = class {
22196
22922
  }
22197
22923
  /** Load all enabled schedules from DB and register cron jobs. */
22198
22924
  start() {
22199
- const allSchedules = this.db.select().from(schedules).where(eq30(schedules.enabled, 1)).all();
22925
+ const allSchedules = this.db.select().from(schedules).where(eq31(schedules.enabled, 1)).all();
22200
22926
  for (const schedule of allSchedules) {
22201
22927
  const missedRunAt = schedule.nextRunAt;
22202
22928
  this.registerCronTask(schedule);
@@ -22226,7 +22952,7 @@ var Scheduler = class {
22226
22952
  this.stopTask(key, existing, "Stopped");
22227
22953
  this.tasks.delete(key);
22228
22954
  }
22229
- const schedule = this.db.select().from(schedules).where(and20(eq30(schedules.projectId, projectId), eq30(schedules.kind, kind))).get();
22955
+ const schedule = this.db.select().from(schedules).where(and21(eq31(schedules.projectId, projectId), eq31(schedules.kind, kind))).get();
22230
22956
  if (schedule && schedule.enabled === 1) {
22231
22957
  this.registerCronTask(schedule);
22232
22958
  }
@@ -22267,14 +22993,14 @@ var Scheduler = class {
22267
22993
  this.db.update(schedules).set({
22268
22994
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
22269
22995
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22270
- }).where(eq30(schedules.id, scheduleId)).run();
22996
+ }).where(eq31(schedules.id, scheduleId)).run();
22271
22997
  const label = schedule.preset ?? cronExpr;
22272
22998
  log8.info("cron.registered", { projectId, kind, schedule: label, timezone });
22273
22999
  }
22274
23000
  triggerRun(scheduleId, projectId, kind) {
22275
23001
  try {
22276
23002
  const now = (/* @__PURE__ */ new Date()).toISOString();
22277
- const currentSchedule = this.db.select().from(schedules).where(eq30(schedules.id, scheduleId)).get();
23003
+ const currentSchedule = this.db.select().from(schedules).where(eq31(schedules.id, scheduleId)).get();
22278
23004
  if (!currentSchedule || currentSchedule.enabled !== 1) {
22279
23005
  log8.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
22280
23006
  this.remove(projectId, kind);
@@ -22282,7 +23008,7 @@ var Scheduler = class {
22282
23008
  }
22283
23009
  const task = this.tasks.get(taskKey(projectId, kind));
22284
23010
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
22285
- const project = this.db.select().from(projects).where(eq30(projects.id, projectId)).get();
23011
+ const project = this.db.select().from(projects).where(eq31(projects.id, projectId)).get();
22286
23012
  if (!project) {
22287
23013
  log8.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
22288
23014
  this.remove(projectId, kind);
@@ -22302,7 +23028,7 @@ var Scheduler = class {
22302
23028
  lastRunAt: now,
22303
23029
  nextRunAt,
22304
23030
  updatedAt: now
22305
- }).where(eq30(schedules.id, currentSchedule.id)).run();
23031
+ }).where(eq31(schedules.id, currentSchedule.id)).run();
22306
23032
  log8.info("traffic-sync.triggered", { projectName: project.name, sourceId });
22307
23033
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
22308
23034
  return;
@@ -22330,7 +23056,7 @@ var Scheduler = class {
22330
23056
  this.db.update(schedules).set({
22331
23057
  nextRunAt,
22332
23058
  updatedAt: now
22333
- }).where(eq30(schedules.id, currentSchedule.id)).run();
23059
+ }).where(eq31(schedules.id, currentSchedule.id)).run();
22334
23060
  return;
22335
23061
  }
22336
23062
  const runId = queueResult.runId;
@@ -22338,7 +23064,7 @@ var Scheduler = class {
22338
23064
  lastRunAt: now,
22339
23065
  nextRunAt,
22340
23066
  updatedAt: now
22341
- }).where(eq30(schedules.id, currentSchedule.id)).run();
23067
+ }).where(eq31(schedules.id, currentSchedule.id)).run();
22342
23068
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
22343
23069
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
22344
23070
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -22350,7 +23076,7 @@ var Scheduler = class {
22350
23076
  };
22351
23077
 
22352
23078
  // src/notifier.ts
22353
- import { eq as eq31, desc as desc15, and as and21, or as or4 } from "drizzle-orm";
23079
+ import { eq as eq32, desc as desc15, and as and22, or as or4 } from "drizzle-orm";
22354
23080
  import crypto28 from "crypto";
22355
23081
  var log9 = createLogger("Notifier");
22356
23082
  var Notifier = class {
@@ -22363,18 +23089,18 @@ var Notifier = class {
22363
23089
  /** Called after a run completes (success, partial, or failed). */
22364
23090
  async onRunCompleted(runId, projectId) {
22365
23091
  log9.info("run.completed", { runId, projectId });
22366
- const notifs = this.db.select().from(notifications).where(eq31(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
23092
+ const notifs = this.db.select().from(notifications).where(eq32(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
22367
23093
  if (notifs.length === 0) {
22368
23094
  log9.info("notifications.none-enabled", { projectId });
22369
23095
  return;
22370
23096
  }
22371
23097
  log9.info("notifications.found", { projectId, count: notifs.length });
22372
- const run = this.db.select().from(runs).where(eq31(runs.id, runId)).get();
23098
+ const run = this.db.select().from(runs).where(eq32(runs.id, runId)).get();
22373
23099
  if (!run) {
22374
23100
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
22375
23101
  return;
22376
23102
  }
22377
- const project = this.db.select().from(projects).where(eq31(projects.id, projectId)).get();
23103
+ const project = this.db.select().from(projects).where(eq32(projects.id, projectId)).get();
22378
23104
  if (!project) {
22379
23105
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
22380
23106
  return;
@@ -22421,11 +23147,11 @@ var Notifier = class {
22421
23147
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
22422
23148
  if (highInsights.length > 0) insightEvents.push("insight.high");
22423
23149
  if (insightEvents.length === 0) return;
22424
- const notifs = this.db.select().from(notifications).where(eq31(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
23150
+ const notifs = this.db.select().from(notifications).where(eq32(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
22425
23151
  if (notifs.length === 0) return;
22426
- const run = this.db.select().from(runs).where(eq31(runs.id, runId)).get();
23152
+ const run = this.db.select().from(runs).where(eq32(runs.id, runId)).get();
22427
23153
  if (!run) return;
22428
- const project = this.db.select().from(projects).where(eq31(projects.id, projectId)).get();
23154
+ const project = this.db.select().from(projects).where(eq32(projects.id, projectId)).get();
22429
23155
  if (!project) return;
22430
23156
  for (const notif of notifs) {
22431
23157
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -22456,9 +23182,9 @@ var Notifier = class {
22456
23182
  }
22457
23183
  computeTransitions(runId, projectId) {
22458
23184
  const recentRuns = this.db.select().from(runs).where(
22459
- and21(
22460
- eq31(runs.projectId, projectId),
22461
- or4(eq31(runs.status, "completed"), eq31(runs.status, "partial"))
23185
+ and22(
23186
+ eq32(runs.projectId, projectId),
23187
+ or4(eq32(runs.status, "completed"), eq32(runs.status, "partial"))
22462
23188
  )
22463
23189
  ).orderBy(desc15(runs.createdAt)).limit(2).all();
22464
23190
  if (recentRuns.length < 2) return [];
@@ -22470,12 +23196,12 @@ var Notifier = class {
22470
23196
  query: queries.query,
22471
23197
  provider: querySnapshots.provider,
22472
23198
  citationState: querySnapshots.citationState
22473
- }).from(querySnapshots).leftJoin(queries, eq31(querySnapshots.queryId, queries.id)).where(eq31(querySnapshots.runId, currentRunId)).all();
23199
+ }).from(querySnapshots).leftJoin(queries, eq32(querySnapshots.queryId, queries.id)).where(eq32(querySnapshots.runId, currentRunId)).all();
22474
23200
  const previousSnapshots = this.db.select({
22475
23201
  queryId: querySnapshots.queryId,
22476
23202
  provider: querySnapshots.provider,
22477
23203
  citationState: querySnapshots.citationState
22478
- }).from(querySnapshots).where(eq31(querySnapshots.runId, previousRunId)).all();
23204
+ }).from(querySnapshots).where(eq32(querySnapshots.runId, previousRunId)).all();
22479
23205
  const prevMap = /* @__PURE__ */ new Map();
22480
23206
  for (const s of previousSnapshots) {
22481
23207
  prevMap.set(`${s.queryId}:${s.provider}`, s.citationState);
@@ -22592,7 +23318,7 @@ var RunCoordinator = class {
22592
23318
 
22593
23319
  // src/agent/session-registry.ts
22594
23320
  import crypto30 from "crypto";
22595
- import { eq as eq33 } from "drizzle-orm";
23321
+ import { eq as eq34 } from "drizzle-orm";
22596
23322
 
22597
23323
  // src/agent/session.ts
22598
23324
  import fs11 from "fs";
@@ -22942,7 +23668,7 @@ function resolveSessionProviderAndModel(config, opts) {
22942
23668
 
22943
23669
  // src/agent/memory-store.ts
22944
23670
  import crypto29 from "crypto";
22945
- import { and as and22, desc as desc16, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
23671
+ import { and as and23, desc as desc16, eq as eq33, like as like2, sql as sql13 } from "drizzle-orm";
22946
23672
  var COMPACTION_KEY_PREFIX = "compaction:";
22947
23673
  var COMPACTION_NOTES_PER_SESSION = 3;
22948
23674
  function rowToDto2(row) {
@@ -22956,7 +23682,7 @@ function rowToDto2(row) {
22956
23682
  };
22957
23683
  }
22958
23684
  function listMemoryEntries(db, projectId, opts = {}) {
22959
- const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc16(agentMemory.updatedAt));
23685
+ const query = db.select().from(agentMemory).where(eq33(agentMemory.projectId, projectId)).orderBy(desc16(agentMemory.updatedAt));
22960
23686
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
22961
23687
  return rows.map(rowToDto2);
22962
23688
  }
@@ -22987,12 +23713,12 @@ function upsertMemoryEntry(db, args) {
22987
23713
  updatedAt: now
22988
23714
  }
22989
23715
  }).run();
22990
- const row = db.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
23716
+ const row = db.select().from(agentMemory).where(and23(eq33(agentMemory.projectId, args.projectId), eq33(agentMemory.key, args.key))).get();
22991
23717
  if (!row) throw new Error("memory upsert produced no row");
22992
23718
  return rowToDto2(row);
22993
23719
  }
22994
23720
  function deleteMemoryEntry(db, projectId, key) {
22995
- const result = db.delete(agentMemory).where(and22(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
23721
+ const result = db.delete(agentMemory).where(and23(eq33(agentMemory.projectId, projectId), eq33(agentMemory.key, key))).run();
22996
23722
  const changes = result.changes ?? 0;
22997
23723
  return changes > 0;
22998
23724
  }
@@ -23021,16 +23747,16 @@ function writeCompactionNote(db, args) {
23021
23747
  }).run();
23022
23748
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
23023
23749
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
23024
- and22(
23025
- eq32(agentMemory.projectId, args.projectId),
23750
+ and23(
23751
+ eq33(agentMemory.projectId, args.projectId),
23026
23752
  like2(agentMemory.key, `${sessionPrefix}%`)
23027
23753
  )
23028
23754
  ).orderBy(desc16(agentMemory.updatedAt)).all();
23029
23755
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
23030
23756
  if (stale.length > 0) {
23031
- tx.delete(agentMemory).where(sql11`${agentMemory.id} IN (${sql11.join(stale.map((s) => sql11`${s}`), sql11`, `)})`).run();
23757
+ tx.delete(agentMemory).where(sql13`${agentMemory.id} IN (${sql13.join(stale.map((s) => sql13`${s}`), sql13`, `)})`).run();
23032
23758
  }
23033
- const row = tx.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
23759
+ const row = tx.select().from(agentMemory).where(and23(eq33(agentMemory.projectId, args.projectId), eq33(agentMemory.key, key))).get();
23034
23760
  if (row) inserted = rowToDto2(row);
23035
23761
  });
23036
23762
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -23212,7 +23938,7 @@ var SessionRegistry = class {
23212
23938
  modelProvider: effectiveProvider,
23213
23939
  modelId: effectiveModelId,
23214
23940
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23215
- }).where(eq33(agentSessions.projectId, projectId)).run();
23941
+ }).where(eq34(agentSessions.projectId, projectId)).run();
23216
23942
  }
23217
23943
  const agent2 = createAeroSession({
23218
23944
  projectName,
@@ -23426,7 +24152,7 @@ ${lines.join("\n")}
23426
24152
  modelProvider: nextProvider,
23427
24153
  modelId: nextModelId,
23428
24154
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23429
- }).where(eq33(agentSessions.projectId, projectId)).run();
24155
+ }).where(eq34(agentSessions.projectId, projectId)).run();
23430
24156
  }
23431
24157
  /** Persist a session's transcript back to the DB. Call after any run settles. */
23432
24158
  save(projectName) {
@@ -23588,11 +24314,11 @@ ${lines.join("\n")}
23588
24314
  return id;
23589
24315
  }
23590
24316
  tryResolveProjectId(projectName) {
23591
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq33(projects.name, projectName)).get();
24317
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectName)).get();
23592
24318
  return row?.id;
23593
24319
  }
23594
24320
  loadRow(projectId) {
23595
- const row = this.opts.db.select().from(agentSessions).where(eq33(agentSessions.projectId, projectId)).get();
24321
+ const row = this.opts.db.select().from(agentSessions).where(eq34(agentSessions.projectId, projectId)).get();
23596
24322
  return row ?? null;
23597
24323
  }
23598
24324
  insertRow(params) {
@@ -23611,14 +24337,14 @@ ${lines.join("\n")}
23611
24337
  }
23612
24338
  updateRow(projectId, patch) {
23613
24339
  const now = (/* @__PURE__ */ new Date()).toISOString();
23614
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq33(agentSessions.projectId, projectId)).run();
24340
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq34(agentSessions.projectId, projectId)).run();
23615
24341
  }
23616
24342
  };
23617
24343
 
23618
24344
  // src/agent/agent-routes.ts
23619
- import { eq as eq34 } from "drizzle-orm";
24345
+ import { eq as eq35 } from "drizzle-orm";
23620
24346
  function resolveProject2(db, name) {
23621
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq34(projects.name, name)).get();
24347
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq35(projects.name, name)).get();
23622
24348
  if (!row) throw notFound("project", name);
23623
24349
  return row;
23624
24350
  }
@@ -23627,7 +24353,7 @@ function registerAgentRoutes(app, opts) {
23627
24353
  "/projects/:name/agent/transcript",
23628
24354
  async (request) => {
23629
24355
  const project = resolveProject2(opts.db, request.params.name);
23630
- const row = opts.db.select().from(agentSessions).where(eq34(agentSessions.projectId, project.id)).get();
24356
+ const row = opts.db.select().from(agentSessions).where(eq35(agentSessions.projectId, project.id)).get();
23631
24357
  if (!row) {
23632
24358
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
23633
24359
  }
@@ -23651,7 +24377,7 @@ function registerAgentRoutes(app, opts) {
23651
24377
  async (request) => {
23652
24378
  const project = resolveProject2(opts.db, request.params.name);
23653
24379
  opts.sessionRegistry.reset(project.name);
23654
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq34(agentSessions.projectId, project.id)).run();
24380
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq35(agentSessions.projectId, project.id)).run();
23655
24381
  return { status: "reset" };
23656
24382
  }
23657
24383
  );
@@ -24673,7 +25399,7 @@ async function createServer(opts) {
24673
25399
  intelligenceService,
24674
25400
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
24675
25401
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
24676
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq35(projects.id, projectId)).get();
25402
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq36(projects.id, projectId)).get();
24677
25403
  if (!project) return;
24678
25404
  sessionRegistry.queueFollowUp(project.name, {
24679
25405
  role: "user",
@@ -24833,7 +25559,7 @@ async function createServer(opts) {
24833
25559
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
24834
25560
  if (opts.config.apiKey) {
24835
25561
  const keyHash = hashApiKey(opts.config.apiKey);
24836
- const existing = opts.db.select().from(apiKeys).where(eq35(apiKeys.keyHash, keyHash)).get();
25562
+ const existing = opts.db.select().from(apiKeys).where(eq36(apiKeys.keyHash, keyHash)).get();
24837
25563
  if (!existing) {
24838
25564
  const prefix = opts.config.apiKey.slice(0, 12);
24839
25565
  opts.db.insert(apiKeys).values({
@@ -24885,7 +25611,7 @@ async function createServer(opts) {
24885
25611
  };
24886
25612
  const getDefaultApiKey = () => {
24887
25613
  if (!opts.config.apiKey) return void 0;
24888
- return opts.db.select().from(apiKeys).where(eq35(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
25614
+ return opts.db.select().from(apiKeys).where(eq36(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
24889
25615
  };
24890
25616
  const createPasswordSession = (reply) => {
24891
25617
  const key = getDefaultApiKey();
@@ -24942,12 +25668,12 @@ async function createServer(opts) {
24942
25668
  return reply.send({ authenticated: true });
24943
25669
  }
24944
25670
  if (apiKey) {
24945
- const key = opts.db.select().from(apiKeys).where(eq35(apiKeys.keyHash, hashApiKey(apiKey))).get();
25671
+ const key = opts.db.select().from(apiKeys).where(eq36(apiKeys.keyHash, hashApiKey(apiKey))).get();
24946
25672
  if (!key || key.revokedAt) {
24947
25673
  const err2 = authInvalid();
24948
25674
  return reply.status(err2.statusCode).send(err2.toJSON());
24949
25675
  }
24950
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq35(apiKeys.id, key.id)).run();
25676
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq36(apiKeys.id, key.id)).run();
24951
25677
  const sessionId = createSession(key.id);
24952
25678
  reply.header("set-cookie", serializeSessionCookie({
24953
25679
  name: SESSION_COOKIE_NAME,
@@ -25131,6 +25857,17 @@ async function createServer(opts) {
25131
25857
  wordpressConnectionStore,
25132
25858
  ga4CredentialStore,
25133
25859
  cloudRunCredentialStore,
25860
+ onTrafficSynced: (event) => {
25861
+ trackEvent("traffic.synced", {
25862
+ status: event.status,
25863
+ sourceType: event.sourceType,
25864
+ sourceId: event.sourceId,
25865
+ pulledEvents: event.pulledEvents,
25866
+ crawlerHits: event.crawlerHits,
25867
+ aiReferralHits: event.aiReferralHits,
25868
+ durationMs: event.durationMs
25869
+ }, event.errorCode ? { errorCode: event.errorCode } : void 0);
25870
+ },
25134
25871
  onRunCreated: (runId, projectId, providers2, location) => {
25135
25872
  jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
25136
25873
  app.log.error({ runId, err }, "Job runner failed");