@ainyc/canonry 2.9.0 → 2.10.3

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.
@@ -6,6 +6,7 @@ import {
6
6
  ApiClient,
7
7
  AppError,
8
8
  CcReleaseSyncStatuses,
9
+ CitationStates,
9
10
  MemorySources,
10
11
  RunKinds,
11
12
  RunStatuses,
@@ -16,6 +17,7 @@ import {
16
17
  authInvalid,
17
18
  authRequired,
18
19
  brandKeyFromText,
20
+ buildRunErrorFromMessages,
19
21
  categorizeSource,
20
22
  categoryLabel,
21
23
  competitorBatchRequestSchema,
@@ -36,6 +38,7 @@ import {
36
38
  normalizeProjectDomain,
37
39
  notFound,
38
40
  notImplemented,
41
+ parseRunError,
39
42
  parseWindow,
40
43
  projectConfigSchema,
41
44
  projectUpsertRequestSchema,
@@ -45,13 +48,14 @@ import {
45
48
  runTriggerRequestSchema,
46
49
  saveConfigPatch,
47
50
  scheduleUpsertRequestSchema,
51
+ serializeRunError,
48
52
  snapshotRequestSchema,
49
53
  unsupportedKind,
50
54
  validationError,
51
55
  visibilityStateFromAnswerMentioned,
52
56
  windowCutoff,
53
57
  wordpressEnvSchema
54
- } from "./chunk-FPZUQADO.js";
58
+ } from "./chunk-Z3BWDCBJ.js";
55
59
  import {
56
60
  IntelligenceService,
57
61
  agentMemory,
@@ -62,6 +66,10 @@ import {
62
66
  backlinkSummaries,
63
67
  bingCoverageSnapshots,
64
68
  bingUrlInspections,
69
+ buildContentGapRows,
70
+ buildContentSourceRows,
71
+ buildContentTargetRows,
72
+ buildInventory,
65
73
  ccReleaseSyncs,
66
74
  competitors,
67
75
  createLogger,
@@ -76,6 +84,7 @@ import {
76
84
  gscUrlInspections,
77
85
  healthSnapshots,
78
86
  insights,
87
+ isBlogShapedQuery,
79
88
  keywords,
80
89
  notifications,
81
90
  parseJsonColumn,
@@ -84,7 +93,7 @@ import {
84
93
  runs,
85
94
  schedules,
86
95
  usageCounters
87
- } from "./chunk-PYHANJ3B.js";
96
+ } from "./chunk-UM6RDSRJ.js";
88
97
 
89
98
  // src/telemetry.ts
90
99
  import crypto from "crypto";
@@ -164,7 +173,7 @@ import crypto28 from "crypto";
164
173
  import fs12 from "fs";
165
174
  import path14 from "path";
166
175
  import { fileURLToPath as fileURLToPath2 } from "url";
167
- import { eq as eq30 } from "drizzle-orm";
176
+ import { eq as eq31 } from "drizzle-orm";
168
177
  import Fastify from "fastify";
169
178
 
170
179
  // ../api-routes/src/auth.ts
@@ -1043,7 +1052,7 @@ async function runRoutes(app, opts) {
1043
1052
  const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
1044
1053
  if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
1045
1054
  const now = (/* @__PURE__ */ new Date()).toISOString();
1046
- app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: "Cancelled by user" }).where(eq7(runs.id, run.id)).run();
1055
+ app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(eq7(runs.id, run.id)).run();
1047
1056
  writeAuditLog(app.db, {
1048
1057
  projectId: run.projectId,
1049
1058
  actor: "api",
@@ -1080,7 +1089,7 @@ function formatRun(row) {
1080
1089
  location: row.location,
1081
1090
  startedAt: row.startedAt,
1082
1091
  finishedAt: row.finishedAt,
1083
- error: row.error,
1092
+ error: parseRunError(row.error),
1084
1093
  createdAt: row.createdAt
1085
1094
  };
1086
1095
  }
@@ -2255,6 +2264,20 @@ function buildCategoryCounts(counts) {
2255
2264
 
2256
2265
  // ../api-routes/src/intelligence.ts
2257
2266
  import { eq as eq11, desc as desc4, and as and2 } from "drizzle-orm";
2267
+ function emptyHealthSnapshot(projectId) {
2268
+ return {
2269
+ id: `no-data:${projectId}`,
2270
+ projectId,
2271
+ runId: null,
2272
+ overallCitedRate: 0,
2273
+ totalPairs: 0,
2274
+ citedPairs: 0,
2275
+ providerBreakdown: {},
2276
+ createdAt: "",
2277
+ status: "no-data",
2278
+ reason: "no-runs-yet"
2279
+ };
2280
+ }
2258
2281
  function mapInsightRow(r) {
2259
2282
  return {
2260
2283
  id: r.id,
@@ -2280,7 +2303,8 @@ function mapHealthRow(r) {
2280
2303
  totalPairs: r.totalPairs,
2281
2304
  citedPairs: r.citedPairs,
2282
2305
  providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
2283
- createdAt: r.createdAt
2306
+ createdAt: r.createdAt,
2307
+ status: "ready"
2284
2308
  };
2285
2309
  }
2286
2310
  async function intelligenceRoutes(app) {
@@ -2316,7 +2340,7 @@ async function intelligenceRoutes(app) {
2316
2340
  const project = resolveProject(app.db, request.params.name);
2317
2341
  const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
2318
2342
  if (!row) {
2319
- throw notFound("Health data for project", request.params.name);
2343
+ return reply.send(emptyHealthSnapshot(project.id));
2320
2344
  }
2321
2345
  return reply.send(mapHealthRow(row));
2322
2346
  });
@@ -2329,6 +2353,337 @@ async function intelligenceRoutes(app) {
2329
2353
  });
2330
2354
  }
2331
2355
 
2356
+ // ../api-routes/src/content-data.ts
2357
+ import { and as and3, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
2358
+ var RECENT_RUNS_WINDOW = 5;
2359
+ function loadOrchestratorInput(db, project) {
2360
+ const projectId = project.id;
2361
+ const ownDomain = normalizeDomain(project.canonicalDomain);
2362
+ const ownedDomains = parseJsonColumn(project.ownedDomains, []);
2363
+ const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
2364
+ const trackedKeywords = listKeywords(db, projectId);
2365
+ const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
2366
+ const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
2367
+ const competitorSet = new Set(trackedCompetitors);
2368
+ const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
2369
+ const latestRunId = recentRunIds[0] ?? "";
2370
+ const latestRunTimestamp = latestRunId ? lookupRunTimestamp(db, latestRunId) : "";
2371
+ const candidateQueries = buildCandidateQueries({
2372
+ db,
2373
+ projectId,
2374
+ candidateQueryStrings,
2375
+ recentRunIds,
2376
+ latestRunId,
2377
+ ourDomains,
2378
+ competitorSet
2379
+ });
2380
+ const inventory = buildInventory({
2381
+ gscPages: listGscPagesForProject(db, projectId),
2382
+ ga4LandingPages: listGa4LandingPagesForProject(db, projectId),
2383
+ sitemapUrls: [],
2384
+ wpPosts: []
2385
+ });
2386
+ const gaTrafficByPage = buildGaTrafficByPage(db, projectId);
2387
+ const totalAiReferralSessions = sumAiReferralSessions(db, projectId);
2388
+ return {
2389
+ projectId,
2390
+ ownDomain,
2391
+ competitors: trackedCompetitors,
2392
+ candidateQueries,
2393
+ inventory,
2394
+ wpSchemaAudit: /* @__PURE__ */ new Map(),
2395
+ gaTrafficByPage,
2396
+ totalAiReferralSessions,
2397
+ latestRunId,
2398
+ latestRunTimestamp,
2399
+ inProgressActions: /* @__PURE__ */ new Map()
2400
+ };
2401
+ }
2402
+ function listKeywords(db, projectId) {
2403
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, projectId)).all();
2404
+ return rows.map((r) => r.text);
2405
+ }
2406
+ function listCompetitorDomains(db, projectId) {
2407
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, projectId)).all();
2408
+ return rows.map((r) => r.domain);
2409
+ }
2410
+ function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2411
+ const rows = db.select({ id: runs.id }).from(runs).where(
2412
+ and3(
2413
+ eq12(runs.projectId, projectId),
2414
+ eq12(runs.kind, RunKinds["answer-visibility"]),
2415
+ // Queued/running/failed/cancelled runs may have partial or no
2416
+ // snapshots; including them risks pointing latestRunId at a run with
2417
+ // no usable evidence.
2418
+ inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2419
+ )
2420
+ ).orderBy(desc5(runs.createdAt)).limit(limit).all();
2421
+ return rows.map((r) => r.id);
2422
+ }
2423
+ function lookupRunTimestamp(db, runId) {
2424
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq12(runs.id, runId)).get();
2425
+ return row?.createdAt ?? "";
2426
+ }
2427
+ function listGscPagesForProject(db, projectId) {
2428
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq12(gscSearchData.projectId, projectId)).all();
2429
+ return rows.map((r) => r.page);
2430
+ }
2431
+ function listGa4LandingPagesForProject(db, projectId) {
2432
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
2433
+ return rows.map((r) => r.landingPage);
2434
+ }
2435
+ function buildGaTrafficByPage(db, projectId) {
2436
+ const rows = db.select({
2437
+ landingPage: gaTrafficSnapshots.landingPage,
2438
+ sessions: gaTrafficSnapshots.sessions
2439
+ }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
2440
+ const map = /* @__PURE__ */ new Map();
2441
+ for (const row of rows) {
2442
+ const path15 = extractPath(row.landingPage);
2443
+ if (!path15) continue;
2444
+ map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
2445
+ }
2446
+ return map;
2447
+ }
2448
+ function sumAiReferralSessions(db, projectId) {
2449
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq12(gaAiReferrals.projectId, projectId)).all();
2450
+ return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2451
+ }
2452
+ function buildCandidateQueries(opts) {
2453
+ if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2454
+ return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2455
+ }
2456
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, opts.projectId)).all();
2457
+ const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2458
+ const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2459
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2460
+ const snapshotsByKeyword = /* @__PURE__ */ new Map();
2461
+ for (const row of snapshotRows) {
2462
+ const list = snapshotsByKeyword.get(row.keywordId) ?? [];
2463
+ list.push(row);
2464
+ snapshotsByKeyword.set(row.keywordId, list);
2465
+ }
2466
+ const gscRows = opts.db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, opts.projectId)).all();
2467
+ const gscByQuery = aggregateGscByQuery(gscRows);
2468
+ return opts.candidateQueryStrings.map((query) => {
2469
+ const keywordId = keywordIdByText.get(query);
2470
+ const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
2471
+ const gsc = gscByQuery.get(query) ?? null;
2472
+ return aggregateCandidate({
2473
+ query,
2474
+ snapshots: snaps,
2475
+ gsc,
2476
+ ourDomains: opts.ourDomains,
2477
+ competitorSet: opts.competitorSet,
2478
+ latestRunId: opts.latestRunId
2479
+ });
2480
+ });
2481
+ }
2482
+ function aggregateGscByQuery(rows) {
2483
+ const byQuery = /* @__PURE__ */ new Map();
2484
+ for (const r of rows) {
2485
+ const existing = byQuery.get(r.query);
2486
+ const candidate = {
2487
+ // GSC stores `page` as a full URL for url-prefix properties; normalize to
2488
+ // a path so it can be joined against `gaTrafficByPage` (which is keyed by
2489
+ // path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
2490
+ // of whether the page is sourced from GSC or from inventory.
2491
+ page: extractPath(r.page),
2492
+ position: Number(r.position) || 0,
2493
+ impressions: r.impressions,
2494
+ clicks: r.clicks,
2495
+ ctr: Number(r.ctr) || 0
2496
+ };
2497
+ if (!existing) {
2498
+ byQuery.set(r.query, candidate);
2499
+ continue;
2500
+ }
2501
+ if (candidate.impressions > existing.impressions) {
2502
+ byQuery.set(r.query, candidate);
2503
+ }
2504
+ }
2505
+ return byQuery;
2506
+ }
2507
+ function aggregateCandidate(opts) {
2508
+ const totalSnaps = opts.snapshots.length;
2509
+ if (totalSnaps === 0) {
2510
+ return {
2511
+ ...emptyCandidate(opts.query),
2512
+ gscPage: opts.gsc?.page ?? null,
2513
+ gscPosition: opts.gsc ? opts.gsc.position : null,
2514
+ gscImpressions: opts.gsc?.impressions ?? 0,
2515
+ gscClicks: opts.gsc?.clicks ?? 0,
2516
+ gscCtr: opts.gsc?.ctr ?? 0
2517
+ };
2518
+ }
2519
+ const citedCount = opts.snapshots.filter((s) => s.citationState === CitationStates.cited).length;
2520
+ const ourCitedRate = citedCount / totalSnaps;
2521
+ const recentMissRate = 1 - ourCitedRate;
2522
+ const competitorTally = /* @__PURE__ */ new Map();
2523
+ const competitorGroundingTally = /* @__PURE__ */ new Map();
2524
+ const ourGroundingTally = /* @__PURE__ */ new Map();
2525
+ let ourCitedInLatestRun = false;
2526
+ for (const snap of opts.snapshots) {
2527
+ const isLatestRun = snap.runId === opts.latestRunId;
2528
+ const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
2529
+ for (const domain of competitorOverlap) {
2530
+ const normalized = normalizeDomain(domain);
2531
+ if (!opts.competitorSet.has(normalized)) continue;
2532
+ competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
2533
+ }
2534
+ const grounding = extractGroundingSources(snap.rawResponse);
2535
+ for (const g of grounding) {
2536
+ const domain = normalizeDomain(extractHostFromUri(g.uri));
2537
+ if (!domain) continue;
2538
+ if (opts.ourDomains.has(domain)) {
2539
+ if (isLatestRun) ourCitedInLatestRun = true;
2540
+ recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
2541
+ continue;
2542
+ }
2543
+ if (!opts.competitorSet.has(domain)) continue;
2544
+ recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
2545
+ }
2546
+ }
2547
+ return {
2548
+ query: opts.query,
2549
+ gscPage: opts.gsc?.page ?? null,
2550
+ gscPosition: opts.gsc ? opts.gsc.position : null,
2551
+ gscImpressions: opts.gsc?.impressions ?? 0,
2552
+ gscClicks: opts.gsc?.clicks ?? 0,
2553
+ gscCtr: opts.gsc?.ctr ?? 0,
2554
+ ourCitedRate,
2555
+ ourCitedInLatestRun,
2556
+ competitorDomains: Array.from(competitorTally.keys()),
2557
+ competitorCitationCount: Array.from(competitorTally.values()).reduce((a, b) => a + b, 0),
2558
+ recentMissRate,
2559
+ ourGroundingUrls: Array.from(ourGroundingTally.values()),
2560
+ competitorGroundingUrls: Array.from(competitorGroundingTally.values()),
2561
+ runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
2562
+ };
2563
+ }
2564
+ function recordGroundingHit(tally, g, domain, provider) {
2565
+ const existing = tally.get(g.uri);
2566
+ if (existing) {
2567
+ existing.citationCount += 1;
2568
+ if (provider && !existing.providers.includes(provider)) {
2569
+ existing.providers.push(provider);
2570
+ }
2571
+ return;
2572
+ }
2573
+ tally.set(g.uri, {
2574
+ uri: g.uri,
2575
+ title: g.title,
2576
+ domain,
2577
+ citationCount: 1,
2578
+ providers: provider ? [provider] : []
2579
+ });
2580
+ }
2581
+ function emptyCandidate(query) {
2582
+ return {
2583
+ query,
2584
+ gscPage: null,
2585
+ gscPosition: null,
2586
+ gscImpressions: 0,
2587
+ gscClicks: 0,
2588
+ gscCtr: 0,
2589
+ ourCitedRate: 0,
2590
+ ourCitedInLatestRun: false,
2591
+ competitorDomains: [],
2592
+ competitorCitationCount: 0,
2593
+ recentMissRate: 0,
2594
+ ourGroundingUrls: [],
2595
+ competitorGroundingUrls: [],
2596
+ runsOfHistory: 0
2597
+ };
2598
+ }
2599
+ function extractGroundingSources(rawResponse) {
2600
+ if (!rawResponse) return [];
2601
+ try {
2602
+ const parsed = JSON.parse(rawResponse);
2603
+ if (parsed && typeof parsed === "object" && "groundingSources" in parsed) {
2604
+ const grounding = parsed.groundingSources;
2605
+ if (Array.isArray(grounding)) {
2606
+ return grounding.filter(
2607
+ (g) => typeof g === "object" && g !== null && typeof g.uri === "string"
2608
+ ).map((g) => ({ uri: g.uri, title: g.title ?? "" }));
2609
+ }
2610
+ }
2611
+ } catch {
2612
+ }
2613
+ return [];
2614
+ }
2615
+ function extractHostFromUri(uri) {
2616
+ try {
2617
+ return new URL(uri).hostname;
2618
+ } catch {
2619
+ return "";
2620
+ }
2621
+ }
2622
+ function normalizeDomain(domain) {
2623
+ return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2624
+ }
2625
+ function extractPath(url) {
2626
+ if (!url) return "";
2627
+ const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
2628
+ const path15 = match ? match[1] : url.trim();
2629
+ const stripped = path15.replace(/\/+$/, "");
2630
+ return stripped || "/";
2631
+ }
2632
+
2633
+ // ../api-routes/src/content.ts
2634
+ async function contentRoutes(app) {
2635
+ app.get("/projects/:name/content/targets", async (request) => {
2636
+ const project = resolveProject(app.db, request.params.name);
2637
+ const includeInProgress = request.query["include-in-progress"] === "true";
2638
+ const limit = parseLimitParam(request.query.limit);
2639
+ const input = loadOrchestratorInput(app.db, project);
2640
+ let rows = buildContentTargetRows(input);
2641
+ if (!includeInProgress) {
2642
+ rows = rows.filter((r) => r.existingAction === null);
2643
+ }
2644
+ if (limit !== void 0) {
2645
+ rows = rows.slice(0, limit);
2646
+ }
2647
+ const response = {
2648
+ targets: rows,
2649
+ contextMetrics: {
2650
+ totalAiReferralSessions: input.totalAiReferralSessions,
2651
+ latestRunId: input.latestRunId,
2652
+ runTimestamp: input.latestRunTimestamp
2653
+ }
2654
+ };
2655
+ return response;
2656
+ });
2657
+ app.get("/projects/:name/content/sources", async (request) => {
2658
+ const project = resolveProject(app.db, request.params.name);
2659
+ const input = loadOrchestratorInput(app.db, project);
2660
+ const rows = buildContentSourceRows(input);
2661
+ const response = {
2662
+ sources: rows,
2663
+ latestRunId: input.latestRunId
2664
+ };
2665
+ return response;
2666
+ });
2667
+ app.get("/projects/:name/content/gaps", async (request) => {
2668
+ const project = resolveProject(app.db, request.params.name);
2669
+ const input = loadOrchestratorInput(app.db, project);
2670
+ const rows = buildContentGapRows(input);
2671
+ const response = {
2672
+ gaps: rows,
2673
+ latestRunId: input.latestRunId
2674
+ };
2675
+ return response;
2676
+ });
2677
+ }
2678
+ function parseLimitParam(raw) {
2679
+ if (raw === void 0) return void 0;
2680
+ const parsed = Number(raw);
2681
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
2682
+ throw validationError('"limit" must be a non-negative integer');
2683
+ }
2684
+ return parsed;
2685
+ }
2686
+
2332
2687
  // ../api-routes/src/openapi.ts
2333
2688
  var stringSchema = { type: "string" };
2334
2689
  var booleanSchema = { type: "boolean" };
@@ -4533,10 +4888,11 @@ var routeCatalog = [
4533
4888
  method: "get",
4534
4889
  path: "/api/v1/projects/{name}/health/latest",
4535
4890
  summary: "Get latest health snapshot",
4891
+ description: 'Returns the latest health snapshot. Always 200 once the project exists: when no snapshot exists yet (newly-created project, or only failed runs), the response carries `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics. Real snapshots carry `status: "ready"`.',
4536
4892
  tags: ["intelligence"],
4537
4893
  parameters: [nameParameter],
4538
4894
  responses: {
4539
- 200: { description: "Health snapshot returned." },
4895
+ 200: { description: "Health snapshot or no-data sentinel returned." },
4540
4896
  404: { description: "Project not found." }
4541
4897
  }
4542
4898
  },
@@ -4554,6 +4910,48 @@ var routeCatalog = [
4554
4910
  404: { description: "Project not found." }
4555
4911
  }
4556
4912
  },
4913
+ // Content opportunity engine
4914
+ {
4915
+ method: "get",
4916
+ path: "/api/v1/projects/{name}/content/targets",
4917
+ summary: "Ranked, action-typed content opportunities",
4918
+ description: "Returns the canonical opportunity list. Each row is `{query, action, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, existingAction?}`. Hides rows with in-progress actions by default; pass `?include-in-progress=true` to include them annotated.",
4919
+ tags: ["content"],
4920
+ parameters: [
4921
+ nameParameter,
4922
+ { name: "limit", in: "query", description: "Max rows returned.", schema: stringSchema },
4923
+ { name: "include-in-progress", in: "query", description: "Include rows with in-flight tracked actions.", schema: stringSchema }
4924
+ ],
4925
+ responses: {
4926
+ 200: { description: "Targets returned." },
4927
+ 400: { description: "Invalid limit." },
4928
+ 404: { description: "Project not found." }
4929
+ }
4930
+ },
4931
+ {
4932
+ method: "get",
4933
+ path: "/api/v1/projects/{name}/content/sources",
4934
+ summary: "URL-level competitive grounding-source map per query",
4935
+ description: "Returns one row per blog-shaped query containing the grounding URLs the LLM cited. Distinguishes our domain (isOurDomain) from competitor URLs (isCompetitor). Pure DB read \u2014 canonry surfaces URLs but never fetches them.",
4936
+ tags: ["content"],
4937
+ parameters: [nameParameter],
4938
+ responses: {
4939
+ 200: { description: "Sources returned." },
4940
+ 404: { description: "Project not found." }
4941
+ }
4942
+ },
4943
+ {
4944
+ method: "get",
4945
+ path: "/api/v1/projects/{name}/content/gaps",
4946
+ summary: "Queries where competitors are cited but you are not",
4947
+ description: "Returns gap rows ranked by miss rate then by competitor count. Excludes queries with no competitor citations and queries where our cited rate is 100%.",
4948
+ tags: ["content"],
4949
+ parameters: [nameParameter],
4950
+ responses: {
4951
+ 200: { description: "Gaps returned." },
4952
+ 404: { description: "Project not found." }
4953
+ }
4954
+ },
4557
4955
  {
4558
4956
  method: "get",
4559
4957
  path: "/api/v1/backlinks/status",
@@ -5012,7 +5410,7 @@ async function telemetryRoutes(app, opts) {
5012
5410
 
5013
5411
  // ../api-routes/src/schedules.ts
5014
5412
  import crypto11 from "crypto";
5015
- import { eq as eq12 } from "drizzle-orm";
5413
+ import { eq as eq13 } from "drizzle-orm";
5016
5414
  async function scheduleRoutes(app, opts) {
5017
5415
  app.put("/projects/:name/schedule", async (request, reply) => {
5018
5416
  const project = resolveProject(app.db, request.params.name);
@@ -5055,7 +5453,7 @@ async function scheduleRoutes(app, opts) {
5055
5453
  }
5056
5454
  const now = (/* @__PURE__ */ new Date()).toISOString();
5057
5455
  const enabledInt = enabled === false ? 0 : 1;
5058
- const existing = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5456
+ const existing = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5059
5457
  if (existing) {
5060
5458
  app.db.update(schedules).set({
5061
5459
  cronExpr,
@@ -5064,7 +5462,7 @@ async function scheduleRoutes(app, opts) {
5064
5462
  providers: JSON.stringify(providers),
5065
5463
  enabled: enabledInt,
5066
5464
  updatedAt: now
5067
- }).where(eq12(schedules.id, existing.id)).run();
5465
+ }).where(eq13(schedules.id, existing.id)).run();
5068
5466
  } else {
5069
5467
  app.db.insert(schedules).values({
5070
5468
  id: crypto11.randomUUID(),
@@ -5086,12 +5484,12 @@ async function scheduleRoutes(app, opts) {
5086
5484
  diff: { cronExpr, preset, timezone, providers }
5087
5485
  });
5088
5486
  opts.onScheduleUpdated?.("upsert", project.id);
5089
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5487
+ const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5090
5488
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
5091
5489
  });
5092
5490
  app.get("/projects/:name/schedule", async (request, reply) => {
5093
5491
  const project = resolveProject(app.db, request.params.name);
5094
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5492
+ const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5095
5493
  if (!schedule) {
5096
5494
  throw notFound("Schedule", request.params.name);
5097
5495
  }
@@ -5099,11 +5497,11 @@ async function scheduleRoutes(app, opts) {
5099
5497
  });
5100
5498
  app.delete("/projects/:name/schedule", async (request, reply) => {
5101
5499
  const project = resolveProject(app.db, request.params.name);
5102
- const schedule = app.db.select().from(schedules).where(eq12(schedules.projectId, project.id)).get();
5500
+ const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
5103
5501
  if (!schedule) {
5104
5502
  throw notFound("Schedule", request.params.name);
5105
5503
  }
5106
- app.db.delete(schedules).where(eq12(schedules.id, schedule.id)).run();
5504
+ app.db.delete(schedules).where(eq13(schedules.id, schedule.id)).run();
5107
5505
  writeAuditLog(app.db, {
5108
5506
  projectId: project.id,
5109
5507
  actor: "api",
@@ -5133,7 +5531,7 @@ function formatSchedule(row) {
5133
5531
 
5134
5532
  // ../api-routes/src/notifications.ts
5135
5533
  import crypto12 from "crypto";
5136
- import { eq as eq13 } from "drizzle-orm";
5534
+ import { eq as eq14 } from "drizzle-orm";
5137
5535
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
5138
5536
  async function notificationRoutes(app) {
5139
5537
  app.get("/notifications/events", async (_request, reply) => {
@@ -5172,22 +5570,22 @@ async function notificationRoutes(app) {
5172
5570
  diff: { channel, ...redactNotificationUrl(url), events }
5173
5571
  });
5174
5572
  return reply.status(201).send({
5175
- ...formatNotification(app.db.select().from(notifications).where(eq13(notifications.id, id)).get()),
5573
+ ...formatNotification(app.db.select().from(notifications).where(eq14(notifications.id, id)).get()),
5176
5574
  webhookSecret
5177
5575
  });
5178
5576
  });
5179
5577
  app.get("/projects/:name/notifications", async (request, reply) => {
5180
5578
  const project = resolveProject(app.db, request.params.name);
5181
- const rows = app.db.select().from(notifications).where(eq13(notifications.projectId, project.id)).all();
5579
+ const rows = app.db.select().from(notifications).where(eq14(notifications.projectId, project.id)).all();
5182
5580
  return reply.send(rows.map(formatNotification));
5183
5581
  });
5184
5582
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
5185
5583
  const project = resolveProject(app.db, request.params.name);
5186
- const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
5584
+ const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
5187
5585
  if (!notification || notification.projectId !== project.id) {
5188
5586
  throw notFound("Notification", request.params.id);
5189
5587
  }
5190
- app.db.delete(notifications).where(eq13(notifications.id, notification.id)).run();
5588
+ app.db.delete(notifications).where(eq14(notifications.id, notification.id)).run();
5191
5589
  writeAuditLog(app.db, {
5192
5590
  projectId: project.id,
5193
5591
  actor: "api",
@@ -5199,7 +5597,7 @@ async function notificationRoutes(app) {
5199
5597
  });
5200
5598
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
5201
5599
  const project = resolveProject(app.db, request.params.name);
5202
- const notification = app.db.select().from(notifications).where(eq13(notifications.id, request.params.id)).get();
5600
+ const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
5203
5601
  if (!notification || notification.projectId !== project.id) {
5204
5602
  throw notFound("Notification", request.params.id);
5205
5603
  }
@@ -5252,7 +5650,7 @@ function formatNotification(row) {
5252
5650
 
5253
5651
  // ../api-routes/src/google.ts
5254
5652
  import crypto14 from "crypto";
5255
- import { eq as eq14, and as and3, desc as desc5, sql as sql3 } from "drizzle-orm";
5653
+ import { eq as eq15, and as and4, desc as desc6, sql as sql3 } from "drizzle-orm";
5256
5654
 
5257
5655
  // ../integration-google/src/constants.ts
5258
5656
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6356,20 +6754,20 @@ async function googleRoutes(app, opts) {
6356
6754
  if (opts.onGscSyncRequested) {
6357
6755
  opts.onGscSyncRequested(runId, project.id, { days, full });
6358
6756
  }
6359
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
6757
+ const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
6360
6758
  return run;
6361
6759
  });
6362
6760
  app.get("/projects/:name/google/gsc/performance", async (request) => {
6363
6761
  const project = resolveProject(app.db, request.params.name);
6364
6762
  const { startDate, endDate, query, page, limit } = request.query;
6365
6763
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
6366
- const conditions = [eq14(gscSearchData.projectId, project.id)];
6764
+ const conditions = [eq15(gscSearchData.projectId, project.id)];
6367
6765
  if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
6368
6766
  else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
6369
6767
  if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
6370
6768
  if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
6371
6769
  if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
6372
- const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc5(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
6770
+ const rows = app.db.select().from(gscSearchData).where(and4(...conditions)).orderBy(desc6(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
6373
6771
  return rows.map((r) => ({
6374
6772
  date: r.date,
6375
6773
  query: r.query,
@@ -6441,9 +6839,9 @@ async function googleRoutes(app, opts) {
6441
6839
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
6442
6840
  const project = resolveProject(app.db, request.params.name);
6443
6841
  const { url, limit } = request.query;
6444
- const conditions = [eq14(gscUrlInspections.projectId, project.id)];
6445
- if (url) conditions.push(eq14(gscUrlInspections.url, url));
6446
- const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc5(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
6842
+ const conditions = [eq15(gscUrlInspections.projectId, project.id)];
6843
+ if (url) conditions.push(eq15(gscUrlInspections.url, url));
6844
+ const rows = app.db.select().from(gscUrlInspections).where(and4(...conditions)).orderBy(desc6(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
6447
6845
  return rows.map((r) => ({
6448
6846
  id: r.id,
6449
6847
  url: r.url,
@@ -6462,7 +6860,7 @@ async function googleRoutes(app, opts) {
6462
6860
  });
6463
6861
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
6464
6862
  const project = resolveProject(app.db, request.params.name);
6465
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
6863
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
6466
6864
  const byUrl = /* @__PURE__ */ new Map();
6467
6865
  for (const row of allInspections) {
6468
6866
  const existing = byUrl.get(row.url);
@@ -6490,7 +6888,7 @@ async function googleRoutes(app, opts) {
6490
6888
  });
6491
6889
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
6492
6890
  const project = resolveProject(app.db, request.params.name);
6493
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
6891
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
6494
6892
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
6495
6893
  const latestByUrl = /* @__PURE__ */ new Map();
6496
6894
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -6587,7 +6985,7 @@ async function googleRoutes(app, opts) {
6587
6985
  const project = resolveProject(app.db, request.params.name);
6588
6986
  const parsed = parseInt(request.query.limit ?? "90", 10);
6589
6987
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
6590
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq14(gscCoverageSnapshots.projectId, project.id)).orderBy(desc5(gscCoverageSnapshots.date)).limit(limit).all();
6988
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, project.id)).orderBy(desc6(gscCoverageSnapshots.date)).limit(limit).all();
6591
6989
  return rows.map((r) => ({
6592
6990
  date: r.date,
6593
6991
  indexed: r.indexed,
@@ -6647,7 +7045,7 @@ async function googleRoutes(app, opts) {
6647
7045
  if (opts.onInspectSitemapRequested) {
6648
7046
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
6649
7047
  }
6650
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7048
+ const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
6651
7049
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
6652
7050
  });
6653
7051
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -6674,7 +7072,7 @@ async function googleRoutes(app, opts) {
6674
7072
  if (opts.onInspectSitemapRequested) {
6675
7073
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
6676
7074
  }
6677
- const run = app.db.select().from(runs).where(eq14(runs.id, runId)).get();
7075
+ const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
6678
7076
  return run;
6679
7077
  });
6680
7078
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -6721,7 +7119,7 @@ async function googleRoutes(app, opts) {
6721
7119
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
6722
7120
  let urlsToNotify = request.body?.urls ?? [];
6723
7121
  if (request.body?.allUnindexed) {
6724
- const allInspections = app.db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, project.id)).orderBy(desc5(gscUrlInspections.inspectedAt)).all();
7122
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
6725
7123
  const latestByUrl = /* @__PURE__ */ new Map();
6726
7124
  for (const row of allInspections) {
6727
7125
  if (!latestByUrl.has(row.url)) {
@@ -6792,7 +7190,7 @@ async function googleRoutes(app, opts) {
6792
7190
 
6793
7191
  // ../api-routes/src/bing.ts
6794
7192
  import crypto15 from "crypto";
6795
- import { eq as eq15, and as and4, desc as desc6 } from "drizzle-orm";
7193
+ import { eq as eq16, and as and5, desc as desc7 } from "drizzle-orm";
6796
7194
 
6797
7195
  // ../integration-bing/src/constants.ts
6798
7196
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -7105,7 +7503,7 @@ async function bingRoutes(app, opts) {
7105
7503
  const store = requireConnectionStore();
7106
7504
  const project = resolveProject(app.db, request.params.name);
7107
7505
  requireConnection(store, project.canonicalDomain);
7108
- const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7506
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
7109
7507
  const latestByUrl = /* @__PURE__ */ new Map();
7110
7508
  const definitiveByUrl = /* @__PURE__ */ new Map();
7111
7509
  for (const row of allInspections) {
@@ -7194,7 +7592,7 @@ async function bingRoutes(app, opts) {
7194
7592
  const project = resolveProject(app.db, request.params.name);
7195
7593
  const parsed = parseInt(request.query.limit ?? "90", 10);
7196
7594
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7197
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq15(bingCoverageSnapshots.projectId, project.id)).orderBy(desc6(bingCoverageSnapshots.date)).limit(limit).all();
7595
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, project.id)).orderBy(desc7(bingCoverageSnapshots.date)).limit(limit).all();
7198
7596
  return rows.map((r) => ({
7199
7597
  date: r.date,
7200
7598
  indexed: r.indexed,
@@ -7206,8 +7604,8 @@ async function bingRoutes(app, opts) {
7206
7604
  requireConnectionStore();
7207
7605
  const project = resolveProject(app.db, request.params.name);
7208
7606
  const { url, limit } = request.query;
7209
- const whereClause = url ? and4(eq15(bingUrlInspections.projectId, project.id), eq15(bingUrlInspections.url, url)) : eq15(bingUrlInspections.projectId, project.id);
7210
- const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc6(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
7607
+ const whereClause = url ? and5(eq16(bingUrlInspections.projectId, project.id), eq16(bingUrlInspections.url, url)) : eq16(bingUrlInspections.projectId, project.id);
7608
+ const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc7(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
7211
7609
  return filtered.map((r) => ({
7212
7610
  id: r.id,
7213
7611
  url: r.url,
@@ -7296,7 +7694,7 @@ async function bingRoutes(app, opts) {
7296
7694
  anchorCount: result.AnchorCount ?? null,
7297
7695
  discoveryDate
7298
7696
  }).run();
7299
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq15(runs.id, runId)).run();
7697
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq16(runs.id, runId)).run();
7300
7698
  return {
7301
7699
  id,
7302
7700
  url,
@@ -7312,7 +7710,7 @@ async function bingRoutes(app, opts) {
7312
7710
  } catch (e) {
7313
7711
  const msg = e instanceof Error ? e.message : String(e);
7314
7712
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
7315
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(runs.id, runId)).run();
7713
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
7316
7714
  throw e;
7317
7715
  }
7318
7716
  });
@@ -7339,7 +7737,7 @@ async function bingRoutes(app, opts) {
7339
7737
  } else {
7340
7738
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
7341
7739
  }
7342
- const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
7740
+ const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7343
7741
  return run;
7344
7742
  });
7345
7743
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -7351,7 +7749,7 @@ async function bingRoutes(app, opts) {
7351
7749
  }
7352
7750
  let urlsToSubmit = request.body?.urls ?? [];
7353
7751
  if (request.body?.allUnindexed) {
7354
- const allInspections = app.db.select().from(bingUrlInspections).where(eq15(bingUrlInspections.projectId, project.id)).orderBy(desc6(bingUrlInspections.inspectedAt)).all();
7752
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
7355
7753
  const latestByUrl = /* @__PURE__ */ new Map();
7356
7754
  for (const row of allInspections) {
7357
7755
  if (!latestByUrl.has(row.url)) {
@@ -7438,14 +7836,14 @@ async function bingRoutes(app, opts) {
7438
7836
  import fs from "fs";
7439
7837
  import path from "path";
7440
7838
  import os from "os";
7441
- import { eq as eq16, and as and5 } from "drizzle-orm";
7839
+ import { eq as eq17, and as and6 } from "drizzle-orm";
7442
7840
  function getScreenshotDir() {
7443
7841
  return path.join(os.homedir(), ".canonry", "screenshots");
7444
7842
  }
7445
7843
  async function cdpRoutes(app, opts) {
7446
7844
  app.get("/screenshots/:snapshotId", async (request, reply) => {
7447
7845
  const { snapshotId } = request.params;
7448
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq16(querySnapshots.id, snapshotId)).get();
7846
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq17(querySnapshots.id, snapshotId)).get();
7449
7847
  if (!snapshot?.screenshotPath) {
7450
7848
  const err = notFound("Screenshot", snapshotId);
7451
7849
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7511,7 +7909,7 @@ async function cdpRoutes(app, opts) {
7511
7909
  async (request, reply) => {
7512
7910
  const project = resolveProject(app.db, request.params.name);
7513
7911
  const { runId } = request.params;
7514
- const run = app.db.select().from(runs).where(and5(eq16(runs.id, runId), eq16(runs.projectId, project.id))).get();
7912
+ const run = app.db.select().from(runs).where(and6(eq17(runs.id, runId), eq17(runs.projectId, project.id))).get();
7515
7913
  if (!run) {
7516
7914
  const err = notFound("Run", runId);
7517
7915
  return reply.code(err.statusCode).send(err.toJSON());
@@ -7524,8 +7922,8 @@ async function cdpRoutes(app, opts) {
7524
7922
  citedDomains: querySnapshots.citedDomains,
7525
7923
  screenshotPath: querySnapshots.screenshotPath,
7526
7924
  rawResponse: querySnapshots.rawResponse
7527
- }).from(querySnapshots).where(eq16(querySnapshots.runId, runId)).all();
7528
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq16(keywords.projectId, project.id)).all();
7925
+ }).from(querySnapshots).where(eq17(querySnapshots.runId, runId)).all();
7926
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq17(keywords.projectId, project.id)).all();
7529
7927
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
7530
7928
  const byKeyword = /* @__PURE__ */ new Map();
7531
7929
  for (const snap of snapshots) {
@@ -7608,7 +8006,7 @@ async function cdpRoutes(app, opts) {
7608
8006
 
7609
8007
  // ../api-routes/src/ga.ts
7610
8008
  import crypto16 from "crypto";
7611
- import { eq as eq17, desc as desc7, and as and6, sql as sql4 } from "drizzle-orm";
8009
+ import { eq as eq18, desc as desc8, and as and7, sql as sql4 } from "drizzle-orm";
7612
8010
  function gaLog(level, action, ctx) {
7613
8011
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
7614
8012
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -7765,10 +8163,10 @@ async function ga4Routes(app, opts) {
7765
8163
  if (!saConn && !oauthConn) {
7766
8164
  throw notFound("GA4 connection", project.name);
7767
8165
  }
7768
- app.db.delete(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).run();
7769
- app.db.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
7770
- app.db.delete(gaAiReferrals).where(eq17(gaAiReferrals.projectId, project.id)).run();
7771
- app.db.delete(gaSocialReferrals).where(eq17(gaSocialReferrals.projectId, project.id)).run();
8166
+ app.db.delete(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).run();
8167
+ app.db.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
8168
+ app.db.delete(gaAiReferrals).where(eq18(gaAiReferrals.projectId, project.id)).run();
8169
+ app.db.delete(gaSocialReferrals).where(eq18(gaSocialReferrals.projectId, project.id)).run();
7772
8170
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
7773
8171
  opts.ga4CredentialStore?.deleteConnection(project.name);
7774
8172
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -7789,7 +8187,7 @@ async function ga4Routes(app, opts) {
7789
8187
  if (!connected) {
7790
8188
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
7791
8189
  }
7792
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8190
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
7793
8191
  return {
7794
8192
  connected: true,
7795
8193
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -7848,8 +8246,8 @@ async function ga4Routes(app, opts) {
7848
8246
  app.db.transaction((tx) => {
7849
8247
  if (syncTraffic) {
7850
8248
  tx.delete(gaTrafficSnapshots).where(
7851
- and6(
7852
- eq17(gaTrafficSnapshots.projectId, project.id),
8249
+ and7(
8250
+ eq18(gaTrafficSnapshots.projectId, project.id),
7853
8251
  sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
7854
8252
  sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
7855
8253
  )
@@ -7870,8 +8268,8 @@ async function ga4Routes(app, opts) {
7870
8268
  }
7871
8269
  if (syncAi) {
7872
8270
  tx.delete(gaAiReferrals).where(
7873
- and6(
7874
- eq17(gaAiReferrals.projectId, project.id),
8271
+ and7(
8272
+ eq18(gaAiReferrals.projectId, project.id),
7875
8273
  sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
7876
8274
  sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
7877
8275
  )
@@ -7893,8 +8291,8 @@ async function ga4Routes(app, opts) {
7893
8291
  }
7894
8292
  if (syncSocial) {
7895
8293
  tx.delete(gaSocialReferrals).where(
7896
- and6(
7897
- eq17(gaSocialReferrals.projectId, project.id),
8294
+ and7(
8295
+ eq18(gaSocialReferrals.projectId, project.id),
7898
8296
  sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
7899
8297
  sql4`${gaSocialReferrals.date} <= ${summary.periodEnd}`
7900
8298
  )
@@ -7915,7 +8313,7 @@ async function ga4Routes(app, opts) {
7915
8313
  }
7916
8314
  }
7917
8315
  if (syncSummary) {
7918
- tx.delete(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).run();
8316
+ tx.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
7919
8317
  tx.insert(gaTrafficSummaries).values({
7920
8318
  id: crypto16.randomUUID(),
7921
8319
  projectId: project.id,
@@ -7929,7 +8327,7 @@ async function ga4Routes(app, opts) {
7929
8327
  }).run();
7930
8328
  }
7931
8329
  });
7932
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
8330
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
7933
8331
  const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
7934
8332
  gaLog("info", "sync.complete", {
7935
8333
  projectId: project.id,
@@ -7953,7 +8351,7 @@ async function ga4Routes(app, opts) {
7953
8351
  } catch (e) {
7954
8352
  const msg = e instanceof Error ? e.message : String(e);
7955
8353
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
7956
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
8354
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
7957
8355
  throw e;
7958
8356
  }
7959
8357
  });
@@ -7964,38 +8362,38 @@ async function ga4Routes(app, opts) {
7964
8362
  const window = parseWindow(request.query.window);
7965
8363
  const cutoff = windowCutoff(window);
7966
8364
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
7967
- const snapshotConditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
8365
+ const snapshotConditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
7968
8366
  if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
7969
- const aiConditions = [eq17(gaAiReferrals.projectId, project.id)];
8367
+ const aiConditions = [eq18(gaAiReferrals.projectId, project.id)];
7970
8368
  if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
7971
- const socialConditions = [eq17(gaSocialReferrals.projectId, project.id)];
8369
+ const socialConditions = [eq18(gaSocialReferrals.projectId, project.id)];
7972
8370
  if (cutoffDate) socialConditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
7973
8371
  const summaryRow = cutoffDate ? app.db.select({
7974
8372
  totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
7975
8373
  totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
7976
8374
  totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
7977
- }).from(gaTrafficSnapshots).where(and6(...snapshotConditions)).get() : app.db.select({
8375
+ }).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).get() : app.db.select({
7978
8376
  totalSessions: gaTrafficSummaries.totalSessions,
7979
8377
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
7980
8378
  totalUsers: gaTrafficSummaries.totalUsers
7981
- }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8379
+ }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
7982
8380
  const summaryMeta = app.db.select({
7983
8381
  periodStart: gaTrafficSummaries.periodStart,
7984
8382
  periodEnd: gaTrafficSummaries.periodEnd
7985
- }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).get();
8383
+ }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
7986
8384
  const rows = app.db.select({
7987
8385
  landingPage: gaTrafficSnapshots.landingPage,
7988
8386
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
7989
8387
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
7990
8388
  users: sql4`SUM(${gaTrafficSnapshots.users})`
7991
- }).from(gaTrafficSnapshots).where(and6(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
8389
+ }).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
7992
8390
  const aiReferrals = app.db.select({
7993
8391
  source: gaAiReferrals.source,
7994
8392
  medium: gaAiReferrals.medium,
7995
8393
  sourceDimension: gaAiReferrals.sourceDimension,
7996
8394
  sessions: sql4`SUM(${gaAiReferrals.sessions})`,
7997
8395
  users: sql4`SUM(${gaAiReferrals.users})`
7998
- }).from(gaAiReferrals).where(and6(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8396
+ }).from(gaAiReferrals).where(and7(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
7999
8397
  const aiDeduped = app.db.select({
8000
8398
  sessions: sql4`SUM(max_sessions)`,
8001
8399
  users: sql4`SUM(max_users)`
@@ -8015,12 +8413,12 @@ async function ga4Routes(app, opts) {
8015
8413
  channelGroup: gaSocialReferrals.channelGroup,
8016
8414
  sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8017
8415
  users: sql4`SUM(${gaSocialReferrals.users})`
8018
- }).from(gaSocialReferrals).where(and6(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8416
+ }).from(gaSocialReferrals).where(and7(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
8019
8417
  const socialTotals = app.db.select({
8020
8418
  sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
8021
8419
  users: sql4`SUM(${gaSocialReferrals.users})`
8022
- }).from(gaSocialReferrals).where(and6(...socialConditions)).get();
8023
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq17(gaTrafficSummaries.projectId, project.id)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
8420
+ }).from(gaSocialReferrals).where(and7(...socialConditions)).get();
8421
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
8024
8422
  const total = summaryRow?.totalSessions ?? 0;
8025
8423
  return {
8026
8424
  totalSessions: total,
@@ -8067,7 +8465,7 @@ async function ga4Routes(app, opts) {
8067
8465
  const project = resolveProject(app.db, request.params.name);
8068
8466
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8069
8467
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8070
- const conditions = [eq17(gaAiReferrals.projectId, project.id)];
8468
+ const conditions = [eq18(gaAiReferrals.projectId, project.id)];
8071
8469
  if (cutoffDate) conditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
8072
8470
  const rows = app.db.select({
8073
8471
  date: gaAiReferrals.date,
@@ -8076,14 +8474,14 @@ async function ga4Routes(app, opts) {
8076
8474
  sourceDimension: gaAiReferrals.sourceDimension,
8077
8475
  sessions: gaAiReferrals.sessions,
8078
8476
  users: gaAiReferrals.users
8079
- }).from(gaAiReferrals).where(and6(...conditions)).orderBy(gaAiReferrals.date).all();
8477
+ }).from(gaAiReferrals).where(and7(...conditions)).orderBy(gaAiReferrals.date).all();
8080
8478
  return rows;
8081
8479
  });
8082
8480
  app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
8083
8481
  const project = resolveProject(app.db, request.params.name);
8084
8482
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8085
8483
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8086
- const conditions = [eq17(gaSocialReferrals.projectId, project.id)];
8484
+ const conditions = [eq18(gaSocialReferrals.projectId, project.id)];
8087
8485
  if (cutoffDate) conditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
8088
8486
  const rows = app.db.select({
8089
8487
  date: gaSocialReferrals.date,
@@ -8092,7 +8490,7 @@ async function ga4Routes(app, opts) {
8092
8490
  channelGroup: gaSocialReferrals.channelGroup,
8093
8491
  sessions: gaSocialReferrals.sessions,
8094
8492
  users: gaSocialReferrals.users
8095
- }).from(gaSocialReferrals).where(and6(...conditions)).orderBy(gaSocialReferrals.date).all();
8493
+ }).from(gaSocialReferrals).where(and7(...conditions)).orderBy(gaSocialReferrals.date).all();
8096
8494
  return rows;
8097
8495
  });
8098
8496
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -8105,8 +8503,8 @@ async function ga4Routes(app, opts) {
8105
8503
  d.setDate(d.getDate() - n);
8106
8504
  return fmt(d);
8107
8505
  };
8108
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(
8109
- eq17(gaSocialReferrals.projectId, project.id),
8506
+ const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(
8507
+ eq18(gaSocialReferrals.projectId, project.id),
8110
8508
  sql4`${gaSocialReferrals.date} >= ${from}`,
8111
8509
  sql4`${gaSocialReferrals.date} < ${to}`
8112
8510
  )).get();
@@ -8118,16 +8516,16 @@ async function ga4Routes(app, opts) {
8118
8516
  const sourceCurrent = app.db.select({
8119
8517
  source: gaSocialReferrals.source,
8120
8518
  sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8121
- }).from(gaSocialReferrals).where(and6(
8122
- eq17(gaSocialReferrals.projectId, project.id),
8519
+ }).from(gaSocialReferrals).where(and7(
8520
+ eq18(gaSocialReferrals.projectId, project.id),
8123
8521
  sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
8124
8522
  sql4`${gaSocialReferrals.date} < ${fmt(today)}`
8125
8523
  )).groupBy(gaSocialReferrals.source).all();
8126
8524
  const sourcePrev = app.db.select({
8127
8525
  source: gaSocialReferrals.source,
8128
8526
  sessions: sql4`SUM(${gaSocialReferrals.sessions})`
8129
- }).from(gaSocialReferrals).where(and6(
8130
- eq17(gaSocialReferrals.projectId, project.id),
8527
+ }).from(gaSocialReferrals).where(and7(
8528
+ eq18(gaSocialReferrals.projectId, project.id),
8131
8529
  sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
8132
8530
  sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`
8133
8531
  )).groupBy(gaSocialReferrals.source).all();
@@ -8168,15 +8566,15 @@ async function ga4Routes(app, opts) {
8168
8566
  return fmt(d);
8169
8567
  };
8170
8568
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
8171
- const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8172
- const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and6(eq17(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8569
+ const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8570
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
8173
8571
  const sumAi = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
8174
8572
  SELECT date, source, medium, MAX(sessions) AS max_sessions
8175
8573
  FROM ga_ai_referrals
8176
8574
  WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
8177
8575
  GROUP BY date, source, medium
8178
8576
  )`).get();
8179
- const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
8577
+ const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
8180
8578
  const todayStr = fmt(today);
8181
8579
  const buildTrend = (sum) => {
8182
8580
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -8211,8 +8609,8 @@ async function ga4Routes(app, opts) {
8211
8609
  }
8212
8610
  return mover;
8213
8611
  };
8214
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
8215
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and6(eq17(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
8612
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
8613
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
8216
8614
  return {
8217
8615
  total: buildTrend(sumTotal),
8218
8616
  organic: buildTrend(sumOrganic),
@@ -8226,14 +8624,14 @@ async function ga4Routes(app, opts) {
8226
8624
  const project = resolveProject(app.db, request.params.name);
8227
8625
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8228
8626
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8229
- const conditions = [eq17(gaTrafficSnapshots.projectId, project.id)];
8627
+ const conditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
8230
8628
  if (cutoffDate) conditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8231
8629
  const rows = app.db.select({
8232
8630
  date: gaTrafficSnapshots.date,
8233
8631
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8234
8632
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8235
8633
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8236
- }).from(gaTrafficSnapshots).where(and6(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8634
+ }).from(gaTrafficSnapshots).where(and7(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8237
8635
  return rows.map((r) => ({
8238
8636
  date: r.date,
8239
8637
  sessions: r.sessions ?? 0,
@@ -8249,7 +8647,7 @@ async function ga4Routes(app, opts) {
8249
8647
  sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8250
8648
  organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8251
8649
  users: sql4`SUM(${gaTrafficSnapshots.users})`
8252
- }).from(gaTrafficSnapshots).where(eq17(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8650
+ }).from(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
8253
8651
  return {
8254
8652
  pages: trafficPages.map((r) => ({
8255
8653
  landingPage: r.landingPage,
@@ -9886,7 +10284,7 @@ async function wordpressRoutes(app, opts) {
9886
10284
 
9887
10285
  // ../api-routes/src/backlinks.ts
9888
10286
  import crypto18 from "crypto";
9889
- import { and as and7, asc as asc2, desc as desc8, eq as eq18, sql as sql5 } from "drizzle-orm";
10287
+ import { and as and8, asc as asc2, desc as desc9, eq as eq19, sql as sql5 } from "drizzle-orm";
9890
10288
 
9891
10289
  // ../integration-commoncrawl/src/constants.ts
9892
10290
  import os2 from "os";
@@ -10279,13 +10677,13 @@ function mapRunRow(row) {
10279
10677
  location: row.location ?? null,
10280
10678
  startedAt: row.startedAt ?? null,
10281
10679
  finishedAt: row.finishedAt ?? null,
10282
- error: row.error ?? null,
10680
+ error: parseRunError(row.error),
10283
10681
  createdAt: row.createdAt
10284
10682
  };
10285
10683
  }
10286
10684
  function latestSummaryForProject(db, projectId, release) {
10287
- const condition = release ? and7(eq18(backlinkSummaries.projectId, projectId), eq18(backlinkSummaries.release, release)) : eq18(backlinkSummaries.projectId, projectId);
10288
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc8(backlinkSummaries.queriedAt)).limit(1).get();
10685
+ const condition = release ? and8(eq19(backlinkSummaries.projectId, projectId), eq19(backlinkSummaries.release, release)) : eq19(backlinkSummaries.projectId, projectId);
10686
+ return db.select().from(backlinkSummaries).where(condition).orderBy(desc9(backlinkSummaries.queriedAt)).limit(1).get();
10289
10687
  }
10290
10688
  async function backlinksRoutes(app, opts) {
10291
10689
  app.get("/backlinks/status", async (_request, reply) => {
@@ -10314,7 +10712,7 @@ async function backlinksRoutes(app, opts) {
10314
10712
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
10315
10713
  );
10316
10714
  }
10317
- const existing = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.release, release)).get();
10715
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.release, release)).get();
10318
10716
  const now = (/* @__PURE__ */ new Date()).toISOString();
10319
10717
  if (existing) {
10320
10718
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -10325,9 +10723,9 @@ async function backlinksRoutes(app, opts) {
10325
10723
  phaseDetail: null,
10326
10724
  error: null,
10327
10725
  updatedAt: now
10328
- }).where(eq18(ccReleaseSyncs.id, existing.id)).run();
10726
+ }).where(eq19(ccReleaseSyncs.id, existing.id)).run();
10329
10727
  opts.onReleaseSyncRequested(existing.id, release);
10330
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.id, existing.id)).get();
10728
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, existing.id)).get();
10331
10729
  return reply.status(200).send(mapSyncRow(refreshed));
10332
10730
  }
10333
10731
  const id = crypto18.randomUUID();
@@ -10339,15 +10737,15 @@ async function backlinksRoutes(app, opts) {
10339
10737
  updatedAt: now
10340
10738
  }).run();
10341
10739
  opts.onReleaseSyncRequested(id, release);
10342
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq18(ccReleaseSyncs.id, id)).get();
10740
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, id)).get();
10343
10741
  return reply.status(201).send(mapSyncRow(inserted));
10344
10742
  });
10345
10743
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
10346
- const row = app.db.select().from(ccReleaseSyncs).orderBy(desc8(ccReleaseSyncs.updatedAt)).limit(1).get();
10744
+ const row = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).limit(1).get();
10347
10745
  return reply.send(row ? mapSyncRow(row) : null);
10348
10746
  });
10349
10747
  app.get("/backlinks/syncs", async (_request, reply) => {
10350
- const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc8(ccReleaseSyncs.updatedAt)).all();
10748
+ const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).all();
10351
10749
  return reply.send(rows.map(mapSyncRow));
10352
10750
  });
10353
10751
  app.get("/backlinks/releases", async (_request, reply) => {
@@ -10390,7 +10788,7 @@ async function backlinksRoutes(app, opts) {
10390
10788
  createdAt: now
10391
10789
  }).run();
10392
10790
  opts.onBacklinkExtractRequested(runId, project.id, release);
10393
- const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
10791
+ const run = app.db.select().from(runs).where(eq19(runs.id, runId)).get();
10394
10792
  return reply.status(201).send(mapRunRow(run));
10395
10793
  });
10396
10794
  app.get(
@@ -10411,15 +10809,15 @@ async function backlinksRoutes(app, opts) {
10411
10809
  }
10412
10810
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
10413
10811
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
10414
- const domainCondition = and7(
10415
- eq18(backlinkDomains.projectId, project.id),
10416
- eq18(backlinkDomains.release, targetRelease)
10812
+ const domainCondition = and8(
10813
+ eq19(backlinkDomains.projectId, project.id),
10814
+ eq19(backlinkDomains.release, targetRelease)
10417
10815
  );
10418
10816
  const totalRow = app.db.select({ count: sql5`count(*)` }).from(backlinkDomains).where(domainCondition).get();
10419
10817
  const rows = app.db.select({
10420
10818
  linkingDomain: backlinkDomains.linkingDomain,
10421
10819
  numHosts: backlinkDomains.numHosts
10422
- }).from(backlinkDomains).where(domainCondition).orderBy(desc8(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
10820
+ }).from(backlinkDomains).where(domainCondition).orderBy(desc9(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
10423
10821
  const response = {
10424
10822
  summary: summaryRow ? mapSummaryRow(summaryRow) : null,
10425
10823
  total: Number(totalRow?.count ?? 0),
@@ -10431,7 +10829,7 @@ async function backlinksRoutes(app, opts) {
10431
10829
  "/projects/:name/backlinks/history",
10432
10830
  async (request, reply) => {
10433
10831
  const project = resolveProject(app.db, request.params.name);
10434
- const rows = app.db.select().from(backlinkSummaries).where(eq18(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
10832
+ const rows = app.db.select().from(backlinkSummaries).where(eq19(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
10435
10833
  const response = rows.map((r) => ({
10436
10834
  release: r.release,
10437
10835
  totalLinkingDomains: r.totalLinkingDomains,
@@ -10504,6 +10902,7 @@ async function apiRoutes(app, opts) {
10504
10902
  await api.register(historyRoutes);
10505
10903
  await api.register(analyticsRoutes);
10506
10904
  await api.register(intelligenceRoutes);
10905
+ await api.register(contentRoutes);
10507
10906
  await api.register(settingsRoutes, {
10508
10907
  providerSummary: opts.providerSummary,
10509
10908
  providerAdapters: opts.providerAdapters,
@@ -12649,7 +13048,7 @@ function hasParsedResponseContent4(rawResponse) {
12649
13048
  return Array.isArray(nestedResponse.choices) && nestedResponse.choices.length > 0 || Array.isArray(nestedResponse.search_results) && nestedResponse.search_results.length > 0 || Array.isArray(nestedResponse.citations) && nestedResponse.citations.length > 0;
12650
13049
  }
12651
13050
  function reparseStoredResult4(rawResponse) {
12652
- const groundingSources = extractGroundingSources(rawResponse);
13051
+ const groundingSources = extractGroundingSources2(rawResponse);
12653
13052
  return {
12654
13053
  provider: "perplexity",
12655
13054
  answerText: extractAnswerText3(rawResponse),
@@ -12680,7 +13079,7 @@ function extractCitations(rawResponse) {
12680
13079
  }
12681
13080
  return [];
12682
13081
  }
12683
- function extractGroundingSources(rawResponse) {
13082
+ function extractGroundingSources2(rawResponse) {
12684
13083
  const searchResults = extractSearchResults(rawResponse);
12685
13084
  if (searchResults.length > 0) {
12686
13085
  const seen = /* @__PURE__ */ new Set();
@@ -13025,7 +13424,7 @@ import crypto19 from "crypto";
13025
13424
  import fs7 from "fs";
13026
13425
  import path9 from "path";
13027
13426
  import os4 from "os";
13028
- import { and as and8, eq as eq19, inArray as inArray3, sql as sql6 } from "drizzle-orm";
13427
+ import { and as and9, eq as eq20, inArray as inArray4, sql as sql6 } from "drizzle-orm";
13029
13428
 
13030
13429
  // src/citation-utils.ts
13031
13430
  function domainMatches(domain, canonicalDomain) {
@@ -13277,11 +13676,11 @@ var JobRunner = class {
13277
13676
  this.registry = registry;
13278
13677
  }
13279
13678
  recoverStaleRuns() {
13280
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray3(runs.status, ["running", "queued"])).all();
13679
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
13281
13680
  if (stale.length === 0) return;
13282
13681
  const now = (/* @__PURE__ */ new Date()).toISOString();
13283
13682
  for (const run of stale) {
13284
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq19(runs.id, run.id)).run();
13683
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq20(runs.id, run.id)).run();
13285
13684
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
13286
13685
  }
13287
13686
  }
@@ -13309,10 +13708,10 @@ var JobRunner = class {
13309
13708
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
13310
13709
  }
13311
13710
  if (existingRun.status === "queued") {
13312
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and8(eq19(runs.id, runId), eq19(runs.status, "queued"))).run();
13711
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and9(eq20(runs.id, runId), eq20(runs.status, "queued"))).run();
13313
13712
  }
13314
13713
  this.throwIfRunCancelled(runId);
13315
- const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
13714
+ const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
13316
13715
  if (!project) {
13317
13716
  throw new Error(`Project ${projectId} not found`);
13318
13717
  }
@@ -13332,8 +13731,8 @@ var JobRunner = class {
13332
13731
  throw new Error("No providers configured. Add at least one provider API key.");
13333
13732
  }
13334
13733
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
13335
- projectKeywords = this.db.select().from(keywords).where(eq19(keywords.projectId, projectId)).all();
13336
- const projectCompetitors = this.db.select().from(competitors).where(eq19(competitors.projectId, projectId)).all();
13734
+ projectKeywords = this.db.select().from(keywords).where(eq20(keywords.projectId, projectId)).all();
13735
+ const projectCompetitors = this.db.select().from(competitors).where(eq20(competitors.projectId, projectId)).all();
13337
13736
  const competitorDomains = projectCompetitors.map((c) => c.domain);
13338
13737
  const allDomains = effectiveDomains({
13339
13738
  canonicalDomain: project.canonicalDomain,
@@ -13349,7 +13748,7 @@ var JobRunner = class {
13349
13748
  const todayPeriod = getCurrentUsageDay();
13350
13749
  for (const p of activeProviders) {
13351
13750
  const providerScope = `${projectId}:${p.adapter.name}`;
13352
- const providerUsage = this.db.select().from(usageCounters).where(eq19(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
13751
+ const providerUsage = this.db.select().from(usageCounters).where(eq20(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
13353
13752
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
13354
13753
  if (providerUsage + queriesPerProvider > limit) {
13355
13754
  throw new Error(
@@ -13489,13 +13888,13 @@ var JobRunner = class {
13489
13888
  const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
13490
13889
  const someFailed = providerErrors.size > 0;
13491
13890
  if (allFailed) {
13492
- const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13493
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
13891
+ const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13892
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
13494
13893
  } else if (someFailed) {
13495
- const errorDetail = JSON.stringify(Object.fromEntries(providerErrors));
13496
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq19(runs.id, runId)).run();
13894
+ const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
13895
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
13497
13896
  } else {
13498
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
13897
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
13499
13898
  }
13500
13899
  this.flushProviderUsage(projectId, providerDispatchCounts);
13501
13900
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -13530,7 +13929,7 @@ var JobRunner = class {
13530
13929
  status: "failed",
13531
13930
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13532
13931
  error: errorMessage
13533
- }).where(eq19(runs.id, runId)).run();
13932
+ }).where(eq20(runs.id, runId)).run();
13534
13933
  this.flushProviderUsage(projectId, providerDispatchCounts);
13535
13934
  trackEvent("run.completed", {
13536
13935
  status: "failed",
@@ -13573,7 +13972,7 @@ var JobRunner = class {
13573
13972
  status: runs.status,
13574
13973
  finishedAt: runs.finishedAt,
13575
13974
  error: runs.error
13576
- }).from(runs).where(eq19(runs.id, runId)).get();
13975
+ }).from(runs).where(eq20(runs.id, runId)).get();
13577
13976
  }
13578
13977
  isRunCancelled(runId) {
13579
13978
  return this.getRunState(runId)?.status === "cancelled";
@@ -13589,7 +13988,7 @@ var JobRunner = class {
13589
13988
  this.db.update(runs).set({
13590
13989
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
13591
13990
  error: currentRun.error ?? "Cancelled by user"
13592
- }).where(eq19(runs.id, runId)).run();
13991
+ }).where(eq20(runs.id, runId)).run();
13593
13992
  }
13594
13993
  trackEvent("run.completed", {
13595
13994
  status: "cancelled",
@@ -13612,7 +14011,7 @@ function getCurrentUsageDay() {
13612
14011
 
13613
14012
  // src/gsc-sync.ts
13614
14013
  import crypto20 from "crypto";
13615
- import { eq as eq20, and as and9, sql as sql7 } from "drizzle-orm";
14014
+ import { eq as eq21, and as and10, sql as sql7 } from "drizzle-orm";
13616
14015
  var log2 = createLogger("GscSync");
13617
14016
  function formatDate2(d) {
13618
14017
  return d.toISOString().split("T")[0];
@@ -13624,13 +14023,13 @@ function daysAgo(n) {
13624
14023
  }
13625
14024
  async function executeGscSync(db, runId, projectId, opts) {
13626
14025
  const now = (/* @__PURE__ */ new Date()).toISOString();
13627
- db.update(runs).set({ status: "running", startedAt: now }).where(eq20(runs.id, runId)).run();
14026
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
13628
14027
  try {
13629
14028
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13630
14029
  if (!googleClientId || !googleClientSecret) {
13631
14030
  throw new Error("Google OAuth is not configured in the local Canonry config");
13632
14031
  }
13633
- const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
14032
+ const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
13634
14033
  if (!project) {
13635
14034
  throw new Error(`Project not found: ${projectId}`);
13636
14035
  }
@@ -13664,8 +14063,8 @@ async function executeGscSync(db, runId, projectId, opts) {
13664
14063
  });
13665
14064
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
13666
14065
  db.delete(gscSearchData).where(
13667
- and9(
13668
- eq20(gscSearchData.projectId, projectId),
14066
+ and10(
14067
+ eq21(gscSearchData.projectId, projectId),
13669
14068
  sql7`${gscSearchData.date} >= ${startDate}`,
13670
14069
  sql7`${gscSearchData.date} <= ${endDate}`
13671
14070
  )
@@ -13732,7 +14131,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13732
14131
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
13733
14132
  }
13734
14133
  }
13735
- const allInspections = db.select().from(gscUrlInspections).where(eq20(gscUrlInspections.projectId, projectId)).all();
14134
+ const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
13736
14135
  const latestByUrl = /* @__PURE__ */ new Map();
13737
14136
  for (const row of allInspections) {
13738
14137
  const existing = latestByUrl.get(row.url);
@@ -13753,7 +14152,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13753
14152
  }
13754
14153
  }
13755
14154
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
13756
- db.delete(gscCoverageSnapshots).where(and9(eq20(gscCoverageSnapshots.projectId, projectId), eq20(gscCoverageSnapshots.date, snapshotDate))).run();
14155
+ db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
13757
14156
  db.insert(gscCoverageSnapshots).values({
13758
14157
  id: crypto20.randomUUID(),
13759
14158
  projectId,
@@ -13764,11 +14163,11 @@ async function executeGscSync(db, runId, projectId, opts) {
13764
14163
  reasonBreakdown: JSON.stringify(reasonCounts),
13765
14164
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13766
14165
  }).run();
13767
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14166
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
13768
14167
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
13769
14168
  } catch (err) {
13770
14169
  const errorMsg = err instanceof Error ? err.message : String(err);
13771
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
14170
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
13772
14171
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
13773
14172
  throw err;
13774
14173
  }
@@ -13776,7 +14175,7 @@ async function executeGscSync(db, runId, projectId, opts) {
13776
14175
 
13777
14176
  // src/gsc-inspect-sitemap.ts
13778
14177
  import crypto21 from "crypto";
13779
- import { eq as eq21, and as and10 } from "drizzle-orm";
14178
+ import { eq as eq22, and as and11 } from "drizzle-orm";
13780
14179
 
13781
14180
  // src/sitemap-parser.ts
13782
14181
  var log3 = createLogger("SitemapParser");
@@ -13897,13 +14296,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
13897
14296
  var log4 = createLogger("InspectSitemap");
13898
14297
  async function executeInspectSitemap(db, runId, projectId, opts) {
13899
14298
  const now = (/* @__PURE__ */ new Date()).toISOString();
13900
- db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
14299
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
13901
14300
  try {
13902
14301
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
13903
14302
  if (!googleClientId || !googleClientSecret) {
13904
14303
  throw new Error("Google OAuth is not configured in the local Canonry config");
13905
14304
  }
13906
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
14305
+ const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
13907
14306
  if (!project) {
13908
14307
  throw new Error(`Project not found: ${projectId}`);
13909
14308
  }
@@ -13971,7 +14370,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13971
14370
  await new Promise((r) => setTimeout(r, 1e3));
13972
14371
  }
13973
14372
  }
13974
- const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
14373
+ const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
13975
14374
  const latestByUrl = /* @__PURE__ */ new Map();
13976
14375
  for (const row of allInspections) {
13977
14376
  const existing = latestByUrl.get(row.url);
@@ -13992,7 +14391,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
13992
14391
  }
13993
14392
  }
13994
14393
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13995
- db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
14394
+ db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
13996
14395
  db.insert(gscCoverageSnapshots).values({
13997
14396
  id: crypto21.randomUUID(),
13998
14397
  projectId,
@@ -14004,11 +14403,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14004
14403
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
14005
14404
  }).run();
14006
14405
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
14007
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14406
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14008
14407
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
14009
14408
  } catch (err) {
14010
14409
  const errorMsg = err instanceof Error ? err.message : String(err);
14011
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
14410
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14012
14411
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
14013
14412
  throw err;
14014
14413
  }
@@ -14016,7 +14415,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
14016
14415
 
14017
14416
  // src/bing-inspect-sitemap.ts
14018
14417
  import crypto22 from "crypto";
14019
- import { eq as eq22, desc as desc9 } from "drizzle-orm";
14418
+ import { eq as eq23, desc as desc10 } from "drizzle-orm";
14020
14419
  var log5 = createLogger("BingInspectSitemap");
14021
14420
  function parseBingDate2(value) {
14022
14421
  if (!value) return null;
@@ -14034,9 +14433,9 @@ function isBlockingIssueType2(issueType) {
14034
14433
  }
14035
14434
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
14036
14435
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14037
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq22(runs.id, runId)).run();
14436
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq23(runs.id, runId)).run();
14038
14437
  try {
14039
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
14438
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
14040
14439
  if (!project) {
14041
14440
  throw new Error(`Project not found: ${projectId}`);
14042
14441
  }
@@ -14054,7 +14453,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14054
14453
  if (sitemapUrls.length === 0) {
14055
14454
  throw new Error("No URLs found in sitemap");
14056
14455
  }
14057
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, projectId)).all();
14456
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).all();
14058
14457
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
14059
14458
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
14060
14459
  log5.info("sitemap.diff", {
@@ -14137,7 +14536,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14137
14536
  await new Promise((r) => setTimeout(r, 1e3));
14138
14537
  }
14139
14538
  }
14140
- const allInspections = db.select().from(bingUrlInspections).where(eq22(bingUrlInspections.projectId, projectId)).orderBy(desc9(bingUrlInspections.inspectedAt)).all();
14539
+ const allInspections = db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).orderBy(desc10(bingUrlInspections.inspectedAt)).all();
14141
14540
  const latestByUrl = /* @__PURE__ */ new Map();
14142
14541
  const definitiveByUrl = /* @__PURE__ */ new Map();
14143
14542
  for (const row of allInspections) {
@@ -14180,7 +14579,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14180
14579
  }
14181
14580
  }).run();
14182
14581
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
14183
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14582
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14184
14583
  log5.info("inspect.completed", {
14185
14584
  runId,
14186
14585
  projectId,
@@ -14194,7 +14593,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14194
14593
  });
14195
14594
  } catch (err) {
14196
14595
  const errorMsg = err instanceof Error ? err.message : String(err);
14197
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
14596
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
14198
14597
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
14199
14598
  throw err;
14200
14599
  }
@@ -14203,7 +14602,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
14203
14602
  // src/commoncrawl-sync.ts
14204
14603
  import crypto23 from "crypto";
14205
14604
  import path10 from "path";
14206
- import { and as and11, eq as eq23, sql as sql8 } from "drizzle-orm";
14605
+ import { and as and12, eq as eq24, sql as sql8 } from "drizzle-orm";
14207
14606
  var log6 = createLogger("CommonCrawlSync");
14208
14607
  var INSERT_CHUNK_SIZE = 1e4;
14209
14608
  function defaultDeps() {
@@ -14229,7 +14628,7 @@ async function executeReleaseSync(db, syncId, opts) {
14229
14628
  phaseDetail: "downloading vertices + edges",
14230
14629
  updatedAt: downloadStartedAt,
14231
14630
  error: null
14232
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
14631
+ }).where(eq24(ccReleaseSyncs.id, syncId)).run();
14233
14632
  const paths = ccReleasePaths(release);
14234
14633
  const releaseCacheDir = path10.join(deps.cacheDir, release);
14235
14634
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -14252,7 +14651,7 @@ async function executeReleaseSync(db, syncId, opts) {
14252
14651
  vertexSha256: vertex.sha256,
14253
14652
  edgesSha256: edges.sha256,
14254
14653
  updatedAt: downloadFinishedAt
14255
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
14654
+ }).where(eq24(ccReleaseSyncs.id, syncId)).run();
14256
14655
  const allProjects = db.select().from(projects).all();
14257
14656
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
14258
14657
  let rows = [];
@@ -14268,8 +14667,8 @@ async function executeReleaseSync(db, syncId, opts) {
14268
14667
  }
14269
14668
  const queriedAt = deps.now().toISOString();
14270
14669
  db.transaction((tx) => {
14271
- tx.delete(backlinkDomains).where(eq23(backlinkDomains.releaseSyncId, syncId)).run();
14272
- tx.delete(backlinkSummaries).where(eq23(backlinkSummaries.releaseSyncId, syncId)).run();
14670
+ tx.delete(backlinkDomains).where(eq24(backlinkDomains.releaseSyncId, syncId)).run();
14671
+ tx.delete(backlinkSummaries).where(eq24(backlinkSummaries.releaseSyncId, syncId)).run();
14273
14672
  const expanded = [];
14274
14673
  for (const r of rows) {
14275
14674
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -14328,7 +14727,7 @@ async function executeReleaseSync(db, syncId, opts) {
14328
14727
  domainsDiscovered: rows.length,
14329
14728
  updatedAt: finishedAt,
14330
14729
  error: null
14331
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
14730
+ }).where(eq24(ccReleaseSyncs.id, syncId)).run();
14332
14731
  log6.info("sync.completed", {
14333
14732
  syncId,
14334
14733
  release,
@@ -14358,7 +14757,7 @@ async function executeReleaseSync(db, syncId, opts) {
14358
14757
  error: errorMsg,
14359
14758
  phaseDetail: null,
14360
14759
  updatedAt: finishedAt
14361
- }).where(eq23(ccReleaseSyncs.id, syncId)).run();
14760
+ }).where(eq24(ccReleaseSyncs.id, syncId)).run();
14362
14761
  log6.error("sync.failed", { syncId, release, error: errorMsg });
14363
14762
  throw err;
14364
14763
  }
@@ -14394,7 +14793,7 @@ function computeSummary(rows) {
14394
14793
  // src/backlink-extract.ts
14395
14794
  import crypto24 from "crypto";
14396
14795
  import fs8 from "fs";
14397
- import { and as and12, desc as desc10, eq as eq24 } from "drizzle-orm";
14796
+ import { and as and13, desc as desc11, eq as eq25 } from "drizzle-orm";
14398
14797
  var log7 = createLogger("BacklinkExtract");
14399
14798
  function defaultDeps2() {
14400
14799
  return {
@@ -14406,13 +14805,13 @@ function defaultDeps2() {
14406
14805
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14407
14806
  const deps = { ...defaultDeps2(), ...opts.deps };
14408
14807
  const startedAt = deps.now().toISOString();
14409
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
14808
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
14410
14809
  try {
14411
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
14810
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
14412
14811
  if (!project) {
14413
14812
  throw new Error(`Project not found: ${projectId}`);
14414
14813
  }
14415
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq24(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc10(ccReleaseSyncs.createdAt)).limit(1).get();
14814
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc11(ccReleaseSyncs.createdAt)).limit(1).get();
14416
14815
  if (!sync) {
14417
14816
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
14418
14817
  }
@@ -14440,7 +14839,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14440
14839
  const targetDomain = project.canonicalDomain;
14441
14840
  db.transaction((tx) => {
14442
14841
  tx.delete(backlinkDomains).where(
14443
- and12(eq24(backlinkDomains.projectId, projectId), eq24(backlinkDomains.release, release))
14842
+ and13(eq25(backlinkDomains.projectId, projectId), eq25(backlinkDomains.release, release))
14444
14843
  ).run();
14445
14844
  if (rows.length > 0) {
14446
14845
  const values = rows.map((r) => ({
@@ -14480,7 +14879,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14480
14879
  }).run();
14481
14880
  });
14482
14881
  const finishedAt = deps.now().toISOString();
14483
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq24(runs.id, runId)).run();
14882
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
14484
14883
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
14485
14884
  } catch (err) {
14486
14885
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -14489,7 +14888,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
14489
14888
  status: RunStatuses.failed,
14490
14889
  error: errorMsg,
14491
14890
  finishedAt
14492
- }).where(eq24(runs.id, runId)).run();
14891
+ }).where(eq25(runs.id, runId)).run();
14493
14892
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
14494
14893
  throw err;
14495
14894
  }
@@ -14562,7 +14961,7 @@ var ProviderRegistry = class {
14562
14961
 
14563
14962
  // src/scheduler.ts
14564
14963
  import cron from "node-cron";
14565
- import { eq as eq25 } from "drizzle-orm";
14964
+ import { eq as eq26 } from "drizzle-orm";
14566
14965
  var log8 = createLogger("Scheduler");
14567
14966
  var Scheduler = class {
14568
14967
  db;
@@ -14574,7 +14973,7 @@ var Scheduler = class {
14574
14973
  }
14575
14974
  /** Load all enabled schedules from DB and register cron jobs. */
14576
14975
  start() {
14577
- const allSchedules = this.db.select().from(schedules).where(eq25(schedules.enabled, 1)).all();
14976
+ const allSchedules = this.db.select().from(schedules).where(eq26(schedules.enabled, 1)).all();
14578
14977
  for (const schedule of allSchedules) {
14579
14978
  const missedRunAt = schedule.nextRunAt;
14580
14979
  this.registerCronTask(schedule);
@@ -14599,7 +14998,7 @@ var Scheduler = class {
14599
14998
  this.stopTask(projectId, existing, "Stopped");
14600
14999
  this.tasks.delete(projectId);
14601
15000
  }
14602
- const schedule = this.db.select().from(schedules).where(eq25(schedules.projectId, projectId)).get();
15001
+ const schedule = this.db.select().from(schedules).where(eq26(schedules.projectId, projectId)).get();
14603
15002
  if (schedule && schedule.enabled === 1) {
14604
15003
  this.registerCronTask(schedule);
14605
15004
  }
@@ -14632,14 +15031,14 @@ var Scheduler = class {
14632
15031
  this.db.update(schedules).set({
14633
15032
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
14634
15033
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14635
- }).where(eq25(schedules.id, scheduleId)).run();
15034
+ }).where(eq26(schedules.id, scheduleId)).run();
14636
15035
  const label = schedule.preset ?? cronExpr;
14637
15036
  log8.info("cron.registered", { projectId, schedule: label, timezone });
14638
15037
  }
14639
15038
  triggerRun(scheduleId, projectId) {
14640
15039
  try {
14641
15040
  const now = (/* @__PURE__ */ new Date()).toISOString();
14642
- const currentSchedule = this.db.select().from(schedules).where(eq25(schedules.id, scheduleId)).get();
15041
+ const currentSchedule = this.db.select().from(schedules).where(eq26(schedules.id, scheduleId)).get();
14643
15042
  if (!currentSchedule || currentSchedule.enabled !== 1) {
14644
15043
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
14645
15044
  this.remove(projectId);
@@ -14647,7 +15046,7 @@ var Scheduler = class {
14647
15046
  }
14648
15047
  const task = this.tasks.get(projectId);
14649
15048
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
14650
- const project = this.db.select().from(projects).where(eq25(projects.id, projectId)).get();
15049
+ const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
14651
15050
  if (!project) {
14652
15051
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
14653
15052
  this.remove(projectId);
@@ -14676,7 +15075,7 @@ var Scheduler = class {
14676
15075
  this.db.update(schedules).set({
14677
15076
  nextRunAt,
14678
15077
  updatedAt: now
14679
- }).where(eq25(schedules.id, currentSchedule.id)).run();
15078
+ }).where(eq26(schedules.id, currentSchedule.id)).run();
14680
15079
  return;
14681
15080
  }
14682
15081
  const runId = queueResult.runId;
@@ -14684,7 +15083,7 @@ var Scheduler = class {
14684
15083
  lastRunAt: now,
14685
15084
  nextRunAt,
14686
15085
  updatedAt: now
14687
- }).where(eq25(schedules.id, currentSchedule.id)).run();
15086
+ }).where(eq26(schedules.id, currentSchedule.id)).run();
14688
15087
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
14689
15088
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14690
15089
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -14696,7 +15095,7 @@ var Scheduler = class {
14696
15095
  };
14697
15096
 
14698
15097
  // src/notifier.ts
14699
- import { eq as eq26, desc as desc11, and as and13, or as or2 } from "drizzle-orm";
15098
+ import { eq as eq27, desc as desc12, and as and14, or as or2 } from "drizzle-orm";
14700
15099
  import crypto25 from "crypto";
14701
15100
  var log9 = createLogger("Notifier");
14702
15101
  var Notifier = class {
@@ -14709,18 +15108,18 @@ var Notifier = class {
14709
15108
  /** Called after a run completes (success, partial, or failed). */
14710
15109
  async onRunCompleted(runId, projectId) {
14711
15110
  log9.info("run.completed", { runId, projectId });
14712
- const notifs = this.db.select().from(notifications).where(eq26(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15111
+ const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14713
15112
  if (notifs.length === 0) {
14714
15113
  log9.info("notifications.none-enabled", { projectId });
14715
15114
  return;
14716
15115
  }
14717
15116
  log9.info("notifications.found", { projectId, count: notifs.length });
14718
- const run = this.db.select().from(runs).where(eq26(runs.id, runId)).get();
15117
+ const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
14719
15118
  if (!run) {
14720
15119
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
14721
15120
  return;
14722
15121
  }
14723
- const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
15122
+ const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
14724
15123
  if (!project) {
14725
15124
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
14726
15125
  return;
@@ -14767,11 +15166,11 @@ var Notifier = class {
14767
15166
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
14768
15167
  if (highInsights.length > 0) insightEvents.push("insight.high");
14769
15168
  if (insightEvents.length === 0) return;
14770
- const notifs = this.db.select().from(notifications).where(eq26(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
15169
+ const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
14771
15170
  if (notifs.length === 0) return;
14772
- const run = this.db.select().from(runs).where(eq26(runs.id, runId)).get();
15171
+ const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
14773
15172
  if (!run) return;
14774
- const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
15173
+ const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
14775
15174
  if (!project) return;
14776
15175
  for (const notif of notifs) {
14777
15176
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -14802,11 +15201,11 @@ var Notifier = class {
14802
15201
  }
14803
15202
  computeTransitions(runId, projectId) {
14804
15203
  const recentRuns = this.db.select().from(runs).where(
14805
- and13(
14806
- eq26(runs.projectId, projectId),
14807
- or2(eq26(runs.status, "completed"), eq26(runs.status, "partial"))
15204
+ and14(
15205
+ eq27(runs.projectId, projectId),
15206
+ or2(eq27(runs.status, "completed"), eq27(runs.status, "partial"))
14808
15207
  )
14809
- ).orderBy(desc11(runs.createdAt)).limit(2).all();
15208
+ ).orderBy(desc12(runs.createdAt)).limit(2).all();
14810
15209
  if (recentRuns.length < 2) return [];
14811
15210
  const currentRunId = recentRuns[0].id;
14812
15211
  const previousRunId = recentRuns[1].id;
@@ -14816,12 +15215,12 @@ var Notifier = class {
14816
15215
  keyword: keywords.keyword,
14817
15216
  provider: querySnapshots.provider,
14818
15217
  citationState: querySnapshots.citationState
14819
- }).from(querySnapshots).leftJoin(keywords, eq26(querySnapshots.keywordId, keywords.id)).where(eq26(querySnapshots.runId, currentRunId)).all();
15218
+ }).from(querySnapshots).leftJoin(keywords, eq27(querySnapshots.keywordId, keywords.id)).where(eq27(querySnapshots.runId, currentRunId)).all();
14820
15219
  const previousSnapshots = this.db.select({
14821
15220
  keywordId: querySnapshots.keywordId,
14822
15221
  provider: querySnapshots.provider,
14823
15222
  citationState: querySnapshots.citationState
14824
- }).from(querySnapshots).where(eq26(querySnapshots.runId, previousRunId)).all();
15223
+ }).from(querySnapshots).where(eq27(querySnapshots.runId, previousRunId)).all();
14825
15224
  const prevMap = /* @__PURE__ */ new Map();
14826
15225
  for (const s of previousSnapshots) {
14827
15226
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -14938,7 +15337,7 @@ var RunCoordinator = class {
14938
15337
 
14939
15338
  // src/agent/session-registry.ts
14940
15339
  import crypto27 from "crypto";
14941
- import { eq as eq28 } from "drizzle-orm";
15340
+ import { eq as eq29 } from "drizzle-orm";
14942
15341
 
14943
15342
  // src/agent/session.ts
14944
15343
  import fs11 from "fs";
@@ -15158,7 +15557,7 @@ import { Type as Type2 } from "@sinclair/typebox";
15158
15557
 
15159
15558
  // src/agent/memory-store.ts
15160
15559
  import crypto26 from "crypto";
15161
- import { and as and14, desc as desc12, eq as eq27, like, sql as sql9 } from "drizzle-orm";
15560
+ import { and as and15, desc as desc13, eq as eq28, like, sql as sql9 } from "drizzle-orm";
15162
15561
  var COMPACTION_KEY_PREFIX = "compaction:";
15163
15562
  var COMPACTION_NOTES_PER_SESSION = 3;
15164
15563
  function rowToDto(row) {
@@ -15172,7 +15571,7 @@ function rowToDto(row) {
15172
15571
  };
15173
15572
  }
15174
15573
  function listMemoryEntries(db, projectId, opts = {}) {
15175
- const query = db.select().from(agentMemory).where(eq27(agentMemory.projectId, projectId)).orderBy(desc12(agentMemory.updatedAt));
15574
+ const query = db.select().from(agentMemory).where(eq28(agentMemory.projectId, projectId)).orderBy(desc13(agentMemory.updatedAt));
15176
15575
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
15177
15576
  return rows.map(rowToDto);
15178
15577
  }
@@ -15203,12 +15602,12 @@ function upsertMemoryEntry(db, args) {
15203
15602
  updatedAt: now
15204
15603
  }
15205
15604
  }).run();
15206
- const row = db.select().from(agentMemory).where(and14(eq27(agentMemory.projectId, args.projectId), eq27(agentMemory.key, args.key))).get();
15605
+ const row = db.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, args.key))).get();
15207
15606
  if (!row) throw new Error("memory upsert produced no row");
15208
15607
  return rowToDto(row);
15209
15608
  }
15210
15609
  function deleteMemoryEntry(db, projectId, key) {
15211
- const result = db.delete(agentMemory).where(and14(eq27(agentMemory.projectId, projectId), eq27(agentMemory.key, key))).run();
15610
+ const result = db.delete(agentMemory).where(and15(eq28(agentMemory.projectId, projectId), eq28(agentMemory.key, key))).run();
15212
15611
  const changes = result.changes ?? 0;
15213
15612
  return changes > 0;
15214
15613
  }
@@ -15237,16 +15636,16 @@ function writeCompactionNote(db, args) {
15237
15636
  }).run();
15238
15637
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
15239
15638
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
15240
- and14(
15241
- eq27(agentMemory.projectId, args.projectId),
15639
+ and15(
15640
+ eq28(agentMemory.projectId, args.projectId),
15242
15641
  like(agentMemory.key, `${sessionPrefix}%`)
15243
15642
  )
15244
- ).orderBy(desc12(agentMemory.updatedAt)).all();
15643
+ ).orderBy(desc13(agentMemory.updatedAt)).all();
15245
15644
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
15246
15645
  if (stale.length > 0) {
15247
15646
  tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
15248
15647
  }
15249
- const row = tx.select().from(agentMemory).where(and14(eq27(agentMemory.projectId, args.projectId), eq27(agentMemory.key, key))).get();
15648
+ const row = tx.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, key))).get();
15250
15649
  if (row) inserted = rowToDto(row);
15251
15650
  });
15252
15651
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -15295,7 +15694,7 @@ function buildGetHealthTool(ctx) {
15295
15694
  return {
15296
15695
  name: "get_health",
15297
15696
  label: "Get health",
15298
- description: "Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown.",
15697
+ description: 'Latest visibility health snapshot including overall cited rate, pair counts, and per-provider breakdown. Returns `status: "no-data"` with `reason: "no-runs-yet"` and zeroed metrics for projects with no successful runs yet.',
15299
15698
  parameters: HealthSchema,
15300
15699
  execute: async () => {
15301
15700
  const health = await ctx.client.getHealth(ctx.projectName);
@@ -15376,6 +15775,59 @@ function buildListCompetitorsTool(ctx) {
15376
15775
  }
15377
15776
  };
15378
15777
  }
15778
+ var ContentTargetsSchema = Type2.Object({
15779
+ limit: Type2.Optional(
15780
+ Type2.Number({
15781
+ description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
15782
+ })
15783
+ ),
15784
+ includeInProgress: Type2.Optional(
15785
+ Type2.Boolean({
15786
+ description: "Include rows that already have an in-flight tracked action. Default false."
15787
+ })
15788
+ )
15789
+ });
15790
+ function buildGetContentTargetsTool(ctx) {
15791
+ return {
15792
+ name: "get_content_targets",
15793
+ label: "Get content targets",
15794
+ description: "Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence}`. Use this to recommend which post the user should write/refresh next.",
15795
+ parameters: ContentTargetsSchema,
15796
+ execute: async (_toolCallId, params) => {
15797
+ const response = await ctx.client.getContentTargets(ctx.projectName, {
15798
+ limit: params.limit,
15799
+ includeInProgress: params.includeInProgress === true
15800
+ });
15801
+ return textResult2(response);
15802
+ }
15803
+ };
15804
+ }
15805
+ var ContentSourcesSchema = Type2.Object({});
15806
+ function buildGetContentSourcesTool(ctx) {
15807
+ return {
15808
+ name: "get_grounding_sources",
15809
+ label: "Get grounding sources",
15810
+ description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors), with citation count and providers. Read this to understand what specific competitor URL is winning a query.",
15811
+ parameters: ContentSourcesSchema,
15812
+ execute: async () => {
15813
+ const response = await ctx.client.getContentSources(ctx.projectName);
15814
+ return textResult2(response);
15815
+ }
15816
+ };
15817
+ }
15818
+ var ContentGapsSchema = Type2.Object({});
15819
+ function buildGetContentGapsTool(ctx) {
15820
+ return {
15821
+ name: "get_content_gaps",
15822
+ label: "Get content gaps",
15823
+ description: 'Queries where competitors are cited but our domain is not. Ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use `get_content_targets` for action-typed recommendations on the same data.',
15824
+ parameters: ContentGapsSchema,
15825
+ execute: async () => {
15826
+ const response = await ctx.client.getContentGaps(ctx.projectName);
15827
+ return textResult2(response);
15828
+ }
15829
+ };
15830
+ }
15379
15831
  var RunDetailSchema = Type2.Object({
15380
15832
  runId: Type2.String({
15381
15833
  description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
@@ -15453,7 +15905,10 @@ function buildReadTools(ctx) {
15453
15905
  buildListCompetitorsTool(ctx),
15454
15906
  buildGetRunTool(ctx),
15455
15907
  buildRecallTool(ctx),
15456
- buildListBacklinksTool(ctx)
15908
+ buildListBacklinksTool(ctx),
15909
+ buildGetContentTargetsTool(ctx),
15910
+ buildGetContentSourcesTool(ctx),
15911
+ buildGetContentGapsTool(ctx)
15457
15912
  ];
15458
15913
  }
15459
15914
  var RunSweepSchema = Type2.Object({
@@ -15922,7 +16377,7 @@ var SessionRegistry = class {
15922
16377
  modelProvider: effectiveProvider,
15923
16378
  modelId: effectiveModelId,
15924
16379
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15925
- }).where(eq28(agentSessions.projectId, projectId)).run();
16380
+ }).where(eq29(agentSessions.projectId, projectId)).run();
15926
16381
  }
15927
16382
  const agent2 = createAeroSession({
15928
16383
  projectName,
@@ -16140,7 +16595,7 @@ ${lines.join("\n")}
16140
16595
  modelProvider: nextProvider,
16141
16596
  modelId: nextModelId,
16142
16597
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16143
- }).where(eq28(agentSessions.projectId, projectId)).run();
16598
+ }).where(eq29(agentSessions.projectId, projectId)).run();
16144
16599
  }
16145
16600
  /** Persist a session's transcript back to the DB. Call after any run settles. */
16146
16601
  save(projectName) {
@@ -16302,11 +16757,11 @@ ${lines.join("\n")}
16302
16757
  return id;
16303
16758
  }
16304
16759
  tryResolveProjectId(projectName) {
16305
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq28(projects.name, projectName)).get();
16760
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq29(projects.name, projectName)).get();
16306
16761
  return row?.id;
16307
16762
  }
16308
16763
  loadRow(projectId) {
16309
- const row = this.opts.db.select().from(agentSessions).where(eq28(agentSessions.projectId, projectId)).get();
16764
+ const row = this.opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, projectId)).get();
16310
16765
  return row ?? null;
16311
16766
  }
16312
16767
  insertRow(params) {
@@ -16325,14 +16780,14 @@ ${lines.join("\n")}
16325
16780
  }
16326
16781
  updateRow(projectId, patch) {
16327
16782
  const now = (/* @__PURE__ */ new Date()).toISOString();
16328
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq28(agentSessions.projectId, projectId)).run();
16783
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq29(agentSessions.projectId, projectId)).run();
16329
16784
  }
16330
16785
  };
16331
16786
 
16332
16787
  // src/agent/agent-routes.ts
16333
- import { eq as eq29 } from "drizzle-orm";
16788
+ import { eq as eq30 } from "drizzle-orm";
16334
16789
  function resolveProject2(db, name) {
16335
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq29(projects.name, name)).get();
16790
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq30(projects.name, name)).get();
16336
16791
  if (!row) throw notFound("project", name);
16337
16792
  return row;
16338
16793
  }
@@ -16341,7 +16796,7 @@ function registerAgentRoutes(app, opts) {
16341
16796
  "/projects/:name/agent/transcript",
16342
16797
  async (request) => {
16343
16798
  const project = resolveProject2(opts.db, request.params.name);
16344
- const row = opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, project.id)).get();
16799
+ const row = opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, project.id)).get();
16345
16800
  if (!row) {
16346
16801
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
16347
16802
  }
@@ -16365,7 +16820,7 @@ function registerAgentRoutes(app, opts) {
16365
16820
  async (request) => {
16366
16821
  const project = resolveProject2(opts.db, request.params.name);
16367
16822
  opts.sessionRegistry.reset(project.name);
16368
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(agentSessions.projectId, project.id)).run();
16823
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(agentSessions.projectId, project.id)).run();
16369
16824
  return { status: "reset" };
16370
16825
  }
16371
16826
  );
@@ -16669,7 +17124,7 @@ var SnapshotService = class {
16669
17124
  }
16670
17125
  async createReport(input) {
16671
17126
  const companyName = input.companyName.trim();
16672
- const domain = normalizeDomain(input.domain);
17127
+ const domain = normalizeDomain2(input.domain);
16673
17128
  const manualPhrases = normalizeStringList(input.phrases ?? []);
16674
17129
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
16675
17130
  const providers = this.registry.getAll();
@@ -17109,7 +17564,7 @@ function extractCompetitorsFromResponse(ctx) {
17109
17564
  const targetDomain = extractHostname2(ctx.targetDomain);
17110
17565
  for (const hint of ctx.manualCompetitors) {
17111
17566
  if (isDomainLike(hint)) {
17112
- const normalizedHint = normalizeDomain(hint);
17567
+ const normalizedHint = normalizeDomain2(hint);
17113
17568
  if (domainMatches2(normalizedHint, targetDomain)) continue;
17114
17569
  if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
17115
17570
  competitors2.add(normalizedHint);
@@ -17168,7 +17623,7 @@ function uniqueStrings2(values) {
17168
17623
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
17169
17624
  )];
17170
17625
  }
17171
- function normalizeDomain(value) {
17626
+ function normalizeDomain2(value) {
17172
17627
  const trimmed = value.trim();
17173
17628
  if (!trimmed) return trimmed;
17174
17629
  try {
@@ -17179,15 +17634,15 @@ function normalizeDomain(value) {
17179
17634
  }
17180
17635
  }
17181
17636
  function extractHostname2(value) {
17182
- return normalizeDomain(value);
17637
+ return normalizeDomain2(value);
17183
17638
  }
17184
17639
  function domainMatches2(candidate, target) {
17185
- const normalizedCandidate = normalizeDomain(candidate);
17186
- const normalizedTarget = normalizeDomain(target);
17640
+ const normalizedCandidate = normalizeDomain2(candidate);
17641
+ const normalizedTarget = normalizeDomain2(target);
17187
17642
  return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
17188
17643
  }
17189
17644
  function isDomainLike(value) {
17190
- const normalized = normalizeDomain(value);
17645
+ const normalized = normalizeDomain2(value);
17191
17646
  return normalized.includes(".") && !normalized.includes(" ");
17192
17647
  }
17193
17648
  function clipText(value, length) {
@@ -17387,7 +17842,7 @@ async function createServer(opts) {
17387
17842
  intelligenceService,
17388
17843
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
17389
17844
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
17390
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq30(projects.id, projectId)).get();
17845
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq31(projects.id, projectId)).get();
17391
17846
  if (!project) return;
17392
17847
  sessionRegistry.queueFollowUp(project.name, {
17393
17848
  role: "user",
@@ -17527,7 +17982,7 @@ async function createServer(opts) {
17527
17982
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
17528
17983
  if (opts.config.apiKey) {
17529
17984
  const keyHash = hashApiKey(opts.config.apiKey);
17530
- const existing = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, keyHash)).get();
17985
+ const existing = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, keyHash)).get();
17531
17986
  if (!existing) {
17532
17987
  const prefix = opts.config.apiKey.slice(0, 12);
17533
17988
  opts.db.insert(apiKeys).values({
@@ -17579,7 +18034,7 @@ async function createServer(opts) {
17579
18034
  };
17580
18035
  const getDefaultApiKey = () => {
17581
18036
  if (!opts.config.apiKey) return void 0;
17582
- return opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
18037
+ return opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
17583
18038
  };
17584
18039
  const createPasswordSession = (reply) => {
17585
18040
  const key = getDefaultApiKey();
@@ -17636,12 +18091,12 @@ async function createServer(opts) {
17636
18091
  return reply.send({ authenticated: true });
17637
18092
  }
17638
18093
  if (apiKey) {
17639
- const key = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey(apiKey))).get();
18094
+ const key = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(apiKey))).get();
17640
18095
  if (!key || key.revokedAt) {
17641
18096
  const err2 = authInvalid();
17642
18097
  return reply.status(err2.statusCode).send(err2.toJSON());
17643
18098
  }
17644
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(apiKeys.id, key.id)).run();
18099
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(apiKeys.id, key.id)).run();
17645
18100
  const sessionId = createSession(key.id);
17646
18101
  reply.header("set-cookie", serializeSessionCookie({
17647
18102
  name: SESSION_COOKIE_NAME,