@ainyc/canonry 3.3.2 → 3.3.8

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.
@@ -64,7 +64,7 @@ import {
64
64
  visibilityStateFromAnswerMentioned,
65
65
  windowCutoff,
66
66
  wordpressEnvSchema
67
- } from "./chunk-ALMP3NBQ.js";
67
+ } from "./chunk-24C7RMIS.js";
68
68
  import {
69
69
  IntelligenceService,
70
70
  agentMemory,
@@ -182,7 +182,7 @@ import crypto28 from "crypto";
182
182
  import fs12 from "fs";
183
183
  import path14 from "path";
184
184
  import { fileURLToPath as fileURLToPath2 } from "url";
185
- import { eq as eq33 } from "drizzle-orm";
185
+ import { eq as eq34 } from "drizzle-orm";
186
186
  import Fastify from "fastify";
187
187
 
188
188
  // ../api-routes/src/auth.ts
@@ -2385,17 +2385,654 @@ async function intelligenceRoutes(app) {
2385
2385
  });
2386
2386
  }
2387
2387
 
2388
+ // ../api-routes/src/report.ts
2389
+ import { and as and3, desc as desc5, eq as eq12 } from "drizzle-orm";
2390
+ var TOP_QUERIES_LIMIT = 20;
2391
+ var TOP_LANDING_PAGES_LIMIT = 20;
2392
+ var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
2393
+ var TOP_SOURCE_DOMAINS_LIMIT = 20;
2394
+ var TOP_CAMPAIGN_LIMIT = 10;
2395
+ function safeNum(value) {
2396
+ if (typeof value === "number") return value;
2397
+ if (typeof value === "string") {
2398
+ const parsed = Number(value);
2399
+ return Number.isFinite(parsed) ? parsed : 0;
2400
+ }
2401
+ return 0;
2402
+ }
2403
+ function citedDomainBelongsToProject(citedDomain, projectDomains) {
2404
+ const candidate = normalizeProjectDomain(citedDomain);
2405
+ for (const domain of projectDomains) {
2406
+ const normalized = normalizeProjectDomain(domain);
2407
+ if (candidate === normalized || candidate.endsWith(`.${normalized}`)) return true;
2408
+ }
2409
+ return false;
2410
+ }
2411
+ function categorizeQuery(query, projectName, canonicalDomain) {
2412
+ const lower = query.toLowerCase();
2413
+ const projectTokens = [
2414
+ projectName.toLowerCase(),
2415
+ canonicalDomain.toLowerCase().replace(/\.[^.]+$/, "")
2416
+ ].filter((t) => t.length >= 3);
2417
+ if (projectTokens.some((token) => lower.includes(token))) return "brand";
2418
+ if (/\b(buy|price|pricing|cost|hire|near me|services?|agency|consultant|company)\b/.test(lower)) {
2419
+ return "lead-gen";
2420
+ }
2421
+ if (/\b(what|how|why|when|guide|tutorial|vs|versus|alternatives?|examples?|definition)\b/.test(lower)) {
2422
+ return "industry";
2423
+ }
2424
+ return "other";
2425
+ }
2426
+ function loadSnapshotsForRun(db, runId) {
2427
+ const rows = db.select().from(querySnapshots).where(eq12(querySnapshots.runId, runId)).all();
2428
+ return rows.map((r) => ({
2429
+ id: r.id,
2430
+ runId: r.runId,
2431
+ keywordId: r.keywordId,
2432
+ provider: r.provider,
2433
+ model: r.model,
2434
+ citationState: r.citationState,
2435
+ answerMentioned: r.answerMentioned,
2436
+ answerText: r.answerText,
2437
+ citedDomains: parseJsonColumn(r.citedDomains, []),
2438
+ competitorOverlap: parseJsonColumn(r.competitorOverlap, []),
2439
+ createdAt: r.createdAt
2440
+ }));
2441
+ }
2442
+ function loadKeywordLookup(db, projectId) {
2443
+ const rows = db.select().from(keywords).where(eq12(keywords.projectId, projectId)).all();
2444
+ const byId = /* @__PURE__ */ new Map();
2445
+ for (const row of rows) byId.set(row.id, row.keyword);
2446
+ return { byId };
2447
+ }
2448
+ function buildCitationScorecard(snapshots, keywordLookup) {
2449
+ if (snapshots.length === 0) {
2450
+ return { keywords: [], providers: [], matrix: [], providerRates: [] };
2451
+ }
2452
+ const keywordSet = /* @__PURE__ */ new Set();
2453
+ const providerSet = /* @__PURE__ */ new Set();
2454
+ for (const snap of snapshots) {
2455
+ const kw = keywordLookup.byId.get(snap.keywordId);
2456
+ if (!kw) continue;
2457
+ keywordSet.add(kw);
2458
+ providerSet.add(snap.provider);
2459
+ }
2460
+ const keywordList = [...keywordSet].sort();
2461
+ const providerList = [...providerSet].sort();
2462
+ const matrix = keywordList.map(
2463
+ () => providerList.map(() => null)
2464
+ );
2465
+ const providerCounts = /* @__PURE__ */ new Map();
2466
+ for (const snap of snapshots) {
2467
+ const kw = keywordLookup.byId.get(snap.keywordId);
2468
+ if (!kw) continue;
2469
+ const ki = keywordList.indexOf(kw);
2470
+ const pi = providerList.indexOf(snap.provider);
2471
+ if (ki < 0 || pi < 0) continue;
2472
+ matrix[ki][pi] = {
2473
+ citationState: snap.citationState === "cited" ? "cited" : "not-cited",
2474
+ answerMentioned: snap.answerMentioned ?? null,
2475
+ model: snap.model
2476
+ };
2477
+ const counts = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
2478
+ counts.total++;
2479
+ if (snap.citationState === "cited") counts.cited++;
2480
+ providerCounts.set(snap.provider, counts);
2481
+ }
2482
+ const providerRates = providerList.map((provider) => {
2483
+ const counts = providerCounts.get(provider) ?? { cited: 0, total: 0 };
2484
+ const citationRate = counts.total > 0 ? Math.round(counts.cited / counts.total * 100) : 0;
2485
+ return {
2486
+ provider,
2487
+ citedCount: counts.cited,
2488
+ totalCount: counts.total,
2489
+ citationRate
2490
+ };
2491
+ });
2492
+ return { keywords: keywordList, providers: providerList, matrix, providerRates };
2493
+ }
2494
+ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains, keywordLookup) {
2495
+ let projectCitationCount = 0;
2496
+ const competitorMap = /* @__PURE__ */ new Map();
2497
+ for (const c of competitorDomains) competitorMap.set(c, { count: 0, keywords: /* @__PURE__ */ new Set() });
2498
+ for (const snap of snapshots) {
2499
+ const kw = keywordLookup.byId.get(snap.keywordId);
2500
+ const allDomains = [...snap.citedDomains, ...snap.competitorOverlap];
2501
+ if (allDomains.some((d) => citedDomainBelongsToProject(d, projectDomains))) {
2502
+ projectCitationCount++;
2503
+ }
2504
+ for (const competitor of competitorDomains) {
2505
+ if (allDomains.some((d) => citedDomainBelongsToProject(d, [competitor]))) {
2506
+ const entry = competitorMap.get(competitor);
2507
+ entry.count++;
2508
+ if (kw) entry.keywords.add(kw);
2509
+ }
2510
+ }
2511
+ }
2512
+ const competitorRows = [...competitorMap.entries()].map(([domain, data]) => {
2513
+ const total = snapshots.length;
2514
+ const ratio = total > 0 ? data.count / total : 0;
2515
+ let pressureLabel = "None";
2516
+ if (data.count > 0) {
2517
+ if (ratio >= 0.5) pressureLabel = "High";
2518
+ else if (ratio >= 0.2) pressureLabel = "Moderate";
2519
+ else pressureLabel = "Low";
2520
+ }
2521
+ return {
2522
+ domain,
2523
+ citationCount: data.count,
2524
+ totalCount: total,
2525
+ pressureLabel,
2526
+ citedKeywords: [...data.keywords].sort()
2527
+ };
2528
+ });
2529
+ competitorRows.sort((a, b) => b.citationCount - a.citationCount);
2530
+ return { projectCitationCount, competitors: competitorRows };
2531
+ }
2532
+ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains) {
2533
+ const categoryCounts = /* @__PURE__ */ new Map();
2534
+ const domainCounts = /* @__PURE__ */ new Map();
2535
+ let totalCitations = 0;
2536
+ for (const snap of snapshots) {
2537
+ for (const raw of snap.citedDomains) {
2538
+ if (citedDomainBelongsToProject(raw, projectDomains)) continue;
2539
+ const { category, label, domain } = categorizeSource(raw);
2540
+ const cat = categoryCounts.get(category) ?? { label, count: 0 };
2541
+ cat.count++;
2542
+ categoryCounts.set(category, cat);
2543
+ domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1);
2544
+ totalCitations++;
2545
+ }
2546
+ }
2547
+ const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
2548
+ category,
2549
+ label,
2550
+ count,
2551
+ sharePct: totalCitations > 0 ? Math.round(count / totalCitations * 100) : 0
2552
+ })).sort((a, b) => b.count - a.count);
2553
+ const topDomains = [...domainCounts.entries()].map(([domain, count]) => ({
2554
+ domain,
2555
+ count,
2556
+ isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
2557
+ })).sort((a, b) => b.count - a.count).slice(0, TOP_SOURCE_DOMAINS_LIMIT);
2558
+ return { categories, topDomains };
2559
+ }
2560
+ function buildGscSection(db, projectId, projectName, canonicalDomain) {
2561
+ const rows = db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, projectId)).all();
2562
+ if (rows.length === 0) return null;
2563
+ let totalClicks = 0;
2564
+ let totalImpressions = 0;
2565
+ let weightedPositionSum = 0;
2566
+ const queryAgg = /* @__PURE__ */ new Map();
2567
+ const trendAgg = /* @__PURE__ */ new Map();
2568
+ for (const r of rows) {
2569
+ totalClicks += r.clicks;
2570
+ totalImpressions += r.impressions;
2571
+ weightedPositionSum += safeNum(r.position) * r.impressions;
2572
+ const q = queryAgg.get(r.query) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
2573
+ q.clicks += r.clicks;
2574
+ q.impressions += r.impressions;
2575
+ q.weightedPositionSum += safeNum(r.position) * r.impressions;
2576
+ queryAgg.set(r.query, q);
2577
+ const t = trendAgg.get(r.date) ?? { clicks: 0, impressions: 0 };
2578
+ t.clicks += r.clicks;
2579
+ t.impressions += r.impressions;
2580
+ trendAgg.set(r.date, t);
2581
+ }
2582
+ const ctr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;
2583
+ const avgPosition = totalImpressions > 0 ? weightedPositionSum / totalImpressions : 0;
2584
+ const topQueries = [...queryAgg.entries()].map(([query, agg]) => ({
2585
+ query,
2586
+ clicks: agg.clicks,
2587
+ impressions: agg.impressions,
2588
+ ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
2589
+ avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
2590
+ category: categorizeQuery(query, projectName, canonicalDomain)
2591
+ })).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
2592
+ const categoryAgg = /* @__PURE__ */ new Map();
2593
+ for (const [query, agg] of queryAgg) {
2594
+ const cat = categorizeQuery(query, projectName, canonicalDomain);
2595
+ const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
2596
+ bucket.clicks += agg.clicks;
2597
+ bucket.impressions += agg.impressions;
2598
+ categoryAgg.set(cat, bucket);
2599
+ }
2600
+ const categoryBreakdown = [...categoryAgg.entries()].map(([category, agg]) => ({
2601
+ category,
2602
+ clicks: agg.clicks,
2603
+ impressions: agg.impressions,
2604
+ sharePct: totalClicks > 0 ? Math.round(agg.clicks / totalClicks * 100) : 0
2605
+ })).sort((a, b) => b.clicks - a.clicks);
2606
+ const trend = [...trendAgg.entries()].map(([date, agg]) => ({ date, clicks: agg.clicks, impressions: agg.impressions })).sort((a, b) => a.date.localeCompare(b.date));
2607
+ return {
2608
+ totalClicks,
2609
+ totalImpressions,
2610
+ ctr,
2611
+ avgPosition,
2612
+ topQueries,
2613
+ categoryBreakdown,
2614
+ trend
2615
+ };
2616
+ }
2617
+ function buildGaSection(db, projectId) {
2618
+ const summaryRow = db.select().from(gaTrafficSummaries).where(eq12(gaTrafficSummaries.projectId, projectId)).orderBy(desc5(gaTrafficSummaries.syncedAt)).limit(1).get();
2619
+ const snapshotRows = db.select().from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
2620
+ if (!summaryRow && snapshotRows.length === 0) return null;
2621
+ const totalSessions = summaryRow?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
2622
+ const totalUsers = summaryRow?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
2623
+ const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? snapshotRows.reduce((s, r) => s + r.organicSessions, 0);
2624
+ const pageAgg = /* @__PURE__ */ new Map();
2625
+ let directSessions = 0;
2626
+ for (const r of snapshotRows) {
2627
+ const page = r.landingPageNormalized ?? r.landingPage;
2628
+ const existing = pageAgg.get(page) ?? { sessions: 0, users: 0, organic: 0 };
2629
+ existing.sessions += r.sessions;
2630
+ existing.users += r.users;
2631
+ existing.organic += r.organicSessions;
2632
+ pageAgg.set(page, existing);
2633
+ if (r.directSessions != null) directSessions += r.directSessions;
2634
+ }
2635
+ const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({
2636
+ page,
2637
+ sessions: data.sessions,
2638
+ users: data.users,
2639
+ organicSessions: data.organic
2640
+ })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_LANDING_PAGES_LIMIT);
2641
+ const channelBreakdown = [];
2642
+ if (totalSessions > 0) {
2643
+ const organic = totalOrganicSessions;
2644
+ const direct = directSessions;
2645
+ const other = Math.max(totalSessions - organic - direct, 0);
2646
+ const buckets = [
2647
+ { channel: "Organic Search", sessions: organic },
2648
+ { channel: "Direct", sessions: direct },
2649
+ { channel: "Other", sessions: other }
2650
+ ];
2651
+ for (const b of buckets) {
2652
+ if (b.sessions > 0) {
2653
+ channelBreakdown.push({
2654
+ channel: b.channel,
2655
+ sessions: b.sessions,
2656
+ sharePct: Math.round(b.sessions / totalSessions * 100)
2657
+ });
2658
+ }
2659
+ }
2660
+ }
2661
+ return {
2662
+ totalSessions,
2663
+ totalUsers,
2664
+ totalOrganicSessions,
2665
+ periodStart: summaryRow?.periodStart ?? "",
2666
+ periodEnd: summaryRow?.periodEnd ?? "",
2667
+ topLandingPages,
2668
+ channelBreakdown
2669
+ };
2670
+ }
2671
+ function buildSocialReferrals(db, projectId) {
2672
+ const rows = db.select().from(gaSocialReferrals).where(eq12(gaSocialReferrals.projectId, projectId)).all();
2673
+ if (rows.length === 0) return null;
2674
+ let total = 0;
2675
+ let organic = 0;
2676
+ let paid = 0;
2677
+ const channelAgg = /* @__PURE__ */ new Map();
2678
+ const campaignAgg = /* @__PURE__ */ new Map();
2679
+ for (const r of rows) {
2680
+ total += r.sessions;
2681
+ if (r.channelGroup === "Paid Social") paid += r.sessions;
2682
+ else organic += r.sessions;
2683
+ channelAgg.set(r.channelGroup, (channelAgg.get(r.channelGroup) ?? 0) + r.sessions);
2684
+ const key = `${r.source}::${r.medium}`;
2685
+ const existing = campaignAgg.get(key) ?? { source: r.source, medium: r.medium, sessions: 0 };
2686
+ existing.sessions += r.sessions;
2687
+ campaignAgg.set(key, existing);
2688
+ }
2689
+ const channels = [...channelAgg.entries()].map(([channelGroup, sessions]) => ({
2690
+ channelGroup,
2691
+ sessions,
2692
+ sharePct: total > 0 ? Math.round(sessions / total * 100) : 0
2693
+ })).sort((a, b) => b.sessions - a.sessions);
2694
+ const topCampaigns = [...campaignAgg.values()].sort((a, b) => b.sessions - a.sessions).slice(0, TOP_CAMPAIGN_LIMIT);
2695
+ return {
2696
+ totalSessions: total,
2697
+ organicSessions: organic,
2698
+ paidSessions: paid,
2699
+ channels,
2700
+ topCampaigns
2701
+ };
2702
+ }
2703
+ function buildAiReferrals(db, projectId) {
2704
+ const rows = db.select().from(gaAiReferrals).where(eq12(gaAiReferrals.projectId, projectId)).all();
2705
+ if (rows.length === 0) return null;
2706
+ const dimSessionsByTuple = /* @__PURE__ */ new Map();
2707
+ for (const r of rows) {
2708
+ const tupleKey = `${r.date}::${r.source}::${r.medium}`;
2709
+ let dimMap = dimSessionsByTuple.get(tupleKey);
2710
+ if (!dimMap) {
2711
+ dimMap = /* @__PURE__ */ new Map();
2712
+ dimSessionsByTuple.set(tupleKey, dimMap);
2713
+ }
2714
+ dimMap.set(r.sourceDimension, (dimMap.get(r.sourceDimension) ?? 0) + r.sessions);
2715
+ }
2716
+ const winningDimension = /* @__PURE__ */ new Map();
2717
+ for (const [tupleKey, dimMap] of dimSessionsByTuple) {
2718
+ let bestDim;
2719
+ let bestSessions = -1;
2720
+ for (const [dim, sessions] of dimMap) {
2721
+ if (sessions > bestSessions) {
2722
+ bestSessions = sessions;
2723
+ bestDim = dim;
2724
+ }
2725
+ }
2726
+ if (bestDim) winningDimension.set(tupleKey, bestDim);
2727
+ }
2728
+ const dedupedRows = rows.filter(
2729
+ (r) => winningDimension.get(`${r.date}::${r.source}::${r.medium}`) === r.sourceDimension
2730
+ );
2731
+ let total = 0;
2732
+ let totalUsers = 0;
2733
+ const sourceAgg = /* @__PURE__ */ new Map();
2734
+ const trendAgg = /* @__PURE__ */ new Map();
2735
+ const pageAgg = /* @__PURE__ */ new Map();
2736
+ for (const r of dedupedRows) {
2737
+ total += r.sessions;
2738
+ totalUsers += r.users;
2739
+ const s = sourceAgg.get(r.source) ?? { sessions: 0, users: 0 };
2740
+ s.sessions += r.sessions;
2741
+ s.users += r.users;
2742
+ sourceAgg.set(r.source, s);
2743
+ trendAgg.set(r.date, (trendAgg.get(r.date) ?? 0) + r.sessions);
2744
+ const page = r.landingPageNormalized ?? r.landingPage;
2745
+ const p = pageAgg.get(page) ?? { sessions: 0, users: 0 };
2746
+ p.sessions += r.sessions;
2747
+ p.users += r.users;
2748
+ pageAgg.set(page, p);
2749
+ }
2750
+ const bySource = [...sourceAgg.entries()].map(([source, data]) => ({
2751
+ source,
2752
+ sessions: data.sessions,
2753
+ users: data.users,
2754
+ sharePct: total > 0 ? Math.round(data.sessions / total * 100) : 0
2755
+ })).sort((a, b) => b.sessions - a.sessions);
2756
+ const trend = [...trendAgg.entries()].map(([date, sessions]) => ({ date, sessions })).sort((a, b) => a.date.localeCompare(b.date));
2757
+ const topLandingPages = [...pageAgg.entries()].map(([page, data]) => ({ page, sessions: data.sessions, users: data.users })).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_AI_REFERRAL_PAGES_LIMIT);
2758
+ return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
2759
+ }
2760
+ function buildIndexingHealth(db, projectId) {
2761
+ const gsc = db.select().from(gscCoverageSnapshots).where(eq12(gscCoverageSnapshots.projectId, projectId)).orderBy(desc5(gscCoverageSnapshots.date)).limit(1).get();
2762
+ if (gsc) {
2763
+ const total = gsc.indexed + gsc.notIndexed;
2764
+ return {
2765
+ provider: "google",
2766
+ total,
2767
+ indexed: gsc.indexed,
2768
+ notIndexed: gsc.notIndexed,
2769
+ deindexed: 0,
2770
+ unknown: 0,
2771
+ indexedPct: total > 0 ? Math.round(gsc.indexed / total * 100) : 0
2772
+ };
2773
+ }
2774
+ const bing = db.select().from(bingCoverageSnapshots).where(eq12(bingCoverageSnapshots.projectId, projectId)).orderBy(desc5(bingCoverageSnapshots.date)).limit(1).get();
2775
+ if (bing) {
2776
+ const total = bing.indexed + bing.notIndexed + bing.unknown;
2777
+ return {
2778
+ provider: "bing",
2779
+ total,
2780
+ indexed: bing.indexed,
2781
+ notIndexed: bing.notIndexed,
2782
+ deindexed: 0,
2783
+ unknown: bing.unknown,
2784
+ indexedPct: total > 0 ? Math.round(bing.indexed / total * 100) : 0
2785
+ };
2786
+ }
2787
+ return null;
2788
+ }
2789
+ function buildCitationsTrend(db, projectId, keywordLookup) {
2790
+ const visibilityRuns = db.select().from(runs).where(and3(eq12(runs.projectId, projectId), eq12(runs.kind, RunKinds["answer-visibility"]))).all();
2791
+ const points = [];
2792
+ for (const run of visibilityRuns) {
2793
+ if (run.status !== RunStatuses.completed) continue;
2794
+ const snaps = loadSnapshotsForRun(db, run.id);
2795
+ if (snaps.length === 0) continue;
2796
+ let cited = 0;
2797
+ let considered = 0;
2798
+ const providerCounts = /* @__PURE__ */ new Map();
2799
+ for (const snap of snaps) {
2800
+ if (!keywordLookup.byId.has(snap.keywordId)) continue;
2801
+ considered++;
2802
+ if (snap.citationState === "cited") cited++;
2803
+ const counts = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
2804
+ counts.total++;
2805
+ if (snap.citationState === "cited") counts.cited++;
2806
+ providerCounts.set(snap.provider, counts);
2807
+ }
2808
+ if (considered === 0) continue;
2809
+ const citationRate = Math.round(cited / considered * 100);
2810
+ const providerRates = [...providerCounts.entries()].map(([provider, counts]) => ({
2811
+ provider,
2812
+ citationRate: counts.total > 0 ? Math.round(counts.cited / counts.total * 100) : 0
2813
+ })).sort((a, b) => a.provider.localeCompare(b.provider));
2814
+ points.push({
2815
+ runId: run.id,
2816
+ date: run.finishedAt ?? run.createdAt,
2817
+ citationRate,
2818
+ providerRates
2819
+ });
2820
+ }
2821
+ points.sort((a, b) => a.date.localeCompare(b.date));
2822
+ return points;
2823
+ }
2824
+ function buildInsightList(db, projectId) {
2825
+ const rows = db.select().from(insights).where(eq12(insights.projectId, projectId)).orderBy(desc5(insights.createdAt)).all();
2826
+ const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
2827
+ return rows.filter((r) => !r.dismissed).map((r) => {
2828
+ const recommendation = parseJsonColumn(r.recommendation, null);
2829
+ let recText = null;
2830
+ if (recommendation) {
2831
+ const parts = [];
2832
+ if (recommendation.action) parts.push(recommendation.action);
2833
+ if (recommendation.target) parts.push(recommendation.target);
2834
+ if (recommendation.reason) parts.push(recommendation.reason);
2835
+ if (parts.length > 0) recText = parts.join(" \u2014 ");
2836
+ }
2837
+ return {
2838
+ id: r.id,
2839
+ type: r.type,
2840
+ severity: r.severity,
2841
+ title: r.title,
2842
+ keyword: r.keyword,
2843
+ provider: r.provider,
2844
+ recommendation: recText,
2845
+ createdAt: r.createdAt
2846
+ };
2847
+ }).sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
2848
+ }
2849
+ function buildRecommendedNextSteps(insightList) {
2850
+ const steps = [];
2851
+ const critical = insightList.filter((i) => i.severity === "critical");
2852
+ const high = insightList.filter((i) => i.severity === "high");
2853
+ const medium = insightList.filter((i) => i.severity === "medium");
2854
+ if (critical.length > 0) {
2855
+ steps.push({
2856
+ horizon: "immediate",
2857
+ title: `Resolve ${critical.length} critical regression${critical.length > 1 ? "s" : ""}`,
2858
+ rationale: critical[0].title + (critical.length > 1 ? `, plus ${critical.length - 1} more.` : ".")
2859
+ });
2860
+ }
2861
+ if (high.length > 0) {
2862
+ steps.push({
2863
+ horizon: "short-term",
2864
+ title: `Address ${high.length} high-severity issue${high.length > 1 ? "s" : ""}`,
2865
+ rationale: high[0].title + (high.length > 1 ? `, plus ${high.length - 1} more.` : ".")
2866
+ });
2867
+ }
2868
+ if (medium.length > 0) {
2869
+ steps.push({
2870
+ horizon: "medium-term",
2871
+ title: `Capture ${medium.length} opportunit${medium.length > 1 ? "ies" : "y"}`,
2872
+ rationale: medium[0].title + (medium.length > 1 ? `, plus ${medium.length - 1} more.` : ".")
2873
+ });
2874
+ }
2875
+ return steps;
2876
+ }
2877
+ function buildExecutiveFindings(citationRate, trend, trendsPoints, insightList, competitorRows) {
2878
+ const findings = [];
2879
+ if (trendsPoints.length > 0) {
2880
+ const tone = trend === "up" ? "positive" : trend === "down" ? "negative" : "neutral";
2881
+ let detail;
2882
+ switch (trend) {
2883
+ case "up":
2884
+ detail = "Up from the previous run.";
2885
+ break;
2886
+ case "down":
2887
+ detail = "Down from the previous run.";
2888
+ break;
2889
+ case "flat":
2890
+ detail = "Flat compared to the previous run.";
2891
+ break;
2892
+ case "unknown":
2893
+ detail = "No prior run to compare against.";
2894
+ break;
2895
+ }
2896
+ findings.push({
2897
+ title: `Citation rate at ${citationRate}%`,
2898
+ detail,
2899
+ tone
2900
+ });
2901
+ }
2902
+ const critical = insightList.filter((i) => i.severity === "critical");
2903
+ if (critical.length > 0) {
2904
+ findings.push({
2905
+ title: `${critical.length} critical regression${critical.length > 1 ? "s" : ""}`,
2906
+ detail: critical[0].title,
2907
+ tone: "negative"
2908
+ });
2909
+ }
2910
+ const highPressure = competitorRows.filter((c) => c.pressureLabel === "High");
2911
+ if (highPressure.length > 0) {
2912
+ findings.push({
2913
+ title: `${highPressure.length} competitor${highPressure.length > 1 ? "s" : ""} cited often`,
2914
+ detail: highPressure.map((c) => c.domain).slice(0, 3).join(", "),
2915
+ tone: "caution"
2916
+ });
2917
+ }
2918
+ return findings.slice(0, 5);
2919
+ }
2920
+ async function reportRoutes(app) {
2921
+ app.get("/projects/:name/report", async (request, reply) => {
2922
+ const project = resolveProject(app.db, request.params.name);
2923
+ const keywordLookup = loadKeywordLookup(app.db, project.id);
2924
+ const allRuns = app.db.select().from(runs).where(eq12(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).all();
2925
+ const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
2926
+ const latestRun = visibilityRuns.find(
2927
+ (r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
2928
+ ) ?? visibilityRuns[0];
2929
+ const latestSnapshots = latestRun ? loadSnapshotsForRun(app.db, latestRun.id) : [];
2930
+ const competitorRows = app.db.select().from(competitors).where(eq12(competitors.projectId, project.id)).all();
2931
+ const competitorDomains = competitorRows.map((c) => c.domain);
2932
+ const ownedDomains = parseJsonColumn(project.ownedDomains, []);
2933
+ const projectDomains = [project.canonicalDomain, ...ownedDomains];
2934
+ const citationScorecard = buildCitationScorecard(latestSnapshots, keywordLookup);
2935
+ const competitorLandscape = buildCompetitorLandscape(
2936
+ latestSnapshots,
2937
+ competitorDomains,
2938
+ projectDomains,
2939
+ keywordLookup
2940
+ );
2941
+ const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
2942
+ const gscSection = buildGscSection(app.db, project.id, project.name, project.canonicalDomain);
2943
+ const gaSection = buildGaSection(app.db, project.id);
2944
+ const socialSection = buildSocialReferrals(app.db, project.id);
2945
+ const aiReferralsSection = buildAiReferrals(app.db, project.id);
2946
+ const indexingHealthSection = buildIndexingHealth(app.db, project.id);
2947
+ const citationsTrend = buildCitationsTrend(app.db, project.id, keywordLookup);
2948
+ const insightList = buildInsightList(app.db, project.id);
2949
+ const recommendedNextSteps = buildRecommendedNextSteps(insightList);
2950
+ let latestCited = 0;
2951
+ let latestConsidered = 0;
2952
+ for (const snap of latestSnapshots) {
2953
+ if (!keywordLookup.byId.has(snap.keywordId)) continue;
2954
+ latestConsidered++;
2955
+ if (snap.citationState === "cited") latestCited++;
2956
+ }
2957
+ const citationRate = latestConsidered > 0 ? Math.round(latestCited / latestConsidered * 100) : 0;
2958
+ const latestPoint = citationsTrend.at(-1);
2959
+ const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
2960
+ let trend = "unknown";
2961
+ if (latestPoint && previousPoint) {
2962
+ if (latestPoint.citationRate > previousPoint.citationRate) trend = "up";
2963
+ else if (latestPoint.citationRate < previousPoint.citationRate) trend = "down";
2964
+ else trend = "flat";
2965
+ }
2966
+ const findings = buildExecutiveFindings(
2967
+ citationRate,
2968
+ trend,
2969
+ citationsTrend,
2970
+ insightList,
2971
+ competitorLandscape.competitors
2972
+ );
2973
+ const periodStart = citationsTrend[0]?.date ?? null;
2974
+ const periodEnd = citationsTrend.at(-1)?.date ?? null;
2975
+ const dto = {
2976
+ meta: {
2977
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2978
+ project: {
2979
+ id: project.id,
2980
+ name: project.name,
2981
+ displayName: project.displayName,
2982
+ canonicalDomain: project.canonicalDomain,
2983
+ country: project.country,
2984
+ language: project.language
2985
+ },
2986
+ periodStart,
2987
+ periodEnd
2988
+ },
2989
+ executiveSummary: {
2990
+ citationRate,
2991
+ trend,
2992
+ keywordCount: keywordLookup.byId.size,
2993
+ competitorCount: competitorDomains.length,
2994
+ providerCount: citationScorecard.providers.length,
2995
+ gsc: gscSection ? {
2996
+ clicks: gscSection.totalClicks,
2997
+ impressions: gscSection.totalImpressions,
2998
+ ctr: gscSection.ctr,
2999
+ avgPosition: gscSection.avgPosition
3000
+ } : null,
3001
+ ga: gaSection ? {
3002
+ sessions: gaSection.totalSessions,
3003
+ users: gaSection.totalUsers,
3004
+ periodStart: gaSection.periodStart,
3005
+ periodEnd: gaSection.periodEnd
3006
+ } : null,
3007
+ findings
3008
+ },
3009
+ citationScorecard,
3010
+ competitorLandscape,
3011
+ aiSourceOrigin,
3012
+ gsc: gscSection,
3013
+ ga: gaSection,
3014
+ socialReferrals: socialSection,
3015
+ aiReferrals: aiReferralsSection,
3016
+ indexingHealth: indexingHealthSection,
3017
+ citationsTrend,
3018
+ insights: insightList,
3019
+ recommendedNextSteps
3020
+ };
3021
+ return reply.send(dto);
3022
+ });
3023
+ }
3024
+
2388
3025
  // ../api-routes/src/citations.ts
2389
- import { eq as eq12, inArray as inArray3 } from "drizzle-orm";
3026
+ import { eq as eq13, inArray as inArray3 } from "drizzle-orm";
2390
3027
  async function citationRoutes(app) {
2391
3028
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
2392
3029
  const project = resolveProject(app.db, request.params.name);
2393
3030
  const configuredProviders = parseJsonColumn(project.providers, []);
2394
- const projectKeywords = app.db.select().from(keywords).where(eq12(keywords.projectId, project.id)).all();
3031
+ const projectKeywords = app.db.select().from(keywords).where(eq13(keywords.projectId, project.id)).all();
2395
3032
  if (projectKeywords.length === 0) {
2396
3033
  return reply.send(emptyCitationVisibility("no-keywords"));
2397
3034
  }
2398
- const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq12(runs.projectId, project.id)).all();
3035
+ const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq13(runs.projectId, project.id)).all();
2399
3036
  if (projectRuns.length === 0) {
2400
3037
  return reply.send(emptyCitationVisibility("no-runs-yet"));
2401
3038
  }
@@ -2418,7 +3055,7 @@ async function citationRoutes(app) {
2418
3055
  ...s,
2419
3056
  runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
2420
3057
  }));
2421
- const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, project.id)).all().map((c) => normalizeDomain(c.domain)).filter((d) => d.length > 0);
3058
+ const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, project.id)).all().map((c) => normalizeDomain(c.domain)).filter((d) => d.length > 0);
2422
3059
  const response = computeCitationVisibility({
2423
3060
  keywords: projectKeywords.map((k) => ({ id: k.id, keyword: k.keyword })),
2424
3061
  snapshots,
@@ -2550,21 +3187,21 @@ function normalizeDomain(domain) {
2550
3187
  }
2551
3188
 
2552
3189
  // ../api-routes/src/composites.ts
2553
- import { eq as eq13, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
3190
+ import { eq as eq14, and as and4, desc as desc6, sql as sql3, like, or as or2 } from "drizzle-orm";
2554
3191
  var TOP_INSIGHT_LIMIT = 5;
2555
3192
  var SEARCH_HIT_HARD_LIMIT = 50;
2556
3193
  var SEARCH_SNIPPET_RADIUS = 80;
2557
3194
  async function compositeRoutes(app) {
2558
3195
  app.get("/projects/:name/overview", async (request, reply) => {
2559
3196
  const project = resolveProject(app.db, request.params.name);
2560
- const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq13(runs.projectId, project.id)).get();
3197
+ const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq14(runs.projectId, project.id)).get();
2561
3198
  const totalRuns = totalRunsRow?.count ?? 0;
2562
- const recentRuns = app.db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
3199
+ const recentRuns = app.db.select().from(runs).where(eq14(runs.projectId, project.id)).orderBy(desc6(runs.createdAt)).limit(2).all();
2563
3200
  const [latestRunRow, previousRunRow] = recentRuns;
2564
3201
  const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
2565
- const healthRow = app.db.select().from(healthSnapshots).where(eq13(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
3202
+ const healthRow = app.db.select().from(healthSnapshots).where(eq14(healthSnapshots.projectId, project.id)).orderBy(desc6(healthSnapshots.createdAt)).limit(1).get();
2566
3203
  const health = healthRow ? mapHealthRow2(healthRow) : null;
2567
- const insightRows = app.db.select().from(insights).where(eq13(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
3204
+ const insightRows = app.db.select().from(insights).where(eq14(insights.projectId, project.id)).orderBy(desc6(insights.createdAt)).all();
2568
3205
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
2569
3206
  const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
2570
3207
  const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
@@ -2600,9 +3237,9 @@ async function compositeRoutes(app) {
2600
3237
  citedDomains: querySnapshots.citedDomains,
2601
3238
  rawResponse: querySnapshots.rawResponse,
2602
3239
  createdAt: querySnapshots.createdAt
2603
- }).from(querySnapshots).innerJoin(keywords, eq13(querySnapshots.keywordId, keywords.id)).where(
2604
- and3(
2605
- eq13(keywords.projectId, project.id),
3240
+ }).from(querySnapshots).innerJoin(keywords, eq14(querySnapshots.keywordId, keywords.id)).where(
3241
+ and4(
3242
+ eq14(keywords.projectId, project.id),
2606
3243
  or2(
2607
3244
  sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
2608
3245
  sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
@@ -2610,10 +3247,10 @@ async function compositeRoutes(app) {
2610
3247
  like(keywords.keyword, pattern)
2611
3248
  )
2612
3249
  )
2613
- ).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
3250
+ ).orderBy(desc6(querySnapshots.createdAt)).limit(limit + 1).all();
2614
3251
  const insightMatches = app.db.select().from(insights).where(
2615
- and3(
2616
- eq13(insights.projectId, project.id),
3252
+ and4(
3253
+ eq14(insights.projectId, project.id),
2617
3254
  or2(
2618
3255
  like(insights.title, pattern),
2619
3256
  like(insights.keyword, pattern),
@@ -2621,7 +3258,7 @@ async function compositeRoutes(app) {
2621
3258
  sql3`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
2622
3259
  )
2623
3260
  )
2624
- ).orderBy(desc5(insights.createdAt)).limit(limit + 1).all();
3261
+ ).orderBy(desc6(insights.createdAt)).limit(limit + 1).all();
2625
3262
  const hits = [];
2626
3263
  for (const row of snapshotMatches) {
2627
3264
  hits.push(buildSnapshotHit(row, rawQuery));
@@ -2676,7 +3313,7 @@ function summarizeLatestRun(app, run) {
2676
3313
  keywordId: querySnapshots.keywordId,
2677
3314
  provider: querySnapshots.provider,
2678
3315
  citationState: querySnapshots.citationState
2679
- }).from(querySnapshots).where(eq13(querySnapshots.runId, run.id)).all();
3316
+ }).from(querySnapshots).where(eq14(querySnapshots.runId, run.id)).all();
2680
3317
  if (rows.length === 0) return empty;
2681
3318
  const perKeyword = /* @__PURE__ */ new Map();
2682
3319
  const perProvider = /* @__PURE__ */ new Map();
@@ -2715,7 +3352,7 @@ function summarizeTransitions(app, latest, previous) {
2715
3352
  const rows = app.db.select({
2716
3353
  keywordId: querySnapshots.keywordId,
2717
3354
  citationState: querySnapshots.citationState
2718
- }).from(querySnapshots).where(eq13(querySnapshots.runId, runId)).all();
3355
+ }).from(querySnapshots).where(eq14(querySnapshots.runId, runId)).all();
2719
3356
  const map = /* @__PURE__ */ new Map();
2720
3357
  for (const row of rows) {
2721
3358
  const cited = row.citationState === "cited";
@@ -2872,7 +3509,7 @@ function makeSnippet(text, query) {
2872
3509
  }
2873
3510
 
2874
3511
  // ../api-routes/src/content-data.ts
2875
- import { and as and4, eq as eq14, desc as desc6, inArray as inArray4 } from "drizzle-orm";
3512
+ import { and as and5, eq as eq15, desc as desc7, inArray as inArray4 } from "drizzle-orm";
2876
3513
  var RECENT_RUNS_WINDOW = 5;
2877
3514
  function loadOrchestratorInput(db, project) {
2878
3515
  const projectId = project.id;
@@ -2918,43 +3555,43 @@ function loadOrchestratorInput(db, project) {
2918
3555
  };
2919
3556
  }
2920
3557
  function listKeywords(db, projectId) {
2921
- const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, projectId)).all();
3558
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq15(keywords.projectId, projectId)).all();
2922
3559
  return rows.map((r) => r.text);
2923
3560
  }
2924
3561
  function listCompetitorDomains(db, projectId) {
2925
- const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all();
3562
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, projectId)).all();
2926
3563
  return rows.map((r) => r.domain);
2927
3564
  }
2928
3565
  function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2929
3566
  const rows = db.select({ id: runs.id }).from(runs).where(
2930
- and4(
2931
- eq14(runs.projectId, projectId),
2932
- eq14(runs.kind, RunKinds["answer-visibility"]),
3567
+ and5(
3568
+ eq15(runs.projectId, projectId),
3569
+ eq15(runs.kind, RunKinds["answer-visibility"]),
2933
3570
  // Queued/running/failed/cancelled runs may have partial or no
2934
3571
  // snapshots; including them risks pointing latestRunId at a run with
2935
3572
  // no usable evidence.
2936
3573
  inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
2937
3574
  )
2938
- ).orderBy(desc6(runs.createdAt)).limit(limit).all();
3575
+ ).orderBy(desc7(runs.createdAt)).limit(limit).all();
2939
3576
  return rows.map((r) => r.id);
2940
3577
  }
2941
3578
  function lookupRunTimestamp(db, runId) {
2942
- const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq14(runs.id, runId)).get();
3579
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq15(runs.id, runId)).get();
2943
3580
  return row?.createdAt ?? "";
2944
3581
  }
2945
3582
  function listGscPagesForProject(db, projectId) {
2946
- const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
3583
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq15(gscSearchData.projectId, projectId)).all();
2947
3584
  return rows.map((r) => r.page);
2948
3585
  }
2949
3586
  function listGa4LandingPagesForProject(db, projectId) {
2950
- const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
3587
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq15(gaTrafficSnapshots.projectId, projectId)).all();
2951
3588
  return rows.map((r) => r.landingPage);
2952
3589
  }
2953
3590
  function buildGaTrafficByPage(db, projectId) {
2954
3591
  const rows = db.select({
2955
3592
  landingPage: gaTrafficSnapshots.landingPage,
2956
3593
  sessions: gaTrafficSnapshots.sessions
2957
- }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
3594
+ }).from(gaTrafficSnapshots).where(eq15(gaTrafficSnapshots.projectId, projectId)).all();
2958
3595
  const map = /* @__PURE__ */ new Map();
2959
3596
  for (const row of rows) {
2960
3597
  const path15 = extractPath(row.landingPage);
@@ -2964,14 +3601,14 @@ function buildGaTrafficByPage(db, projectId) {
2964
3601
  return map;
2965
3602
  }
2966
3603
  function sumAiReferralSessions(db, projectId) {
2967
- const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq14(gaAiReferrals.projectId, projectId)).all();
3604
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq15(gaAiReferrals.projectId, projectId)).all();
2968
3605
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2969
3606
  }
2970
3607
  function buildCandidateQueries(opts) {
2971
3608
  if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2972
3609
  return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2973
3610
  }
2974
- const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, opts.projectId)).all();
3611
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq15(keywords.projectId, opts.projectId)).all();
2975
3612
  const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2976
3613
  const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2977
3614
  const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
@@ -2981,7 +3618,7 @@ function buildCandidateQueries(opts) {
2981
3618
  list.push(row);
2982
3619
  snapshotsByKeyword.set(row.keywordId, list);
2983
3620
  }
2984
- const gscRows = opts.db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, opts.projectId)).all();
3621
+ const gscRows = opts.db.select().from(gscSearchData).where(eq15(gscSearchData.projectId, opts.projectId)).all();
2985
3622
  const gscByQuery = aggregateGscByQuery(gscRows);
2986
3623
  return opts.candidateQueryStrings.map((query) => {
2987
3624
  const keywordId = keywordIdByText.get(query);
@@ -5402,6 +6039,18 @@ var routeCatalog = [
5402
6039
  404: { description: "Insight not found." }
5403
6040
  }
5404
6041
  },
6042
+ {
6043
+ method: "get",
6044
+ path: "/api/v1/projects/{name}/report",
6045
+ summary: "Aggregated client-facing AEO report",
6046
+ tags: ["report"],
6047
+ description: "Bundles every section the canonry-report HTML output needs (executive summary, citation scorecard, competitor landscape, AI source origin, GSC, GA4, social/AI referrals, indexing health, citations trend, insights, and recommended next steps) into a single JSON payload. Backs `canonry report <project>`.",
6048
+ parameters: [nameParameter],
6049
+ responses: {
6050
+ 200: { description: "Report returned." },
6051
+ 404: { description: "Project not found." }
6052
+ }
6053
+ },
5405
6054
  {
5406
6055
  method: "get",
5407
6056
  path: "/api/v1/projects/{name}/health/latest",
@@ -6084,7 +6733,7 @@ async function telemetryRoutes(app, opts) {
6084
6733
 
6085
6734
  // ../api-routes/src/schedules.ts
6086
6735
  import crypto11 from "crypto";
6087
- import { eq as eq15 } from "drizzle-orm";
6736
+ import { eq as eq16 } from "drizzle-orm";
6088
6737
  async function scheduleRoutes(app, opts) {
6089
6738
  app.put("/projects/:name/schedule", async (request, reply) => {
6090
6739
  const project = resolveProject(app.db, request.params.name);
@@ -6127,7 +6776,7 @@ async function scheduleRoutes(app, opts) {
6127
6776
  }
6128
6777
  const now = (/* @__PURE__ */ new Date()).toISOString();
6129
6778
  const enabledInt = enabled === false ? 0 : 1;
6130
- const existing = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
6779
+ const existing = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
6131
6780
  if (existing) {
6132
6781
  app.db.update(schedules).set({
6133
6782
  cronExpr,
@@ -6136,7 +6785,7 @@ async function scheduleRoutes(app, opts) {
6136
6785
  providers: JSON.stringify(providers),
6137
6786
  enabled: enabledInt,
6138
6787
  updatedAt: now
6139
- }).where(eq15(schedules.id, existing.id)).run();
6788
+ }).where(eq16(schedules.id, existing.id)).run();
6140
6789
  } else {
6141
6790
  app.db.insert(schedules).values({
6142
6791
  id: crypto11.randomUUID(),
@@ -6158,12 +6807,12 @@ async function scheduleRoutes(app, opts) {
6158
6807
  diff: { cronExpr, preset, timezone, providers }
6159
6808
  });
6160
6809
  opts.onScheduleUpdated?.("upsert", project.id);
6161
- const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
6810
+ const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
6162
6811
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
6163
6812
  });
6164
6813
  app.get("/projects/:name/schedule", async (request, reply) => {
6165
6814
  const project = resolveProject(app.db, request.params.name);
6166
- const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
6815
+ const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
6167
6816
  if (!schedule) {
6168
6817
  throw notFound("Schedule", request.params.name);
6169
6818
  }
@@ -6171,11 +6820,11 @@ async function scheduleRoutes(app, opts) {
6171
6820
  });
6172
6821
  app.delete("/projects/:name/schedule", async (request, reply) => {
6173
6822
  const project = resolveProject(app.db, request.params.name);
6174
- const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
6823
+ const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
6175
6824
  if (!schedule) {
6176
6825
  throw notFound("Schedule", request.params.name);
6177
6826
  }
6178
- app.db.delete(schedules).where(eq15(schedules.id, schedule.id)).run();
6827
+ app.db.delete(schedules).where(eq16(schedules.id, schedule.id)).run();
6179
6828
  writeAuditLog(app.db, {
6180
6829
  projectId: project.id,
6181
6830
  actor: "api",
@@ -6205,7 +6854,7 @@ function formatSchedule(row) {
6205
6854
 
6206
6855
  // ../api-routes/src/notifications.ts
6207
6856
  import crypto12 from "crypto";
6208
- import { eq as eq16 } from "drizzle-orm";
6857
+ import { eq as eq17 } from "drizzle-orm";
6209
6858
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
6210
6859
  async function notificationRoutes(app) {
6211
6860
  app.get("/notifications/events", async (_request, reply) => {
@@ -6244,22 +6893,22 @@ async function notificationRoutes(app) {
6244
6893
  diff: { channel, ...redactNotificationUrl(url), events }
6245
6894
  });
6246
6895
  return reply.status(201).send({
6247
- ...formatNotification(app.db.select().from(notifications).where(eq16(notifications.id, id)).get()),
6896
+ ...formatNotification(app.db.select().from(notifications).where(eq17(notifications.id, id)).get()),
6248
6897
  webhookSecret
6249
6898
  });
6250
6899
  });
6251
6900
  app.get("/projects/:name/notifications", async (request, reply) => {
6252
6901
  const project = resolveProject(app.db, request.params.name);
6253
- const rows = app.db.select().from(notifications).where(eq16(notifications.projectId, project.id)).all();
6902
+ const rows = app.db.select().from(notifications).where(eq17(notifications.projectId, project.id)).all();
6254
6903
  return reply.send(rows.map(formatNotification));
6255
6904
  });
6256
6905
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
6257
6906
  const project = resolveProject(app.db, request.params.name);
6258
- const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
6907
+ const notification = app.db.select().from(notifications).where(eq17(notifications.id, request.params.id)).get();
6259
6908
  if (!notification || notification.projectId !== project.id) {
6260
6909
  throw notFound("Notification", request.params.id);
6261
6910
  }
6262
- app.db.delete(notifications).where(eq16(notifications.id, notification.id)).run();
6911
+ app.db.delete(notifications).where(eq17(notifications.id, notification.id)).run();
6263
6912
  writeAuditLog(app.db, {
6264
6913
  projectId: project.id,
6265
6914
  actor: "api",
@@ -6271,7 +6920,7 @@ async function notificationRoutes(app) {
6271
6920
  });
6272
6921
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
6273
6922
  const project = resolveProject(app.db, request.params.name);
6274
- const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
6923
+ const notification = app.db.select().from(notifications).where(eq17(notifications.id, request.params.id)).get();
6275
6924
  if (!notification || notification.projectId !== project.id) {
6276
6925
  throw notFound("Notification", request.params.id);
6277
6926
  }
@@ -6324,7 +6973,7 @@ function formatNotification(row) {
6324
6973
 
6325
6974
  // ../api-routes/src/google.ts
6326
6975
  import crypto14 from "crypto";
6327
- import { eq as eq17, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
6976
+ import { eq as eq18, and as and6, desc as desc8, sql as sql4 } from "drizzle-orm";
6328
6977
 
6329
6978
  // ../integration-google/src/constants.ts
6330
6979
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -7461,20 +8110,20 @@ async function googleRoutes(app, opts) {
7461
8110
  if (opts.onGscSyncRequested) {
7462
8111
  opts.onGscSyncRequested(runId, project.id, { days, full });
7463
8112
  }
7464
- const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
8113
+ const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
7465
8114
  return run;
7466
8115
  });
7467
8116
  app.get("/projects/:name/google/gsc/performance", async (request) => {
7468
8117
  const project = resolveProject(app.db, request.params.name);
7469
8118
  const { startDate, endDate, query, page, limit } = request.query;
7470
8119
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
7471
- const conditions = [eq17(gscSearchData.projectId, project.id)];
8120
+ const conditions = [eq18(gscSearchData.projectId, project.id)];
7472
8121
  if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
7473
8122
  else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
7474
8123
  if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
7475
8124
  if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
7476
8125
  if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
7477
- const rows = app.db.select().from(gscSearchData).where(and5(...conditions)).orderBy(desc7(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
8126
+ const rows = app.db.select().from(gscSearchData).where(and6(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
7478
8127
  return rows.map((r) => ({
7479
8128
  date: r.date,
7480
8129
  query: r.query,
@@ -7546,9 +8195,9 @@ async function googleRoutes(app, opts) {
7546
8195
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
7547
8196
  const project = resolveProject(app.db, request.params.name);
7548
8197
  const { url, limit } = request.query;
7549
- const conditions = [eq17(gscUrlInspections.projectId, project.id)];
7550
- if (url) conditions.push(eq17(gscUrlInspections.url, url));
7551
- const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
8198
+ const conditions = [eq18(gscUrlInspections.projectId, project.id)];
8199
+ if (url) conditions.push(eq18(gscUrlInspections.url, url));
8200
+ const rows = app.db.select().from(gscUrlInspections).where(and6(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7552
8201
  return rows.map((r) => ({
7553
8202
  id: r.id,
7554
8203
  url: r.url,
@@ -7567,7 +8216,7 @@ async function googleRoutes(app, opts) {
7567
8216
  });
7568
8217
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
7569
8218
  const project = resolveProject(app.db, request.params.name);
7570
- const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
8219
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, project.id)).orderBy(desc8(gscUrlInspections.inspectedAt)).all();
7571
8220
  const byUrl = /* @__PURE__ */ new Map();
7572
8221
  for (const row of allInspections) {
7573
8222
  const existing = byUrl.get(row.url);
@@ -7595,7 +8244,7 @@ async function googleRoutes(app, opts) {
7595
8244
  });
7596
8245
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
7597
8246
  const project = resolveProject(app.db, request.params.name);
7598
- const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
8247
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, project.id)).orderBy(desc8(gscUrlInspections.inspectedAt)).all();
7599
8248
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
7600
8249
  const latestByUrl = /* @__PURE__ */ new Map();
7601
8250
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -7692,7 +8341,7 @@ async function googleRoutes(app, opts) {
7692
8341
  const project = resolveProject(app.db, request.params.name);
7693
8342
  const parsed = parseInt(request.query.limit ?? "90", 10);
7694
8343
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7695
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
8344
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq18(gscCoverageSnapshots.projectId, project.id)).orderBy(desc8(gscCoverageSnapshots.date)).limit(limit).all();
7696
8345
  return rows.map((r) => ({
7697
8346
  date: r.date,
7698
8347
  indexed: r.indexed,
@@ -7752,7 +8401,7 @@ async function googleRoutes(app, opts) {
7752
8401
  if (opts.onInspectSitemapRequested) {
7753
8402
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7754
8403
  }
7755
- const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
8404
+ const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
7756
8405
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7757
8406
  });
7758
8407
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -7779,7 +8428,7 @@ async function googleRoutes(app, opts) {
7779
8428
  if (opts.onInspectSitemapRequested) {
7780
8429
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7781
8430
  }
7782
- const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
8431
+ const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
7783
8432
  return run;
7784
8433
  });
7785
8434
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -7826,7 +8475,7 @@ async function googleRoutes(app, opts) {
7826
8475
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7827
8476
  let urlsToNotify = request.body?.urls ?? [];
7828
8477
  if (request.body?.allUnindexed) {
7829
- const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
8478
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, project.id)).orderBy(desc8(gscUrlInspections.inspectedAt)).all();
7830
8479
  const latestByUrl = /* @__PURE__ */ new Map();
7831
8480
  for (const row of allInspections) {
7832
8481
  if (!latestByUrl.has(row.url)) {
@@ -7897,7 +8546,7 @@ async function googleRoutes(app, opts) {
7897
8546
 
7898
8547
  // ../api-routes/src/bing.ts
7899
8548
  import crypto15 from "crypto";
7900
- import { eq as eq18, and as and6, desc as desc8 } from "drizzle-orm";
8549
+ import { eq as eq19, and as and7, desc as desc9 } from "drizzle-orm";
7901
8550
 
7902
8551
  // ../integration-bing/src/constants.ts
7903
8552
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -8210,7 +8859,7 @@ async function bingRoutes(app, opts) {
8210
8859
  const store = requireConnectionStore();
8211
8860
  const project = resolveProject(app.db, request.params.name);
8212
8861
  requireConnection(store, project.canonicalDomain);
8213
- const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8862
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq19(bingUrlInspections.projectId, project.id)).orderBy(desc9(bingUrlInspections.inspectedAt)).all();
8214
8863
  const latestByUrl = /* @__PURE__ */ new Map();
8215
8864
  const definitiveByUrl = /* @__PURE__ */ new Map();
8216
8865
  for (const row of allInspections) {
@@ -8299,7 +8948,7 @@ async function bingRoutes(app, opts) {
8299
8948
  const project = resolveProject(app.db, request.params.name);
8300
8949
  const parsed = parseInt(request.query.limit ?? "90", 10);
8301
8950
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
8302
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
8951
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq19(bingCoverageSnapshots.projectId, project.id)).orderBy(desc9(bingCoverageSnapshots.date)).limit(limit).all();
8303
8952
  return rows.map((r) => ({
8304
8953
  date: r.date,
8305
8954
  indexed: r.indexed,
@@ -8311,8 +8960,8 @@ async function bingRoutes(app, opts) {
8311
8960
  requireConnectionStore();
8312
8961
  const project = resolveProject(app.db, request.params.name);
8313
8962
  const { url, limit } = request.query;
8314
- const whereClause = url ? and6(eq18(bingUrlInspections.projectId, project.id), eq18(bingUrlInspections.url, url)) : eq18(bingUrlInspections.projectId, project.id);
8315
- const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc8(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
8963
+ const whereClause = url ? and7(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
8964
+ const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
8316
8965
  return filtered.map((r) => ({
8317
8966
  id: r.id,
8318
8967
  url: r.url,
@@ -8401,7 +9050,7 @@ async function bingRoutes(app, opts) {
8401
9050
  anchorCount: result.AnchorCount ?? null,
8402
9051
  discoveryDate
8403
9052
  }).run();
8404
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
9053
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
8405
9054
  return {
8406
9055
  id,
8407
9056
  url,
@@ -8417,7 +9066,7 @@ async function bingRoutes(app, opts) {
8417
9066
  } catch (e) {
8418
9067
  const msg = e instanceof Error ? e.message : String(e);
8419
9068
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
8420
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
9069
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
8421
9070
  throw e;
8422
9071
  }
8423
9072
  });
@@ -8444,7 +9093,7 @@ async function bingRoutes(app, opts) {
8444
9093
  } else {
8445
9094
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
8446
9095
  }
8447
- const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
9096
+ const run = app.db.select().from(runs).where(eq19(runs.id, runId)).get();
8448
9097
  return run;
8449
9098
  });
8450
9099
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -8456,7 +9105,7 @@ async function bingRoutes(app, opts) {
8456
9105
  }
8457
9106
  let urlsToSubmit = request.body?.urls ?? [];
8458
9107
  if (request.body?.allUnindexed) {
8459
- const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
9108
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq19(bingUrlInspections.projectId, project.id)).orderBy(desc9(bingUrlInspections.inspectedAt)).all();
8460
9109
  const latestByUrl = /* @__PURE__ */ new Map();
8461
9110
  for (const row of allInspections) {
8462
9111
  if (!latestByUrl.has(row.url)) {
@@ -8543,14 +9192,14 @@ async function bingRoutes(app, opts) {
8543
9192
  import fs from "fs";
8544
9193
  import path from "path";
8545
9194
  import os from "os";
8546
- import { eq as eq19, and as and7 } from "drizzle-orm";
9195
+ import { eq as eq20, and as and8 } from "drizzle-orm";
8547
9196
  function getScreenshotDir() {
8548
9197
  return path.join(os.homedir(), ".canonry", "screenshots");
8549
9198
  }
8550
9199
  async function cdpRoutes(app, opts) {
8551
9200
  app.get("/screenshots/:snapshotId", async (request, reply) => {
8552
9201
  const { snapshotId } = request.params;
8553
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq19(querySnapshots.id, snapshotId)).get();
9202
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq20(querySnapshots.id, snapshotId)).get();
8554
9203
  if (!snapshot?.screenshotPath) {
8555
9204
  const err = notFound("Screenshot", snapshotId);
8556
9205
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8616,7 +9265,7 @@ async function cdpRoutes(app, opts) {
8616
9265
  async (request, reply) => {
8617
9266
  const project = resolveProject(app.db, request.params.name);
8618
9267
  const { runId } = request.params;
8619
- const run = app.db.select().from(runs).where(and7(eq19(runs.id, runId), eq19(runs.projectId, project.id))).get();
9268
+ const run = app.db.select().from(runs).where(and8(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
8620
9269
  if (!run) {
8621
9270
  const err = notFound("Run", runId);
8622
9271
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8629,8 +9278,8 @@ async function cdpRoutes(app, opts) {
8629
9278
  citedDomains: querySnapshots.citedDomains,
8630
9279
  screenshotPath: querySnapshots.screenshotPath,
8631
9280
  rawResponse: querySnapshots.rawResponse
8632
- }).from(querySnapshots).where(eq19(querySnapshots.runId, runId)).all();
8633
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq19(keywords.projectId, project.id)).all();
9281
+ }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
9282
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq20(keywords.projectId, project.id)).all();
8634
9283
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
8635
9284
  const byKeyword = /* @__PURE__ */ new Map();
8636
9285
  for (const snap of snapshots) {
@@ -8713,7 +9362,7 @@ async function cdpRoutes(app, opts) {
8713
9362
 
8714
9363
  // ../api-routes/src/ga.ts
8715
9364
  import crypto16 from "crypto";
8716
- import { eq as eq20, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
9365
+ import { eq as eq21, desc as desc10, and as and9, sql as sql5 } from "drizzle-orm";
8717
9366
  function gaLog(level, action, ctx) {
8718
9367
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8719
9368
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8878,10 +9527,10 @@ async function ga4Routes(app, opts) {
8878
9527
  if (!saConn && !oauthConn) {
8879
9528
  throw notFound("GA4 connection", project.name);
8880
9529
  }
8881
- app.db.delete(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).run();
8882
- app.db.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
8883
- app.db.delete(gaAiReferrals).where(eq20(gaAiReferrals.projectId, project.id)).run();
8884
- app.db.delete(gaSocialReferrals).where(eq20(gaSocialReferrals.projectId, project.id)).run();
9530
+ app.db.delete(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).run();
9531
+ app.db.delete(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).run();
9532
+ app.db.delete(gaAiReferrals).where(eq21(gaAiReferrals.projectId, project.id)).run();
9533
+ app.db.delete(gaSocialReferrals).where(eq21(gaSocialReferrals.projectId, project.id)).run();
8885
9534
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8886
9535
  opts.ga4CredentialStore?.deleteConnection(project.name);
8887
9536
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8902,7 +9551,7 @@ async function ga4Routes(app, opts) {
8902
9551
  if (!connected) {
8903
9552
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8904
9553
  }
8905
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
9554
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
8906
9555
  return {
8907
9556
  connected: true,
8908
9557
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8961,8 +9610,8 @@ async function ga4Routes(app, opts) {
8961
9610
  app.db.transaction((tx) => {
8962
9611
  if (syncTraffic) {
8963
9612
  tx.delete(gaTrafficSnapshots).where(
8964
- and8(
8965
- eq20(gaTrafficSnapshots.projectId, project.id),
9613
+ and9(
9614
+ eq21(gaTrafficSnapshots.projectId, project.id),
8966
9615
  sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8967
9616
  sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8968
9617
  )
@@ -8985,8 +9634,8 @@ async function ga4Routes(app, opts) {
8985
9634
  }
8986
9635
  if (syncAi) {
8987
9636
  tx.delete(gaAiReferrals).where(
8988
- and8(
8989
- eq20(gaAiReferrals.projectId, project.id),
9637
+ and9(
9638
+ eq21(gaAiReferrals.projectId, project.id),
8990
9639
  sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
8991
9640
  sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
8992
9641
  )
@@ -9010,8 +9659,8 @@ async function ga4Routes(app, opts) {
9010
9659
  }
9011
9660
  if (syncSocial) {
9012
9661
  tx.delete(gaSocialReferrals).where(
9013
- and8(
9014
- eq20(gaSocialReferrals.projectId, project.id),
9662
+ and9(
9663
+ eq21(gaSocialReferrals.projectId, project.id),
9015
9664
  sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
9016
9665
  sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
9017
9666
  )
@@ -9032,7 +9681,7 @@ async function ga4Routes(app, opts) {
9032
9681
  }
9033
9682
  }
9034
9683
  if (syncSummary) {
9035
- tx.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
9684
+ tx.delete(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).run();
9036
9685
  tx.insert(gaTrafficSummaries).values({
9037
9686
  id: crypto16.randomUUID(),
9038
9687
  projectId: project.id,
@@ -9046,7 +9695,7 @@ async function ga4Routes(app, opts) {
9046
9695
  }).run();
9047
9696
  }
9048
9697
  });
9049
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq20(runs.id, runId)).run();
9698
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq21(runs.id, runId)).run();
9050
9699
  const syncedComponents = only ? [
9051
9700
  ...syncTraffic ? ["traffic"] : [],
9052
9701
  ...syncSummary ? ["summary"] : [],
@@ -9075,7 +9724,7 @@ async function ga4Routes(app, opts) {
9075
9724
  } catch (e) {
9076
9725
  const msg = e instanceof Error ? e.message : String(e);
9077
9726
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
9078
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
9727
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
9079
9728
  throw e;
9080
9729
  }
9081
9730
  });
@@ -9086,42 +9735,42 @@ async function ga4Routes(app, opts) {
9086
9735
  const window = parseWindow(request.query.window);
9087
9736
  const cutoff = windowCutoff(window);
9088
9737
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
9089
- const snapshotConditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
9738
+ const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
9090
9739
  if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9091
- const aiConditions = [eq20(gaAiReferrals.projectId, project.id)];
9740
+ const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
9092
9741
  if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
9093
- const socialConditions = [eq20(gaSocialReferrals.projectId, project.id)];
9742
+ const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
9094
9743
  if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
9095
9744
  const summaryRow = cutoffDate ? app.db.select({
9096
9745
  totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
9097
9746
  totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
9098
9747
  totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
9099
- }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get() : app.db.select({
9748
+ }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get() : app.db.select({
9100
9749
  totalSessions: gaTrafficSummaries.totalSessions,
9101
9750
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
9102
9751
  totalUsers: gaTrafficSummaries.totalUsers
9103
- }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
9752
+ }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
9104
9753
  const directTotalRow = app.db.select({
9105
9754
  totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
9106
- }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get();
9755
+ }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).get();
9107
9756
  const summaryMeta = app.db.select({
9108
9757
  periodStart: gaTrafficSummaries.periodStart,
9109
9758
  periodEnd: gaTrafficSummaries.periodEnd
9110
- }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
9759
+ }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
9111
9760
  const rows = app.db.select({
9112
9761
  landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
9113
9762
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
9114
9763
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9115
9764
  directSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
9116
9765
  users: sql5`SUM(${gaTrafficSnapshots.users})`
9117
- }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
9766
+ }).from(gaTrafficSnapshots).where(and9(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
9118
9767
  const aiReferrals = app.db.select({
9119
9768
  source: gaAiReferrals.source,
9120
9769
  medium: gaAiReferrals.medium,
9121
9770
  sourceDimension: gaAiReferrals.sourceDimension,
9122
9771
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9123
9772
  users: sql5`SUM(${gaAiReferrals.users})`
9124
- }).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
9773
+ }).from(gaAiReferrals).where(and9(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
9125
9774
  const aiReferralLandingPages = app.db.select({
9126
9775
  source: gaAiReferrals.source,
9127
9776
  medium: gaAiReferrals.medium,
@@ -9129,7 +9778,7 @@ async function ga4Routes(app, opts) {
9129
9778
  landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
9130
9779
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9131
9780
  users: sql5`SUM(${gaAiReferrals.users})`
9132
- }).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(
9781
+ }).from(gaAiReferrals).where(and9(...aiConditions)).groupBy(
9133
9782
  gaAiReferrals.source,
9134
9783
  gaAiReferrals.medium,
9135
9784
  gaAiReferrals.sourceDimension,
@@ -9157,19 +9806,19 @@ async function ga4Routes(app, opts) {
9157
9806
  const aiBySession = app.db.select({
9158
9807
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
9159
9808
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
9160
- }).from(gaAiReferrals).where(and8(...aiConditions, eq20(gaAiReferrals.sourceDimension, "session"))).get();
9809
+ }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).get();
9161
9810
  const socialReferrals = app.db.select({
9162
9811
  source: gaSocialReferrals.source,
9163
9812
  medium: gaSocialReferrals.medium,
9164
9813
  channelGroup: gaSocialReferrals.channelGroup,
9165
9814
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
9166
9815
  users: sql5`SUM(${gaSocialReferrals.users})`
9167
- }).from(gaSocialReferrals).where(and8(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
9816
+ }).from(gaSocialReferrals).where(and9(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
9168
9817
  const socialTotals = app.db.select({
9169
9818
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
9170
9819
  users: sql5`SUM(${gaSocialReferrals.users})`
9171
- }).from(gaSocialReferrals).where(and8(...socialConditions)).get();
9172
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
9820
+ }).from(gaSocialReferrals).where(and9(...socialConditions)).get();
9821
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
9173
9822
  const total = summaryRow?.totalSessions ?? 0;
9174
9823
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
9175
9824
  return {
@@ -9236,7 +9885,7 @@ async function ga4Routes(app, opts) {
9236
9885
  const project = resolveProject(app.db, request.params.name);
9237
9886
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9238
9887
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9239
- const conditions = [eq20(gaAiReferrals.projectId, project.id)];
9888
+ const conditions = [eq21(gaAiReferrals.projectId, project.id)];
9240
9889
  if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
9241
9890
  const rows = app.db.select({
9242
9891
  date: gaAiReferrals.date,
@@ -9246,7 +9895,7 @@ async function ga4Routes(app, opts) {
9246
9895
  sourceDimension: gaAiReferrals.sourceDimension,
9247
9896
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9248
9897
  users: sql5`SUM(${gaAiReferrals.users})`
9249
- }).from(gaAiReferrals).where(and8(...conditions)).groupBy(
9898
+ }).from(gaAiReferrals).where(and9(...conditions)).groupBy(
9250
9899
  gaAiReferrals.date,
9251
9900
  gaAiReferrals.source,
9252
9901
  gaAiReferrals.medium,
@@ -9259,7 +9908,7 @@ async function ga4Routes(app, opts) {
9259
9908
  const project = resolveProject(app.db, request.params.name);
9260
9909
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9261
9910
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9262
- const conditions = [eq20(gaSocialReferrals.projectId, project.id)];
9911
+ const conditions = [eq21(gaSocialReferrals.projectId, project.id)];
9263
9912
  if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
9264
9913
  const rows = app.db.select({
9265
9914
  date: gaSocialReferrals.date,
@@ -9268,7 +9917,7 @@ async function ga4Routes(app, opts) {
9268
9917
  channelGroup: gaSocialReferrals.channelGroup,
9269
9918
  sessions: gaSocialReferrals.sessions,
9270
9919
  users: gaSocialReferrals.users
9271
- }).from(gaSocialReferrals).where(and8(...conditions)).orderBy(gaSocialReferrals.date).all();
9920
+ }).from(gaSocialReferrals).where(and9(...conditions)).orderBy(gaSocialReferrals.date).all();
9272
9921
  return rows;
9273
9922
  });
9274
9923
  app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
@@ -9281,8 +9930,8 @@ async function ga4Routes(app, opts) {
9281
9930
  d.setDate(d.getDate() - n);
9282
9931
  return fmt(d);
9283
9932
  };
9284
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
9285
- eq20(gaSocialReferrals.projectId, project.id),
9933
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and9(
9934
+ eq21(gaSocialReferrals.projectId, project.id),
9286
9935
  sql5`${gaSocialReferrals.date} >= ${from}`,
9287
9936
  sql5`${gaSocialReferrals.date} < ${to}`
9288
9937
  )).get();
@@ -9294,16 +9943,16 @@ async function ga4Routes(app, opts) {
9294
9943
  const sourceCurrent = app.db.select({
9295
9944
  source: gaSocialReferrals.source,
9296
9945
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
9297
- }).from(gaSocialReferrals).where(and8(
9298
- eq20(gaSocialReferrals.projectId, project.id),
9946
+ }).from(gaSocialReferrals).where(and9(
9947
+ eq21(gaSocialReferrals.projectId, project.id),
9299
9948
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
9300
9949
  sql5`${gaSocialReferrals.date} < ${fmt(today)}`
9301
9950
  )).groupBy(gaSocialReferrals.source).all();
9302
9951
  const sourcePrev = app.db.select({
9303
9952
  source: gaSocialReferrals.source,
9304
9953
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
9305
- }).from(gaSocialReferrals).where(and8(
9306
- eq20(gaSocialReferrals.projectId, project.id),
9954
+ }).from(gaSocialReferrals).where(and9(
9955
+ eq21(gaSocialReferrals.projectId, project.id),
9307
9956
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
9308
9957
  sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
9309
9958
  )).groupBy(gaSocialReferrals.source).all();
@@ -9344,16 +9993,16 @@ async function ga4Routes(app, opts) {
9344
9993
  return fmt(d);
9345
9994
  };
9346
9995
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9347
- const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9348
- const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9349
- const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9350
- const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9351
- eq20(gaAiReferrals.projectId, project.id),
9996
+ const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9997
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9998
+ const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and9(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9999
+ const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
10000
+ eq21(gaAiReferrals.projectId, project.id),
9352
10001
  sql5`${gaAiReferrals.date} >= ${from}`,
9353
10002
  sql5`${gaAiReferrals.date} < ${to}`,
9354
- eq20(gaAiReferrals.sourceDimension, "session")
10003
+ eq21(gaAiReferrals.sourceDimension, "session")
9355
10004
  )).get();
9356
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
10005
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
9357
10006
  const todayStr = fmt(today);
9358
10007
  const buildTrend = (sum) => {
9359
10008
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -9362,17 +10011,17 @@ async function ga4Routes(app, opts) {
9362
10011
  const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
9363
10012
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
9364
10013
  };
9365
- const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9366
- eq20(gaAiReferrals.projectId, project.id),
10014
+ const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
10015
+ eq21(gaAiReferrals.projectId, project.id),
9367
10016
  sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
9368
10017
  sql5`${gaAiReferrals.date} < ${todayStr}`,
9369
- eq20(gaAiReferrals.sourceDimension, "session")
10018
+ eq21(gaAiReferrals.sourceDimension, "session")
9370
10019
  )).groupBy(gaAiReferrals.source).all();
9371
- const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9372
- eq20(gaAiReferrals.projectId, project.id),
10020
+ const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and9(
10021
+ eq21(gaAiReferrals.projectId, project.id),
9373
10022
  sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
9374
10023
  sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
9375
- eq20(gaAiReferrals.sourceDimension, "session")
10024
+ eq21(gaAiReferrals.sourceDimension, "session")
9376
10025
  )).groupBy(gaAiReferrals.source).all();
9377
10026
  const findBiggestMover = (current, prev) => {
9378
10027
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
@@ -9388,8 +10037,8 @@ async function ga4Routes(app, opts) {
9388
10037
  }
9389
10038
  return mover;
9390
10039
  };
9391
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9392
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
10040
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
10041
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and9(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9393
10042
  return {
9394
10043
  total: buildTrend(sumTotal),
9395
10044
  organic: buildTrend(sumOrganic),
@@ -9404,14 +10053,14 @@ async function ga4Routes(app, opts) {
9404
10053
  const project = resolveProject(app.db, request.params.name);
9405
10054
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9406
10055
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9407
- const conditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
10056
+ const conditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
9408
10057
  if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9409
10058
  const rows = app.db.select({
9410
10059
  date: gaTrafficSnapshots.date,
9411
10060
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
9412
10061
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9413
10062
  users: sql5`SUM(${gaTrafficSnapshots.users})`
9414
- }).from(gaTrafficSnapshots).where(and8(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
10063
+ }).from(gaTrafficSnapshots).where(and9(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
9415
10064
  return rows.map((r) => ({
9416
10065
  date: r.date,
9417
10066
  sessions: r.sessions ?? 0,
@@ -9427,7 +10076,7 @@ async function ga4Routes(app, opts) {
9427
10076
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
9428
10077
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9429
10078
  users: sql5`SUM(${gaTrafficSnapshots.users})`
9430
- }).from(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
10079
+ }).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9431
10080
  return {
9432
10081
  pages: trafficPages.map((r) => ({
9433
10082
  landingPage: r.landingPage,
@@ -11064,7 +11713,7 @@ async function wordpressRoutes(app, opts) {
11064
11713
 
11065
11714
  // ../api-routes/src/backlinks.ts
11066
11715
  import crypto18 from "crypto";
11067
- import { and as and10, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
11716
+ import { and as and11, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
11068
11717
 
11069
11718
  // ../integration-commoncrawl/src/constants.ts
11070
11719
  import os2 from "os";
@@ -11461,7 +12110,7 @@ function pruneCachedRelease(release, opts = {}) {
11461
12110
  }
11462
12111
 
11463
12112
  // ../api-routes/src/backlinks-filter.ts
11464
- import { and as and9, ne, notLike } from "drizzle-orm";
12113
+ import { and as and10, ne, notLike } from "drizzle-orm";
11465
12114
  var BACKLINK_FILTER_PATTERNS = [
11466
12115
  "*.google.com",
11467
12116
  "*.googleusercontent.com",
@@ -11484,7 +12133,7 @@ function backlinkCrawlerExclusionClause() {
11484
12133
  conditions.push(ne(backlinkDomains.linkingDomain, pattern));
11485
12134
  }
11486
12135
  }
11487
- const combined = and9(...conditions);
12136
+ const combined = and10(...conditions);
11488
12137
  if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
11489
12138
  return combined;
11490
12139
  }
@@ -11545,8 +12194,8 @@ function mapRunRow(row) {
11545
12194
  };
11546
12195
  }
11547
12196
  function latestSummaryForProject(db, projectId, release) {
11548
- const condition = release ? and10(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
11549
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
12197
+ const condition = release ? and11(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
12198
+ return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
11550
12199
  }
11551
12200
  function parseExcludeCrawlers(value) {
11552
12201
  if (!value) return false;
@@ -11554,11 +12203,11 @@ function parseExcludeCrawlers(value) {
11554
12203
  return lower === "1" || lower === "true" || lower === "yes";
11555
12204
  }
11556
12205
  function computeFilteredSummary(db, base) {
11557
- const baseDomainCondition = and10(
11558
- eq21(backlinkDomains.projectId, base.projectId),
11559
- eq21(backlinkDomains.release, base.release)
12206
+ const baseDomainCondition = and11(
12207
+ eq22(backlinkDomains.projectId, base.projectId),
12208
+ eq22(backlinkDomains.release, base.release)
11560
12209
  );
11561
- const filteredCondition = and10(baseDomainCondition, backlinkCrawlerExclusionClause());
12210
+ const filteredCondition = and11(baseDomainCondition, backlinkCrawlerExclusionClause());
11562
12211
  const unfilteredAgg = db.select({
11563
12212
  count: sql6`count(*)`,
11564
12213
  total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
@@ -11567,7 +12216,7 @@ function computeFilteredSummary(db, base) {
11567
12216
  count: sql6`count(*)`,
11568
12217
  total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
11569
12218
  }).from(backlinkDomains).where(filteredCondition).get();
11570
- const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(10).all();
12219
+ const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(10).all();
11571
12220
  const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
11572
12221
  const totalHosts = Number(filteredAgg?.total ?? 0);
11573
12222
  const unfilteredLinkingDomains = Number(unfilteredAgg?.count ?? 0);
@@ -11627,7 +12276,7 @@ async function backlinksRoutes(app, opts) {
11627
12276
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
11628
12277
  );
11629
12278
  }
11630
- const existing = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.release, release)).get();
12279
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq22(ccReleaseSyncs.release, release)).get();
11631
12280
  const now = (/* @__PURE__ */ new Date()).toISOString();
11632
12281
  if (existing) {
11633
12282
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -11638,9 +12287,9 @@ async function backlinksRoutes(app, opts) {
11638
12287
  phaseDetail: null,
11639
12288
  error: null,
11640
12289
  updatedAt: now
11641
- }).where(eq21(ccReleaseSyncs.id, existing.id)).run();
12290
+ }).where(eq22(ccReleaseSyncs.id, existing.id)).run();
11642
12291
  opts.onReleaseSyncRequested(existing.id, release);
11643
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, existing.id)).get();
12292
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq22(ccReleaseSyncs.id, existing.id)).get();
11644
12293
  return reply.status(200).send(mapSyncRow(refreshed));
11645
12294
  }
11646
12295
  const id = crypto18.randomUUID();
@@ -11652,15 +12301,15 @@ async function backlinksRoutes(app, opts) {
11652
12301
  updatedAt: now
11653
12302
  }).run();
11654
12303
  opts.onReleaseSyncRequested(id, release);
11655
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, id)).get();
12304
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq22(ccReleaseSyncs.id, id)).get();
11656
12305
  return reply.status(201).send(mapSyncRow(inserted));
11657
12306
  });
11658
12307
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
11659
- const row = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).limit(1).get();
12308
+ const row = app.db.select().from(ccReleaseSyncs).orderBy(desc11(ccReleaseSyncs.updatedAt)).limit(1).get();
11660
12309
  return reply.send(row ? mapSyncRow(row) : null);
11661
12310
  });
11662
12311
  app.get("/backlinks/syncs", async (_request, reply) => {
11663
- const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).all();
12312
+ const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc11(ccReleaseSyncs.updatedAt)).all();
11664
12313
  return reply.send(rows.map(mapSyncRow));
11665
12314
  });
11666
12315
  app.get("/backlinks/releases", async (_request, reply) => {
@@ -11710,7 +12359,7 @@ async function backlinksRoutes(app, opts) {
11710
12359
  createdAt: now
11711
12360
  }).run();
11712
12361
  opts.onBacklinkExtractRequested(runId, project.id, release);
11713
- const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
12362
+ const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
11714
12363
  return reply.status(201).send(mapRunRow(run));
11715
12364
  });
11716
12365
  app.get(
@@ -11734,16 +12383,16 @@ async function backlinksRoutes(app, opts) {
11734
12383
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
11735
12384
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
11736
12385
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
11737
- const baseDomainCondition = and10(
11738
- eq21(backlinkDomains.projectId, project.id),
11739
- eq21(backlinkDomains.release, targetRelease)
12386
+ const baseDomainCondition = and11(
12387
+ eq22(backlinkDomains.projectId, project.id),
12388
+ eq22(backlinkDomains.release, targetRelease)
11740
12389
  );
11741
- const domainCondition = excludeCrawlers ? and10(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
12390
+ const domainCondition = excludeCrawlers ? and11(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
11742
12391
  const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
11743
12392
  const rows = app.db.select({
11744
12393
  linkingDomain: backlinkDomains.linkingDomain,
11745
12394
  numHosts: backlinkDomains.numHosts
11746
- }).from(backlinkDomains).where(domainCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
12395
+ }).from(backlinkDomains).where(domainCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
11747
12396
  let summary = null;
11748
12397
  if (summaryRow) {
11749
12398
  summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
@@ -11759,7 +12408,7 @@ async function backlinksRoutes(app, opts) {
11759
12408
  "/projects/:name/backlinks/history",
11760
12409
  async (request, reply) => {
11761
12410
  const project = resolveProject(app.db, request.params.name);
11762
- const rows = app.db.select().from(backlinkSummaries).where(eq21(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
12411
+ const rows = app.db.select().from(backlinkSummaries).where(eq22(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11763
12412
  const response = rows.map((r) => ({
11764
12413
  release: r.release,
11765
12414
  totalLinkingDomains: r.totalLinkingDomains,
@@ -12570,6 +13219,7 @@ async function apiRoutes(app, opts) {
12570
13219
  await api.register(historyRoutes);
12571
13220
  await api.register(analyticsRoutes);
12572
13221
  await api.register(intelligenceRoutes);
13222
+ await api.register(reportRoutes);
12573
13223
  await api.register(citationRoutes);
12574
13224
  await api.register(compositeRoutes);
12575
13225
  await api.register(contentRoutes);
@@ -15103,7 +15753,7 @@ import crypto19 from "crypto";
15103
15753
  import fs7 from "fs";
15104
15754
  import path9 from "path";
15105
15755
  import os4 from "os";
15106
- import { and as and11, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
15756
+ import { and as and12, eq as eq23, inArray as inArray5, sql as sql7 } from "drizzle-orm";
15107
15757
 
15108
15758
  // src/citation-utils.ts
15109
15759
  function domainMatches(domain, canonicalDomain) {
@@ -15364,7 +16014,7 @@ var JobRunner = class {
15364
16014
  if (stale.length === 0) return;
15365
16015
  const now = (/* @__PURE__ */ new Date()).toISOString();
15366
16016
  for (const run of stale) {
15367
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq22(runs.id, run.id)).run();
16017
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq23(runs.id, run.id)).run();
15368
16018
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
15369
16019
  }
15370
16020
  }
@@ -15392,10 +16042,10 @@ var JobRunner = class {
15392
16042
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
15393
16043
  }
15394
16044
  if (existingRun.status === "queued") {
15395
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and11(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
16045
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and12(eq23(runs.id, runId), eq23(runs.status, "queued"))).run();
15396
16046
  }
15397
16047
  this.throwIfRunCancelled(runId);
15398
- const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
16048
+ const project = this.db.select().from(projects).where(eq23(projects.id, projectId)).get();
15399
16049
  if (!project) {
15400
16050
  throw new Error(`Project ${projectId} not found`);
15401
16051
  }
@@ -15415,8 +16065,8 @@ var JobRunner = class {
15415
16065
  throw new Error("No providers configured. Add at least one provider API key.");
15416
16066
  }
15417
16067
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
15418
- projectKeywords = this.db.select().from(keywords).where(eq22(keywords.projectId, projectId)).all();
15419
- const projectCompetitors = this.db.select().from(competitors).where(eq22(competitors.projectId, projectId)).all();
16068
+ projectKeywords = this.db.select().from(keywords).where(eq23(keywords.projectId, projectId)).all();
16069
+ const projectCompetitors = this.db.select().from(competitors).where(eq23(competitors.projectId, projectId)).all();
15420
16070
  const competitorDomains = projectCompetitors.map((c) => c.domain);
15421
16071
  const allDomains = effectiveDomains({
15422
16072
  canonicalDomain: project.canonicalDomain,
@@ -15432,7 +16082,7 @@ var JobRunner = class {
15432
16082
  const todayPeriod = getCurrentUsageDay();
15433
16083
  for (const p of activeProviders) {
15434
16084
  const providerScope = `${projectId}:${p.adapter.name}`;
15435
- const providerUsage = this.db.select().from(usageCounters).where(eq22(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
16085
+ const providerUsage = this.db.select().from(usageCounters).where(eq23(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
15436
16086
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
15437
16087
  if (providerUsage + queriesPerProvider > limit) {
15438
16088
  throw new Error(
@@ -15573,12 +16223,12 @@ var JobRunner = class {
15573
16223
  const someFailed = providerErrors.size > 0;
15574
16224
  if (allFailed) {
15575
16225
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15576
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
16226
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq23(runs.id, runId)).run();
15577
16227
  } else if (someFailed) {
15578
16228
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15579
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
16229
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq23(runs.id, runId)).run();
15580
16230
  } else {
15581
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
16231
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15582
16232
  }
15583
16233
  this.flushProviderUsage(projectId, providerDispatchCounts);
15584
16234
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -15613,7 +16263,7 @@ var JobRunner = class {
15613
16263
  status: "failed",
15614
16264
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15615
16265
  error: errorMessage
15616
- }).where(eq22(runs.id, runId)).run();
16266
+ }).where(eq23(runs.id, runId)).run();
15617
16267
  this.flushProviderUsage(projectId, providerDispatchCounts);
15618
16268
  trackEvent("run.completed", {
15619
16269
  status: "failed",
@@ -15656,7 +16306,7 @@ var JobRunner = class {
15656
16306
  status: runs.status,
15657
16307
  finishedAt: runs.finishedAt,
15658
16308
  error: runs.error
15659
- }).from(runs).where(eq22(runs.id, runId)).get();
16309
+ }).from(runs).where(eq23(runs.id, runId)).get();
15660
16310
  }
15661
16311
  isRunCancelled(runId) {
15662
16312
  return this.getRunState(runId)?.status === "cancelled";
@@ -15672,7 +16322,7 @@ var JobRunner = class {
15672
16322
  this.db.update(runs).set({
15673
16323
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15674
16324
  error: currentRun.error ?? "Cancelled by user"
15675
- }).where(eq22(runs.id, runId)).run();
16325
+ }).where(eq23(runs.id, runId)).run();
15676
16326
  }
15677
16327
  trackEvent("run.completed", {
15678
16328
  status: "cancelled",
@@ -15695,7 +16345,7 @@ function getCurrentUsageDay() {
15695
16345
 
15696
16346
  // src/gsc-sync.ts
15697
16347
  import crypto20 from "crypto";
15698
- import { eq as eq23, and as and12, sql as sql8 } from "drizzle-orm";
16348
+ import { eq as eq24, and as and13, sql as sql8 } from "drizzle-orm";
15699
16349
  var log2 = createLogger("GscSync");
15700
16350
  function formatDate2(d) {
15701
16351
  return d.toISOString().split("T")[0];
@@ -15707,13 +16357,13 @@ function daysAgo(n) {
15707
16357
  }
15708
16358
  async function executeGscSync(db, runId, projectId, opts) {
15709
16359
  const now = (/* @__PURE__ */ new Date()).toISOString();
15710
- db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
16360
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq24(runs.id, runId)).run();
15711
16361
  try {
15712
16362
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15713
16363
  if (!googleClientId || !googleClientSecret) {
15714
16364
  throw new Error("Google OAuth is not configured in the local Canonry config");
15715
16365
  }
15716
- const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
16366
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15717
16367
  if (!project) {
15718
16368
  throw new Error(`Project not found: ${projectId}`);
15719
16369
  }
@@ -15747,8 +16397,8 @@ async function executeGscSync(db, runId, projectId, opts) {
15747
16397
  });
15748
16398
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
15749
16399
  db.delete(gscSearchData).where(
15750
- and12(
15751
- eq23(gscSearchData.projectId, projectId),
16400
+ and13(
16401
+ eq24(gscSearchData.projectId, projectId),
15752
16402
  sql8`${gscSearchData.date} >= ${startDate}`,
15753
16403
  sql8`${gscSearchData.date} <= ${endDate}`
15754
16404
  )
@@ -15815,7 +16465,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15815
16465
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
15816
16466
  }
15817
16467
  }
15818
- const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
16468
+ const allInspections = db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, projectId)).all();
15819
16469
  const latestByUrl = /* @__PURE__ */ new Map();
15820
16470
  for (const row of allInspections) {
15821
16471
  const existing = latestByUrl.get(row.url);
@@ -15836,7 +16486,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15836
16486
  }
15837
16487
  }
15838
16488
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
15839
- db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
16489
+ db.delete(gscCoverageSnapshots).where(and13(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
15840
16490
  db.insert(gscCoverageSnapshots).values({
15841
16491
  id: crypto20.randomUUID(),
15842
16492
  projectId,
@@ -15847,11 +16497,11 @@ async function executeGscSync(db, runId, projectId, opts) {
15847
16497
  reasonBreakdown: JSON.stringify(reasonCounts),
15848
16498
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15849
16499
  }).run();
15850
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16500
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15851
16501
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
15852
16502
  } catch (err) {
15853
16503
  const errorMsg = err instanceof Error ? err.message : String(err);
15854
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
16504
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15855
16505
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
15856
16506
  throw err;
15857
16507
  }
@@ -15859,7 +16509,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15859
16509
 
15860
16510
  // src/gsc-inspect-sitemap.ts
15861
16511
  import crypto21 from "crypto";
15862
- import { eq as eq24, and as and13 } from "drizzle-orm";
16512
+ import { eq as eq25, and as and14 } from "drizzle-orm";
15863
16513
 
15864
16514
  // src/sitemap-parser.ts
15865
16515
  var log3 = createLogger("SitemapParser");
@@ -15980,13 +16630,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
15980
16630
  var log4 = createLogger("InspectSitemap");
15981
16631
  async function executeInspectSitemap(db, runId, projectId, opts) {
15982
16632
  const now = (/* @__PURE__ */ new Date()).toISOString();
15983
- db.update(runs).set({ status: "running", startedAt: now }).where(eq24(runs.id, runId)).run();
16633
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq25(runs.id, runId)).run();
15984
16634
  try {
15985
16635
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15986
16636
  if (!googleClientId || !googleClientSecret) {
15987
16637
  throw new Error("Google OAuth is not configured in the local Canonry config");
15988
16638
  }
15989
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
16639
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
15990
16640
  if (!project) {
15991
16641
  throw new Error(`Project not found: ${projectId}`);
15992
16642
  }
@@ -16054,7 +16704,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
16054
16704
  await new Promise((r) => setTimeout(r, 1e3));
16055
16705
  }
16056
16706
  }
16057
- const allInspections = db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, projectId)).all();
16707
+ const allInspections = db.select().from(gscUrlInspections).where(eq25(gscUrlInspections.projectId, projectId)).all();
16058
16708
  const latestByUrl = /* @__PURE__ */ new Map();
16059
16709
  for (const row of allInspections) {
16060
16710
  const existing = latestByUrl.get(row.url);
@@ -16075,7 +16725,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
16075
16725
  }
16076
16726
  }
16077
16727
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
16078
- db.delete(gscCoverageSnapshots).where(and13(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
16728
+ db.delete(gscCoverageSnapshots).where(and14(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
16079
16729
  db.insert(gscCoverageSnapshots).values({
16080
16730
  id: crypto21.randomUUID(),
16081
16731
  projectId,
@@ -16087,11 +16737,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
16087
16737
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
16088
16738
  }).run();
16089
16739
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
16090
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16740
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
16091
16741
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
16092
16742
  } catch (err) {
16093
16743
  const errorMsg = err instanceof Error ? err.message : String(err);
16094
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16744
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
16095
16745
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
16096
16746
  throw err;
16097
16747
  }
@@ -16099,7 +16749,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
16099
16749
 
16100
16750
  // src/bing-inspect-sitemap.ts
16101
16751
  import crypto22 from "crypto";
16102
- import { eq as eq25, desc as desc11 } from "drizzle-orm";
16752
+ import { eq as eq26, desc as desc12 } from "drizzle-orm";
16103
16753
  var log5 = createLogger("BingInspectSitemap");
16104
16754
  function parseBingDate2(value) {
16105
16755
  if (!value) return null;
@@ -16117,9 +16767,9 @@ function isBlockingIssueType2(issueType) {
16117
16767
  }
16118
16768
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
16119
16769
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
16120
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
16770
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
16121
16771
  try {
16122
- const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
16772
+ const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
16123
16773
  if (!project) {
16124
16774
  throw new Error(`Project not found: ${projectId}`);
16125
16775
  }
@@ -16137,7 +16787,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16137
16787
  if (sitemapUrls.length === 0) {
16138
16788
  throw new Error("No URLs found in sitemap");
16139
16789
  }
16140
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).all();
16790
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, projectId)).all();
16141
16791
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
16142
16792
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
16143
16793
  log5.info("sitemap.diff", {
@@ -16220,7 +16870,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16220
16870
  await new Promise((r) => setTimeout(r, 1e3));
16221
16871
  }
16222
16872
  }
16223
- const allInspections = db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
16873
+ const allInspections = db.select().from(bingUrlInspections).where(eq26(bingUrlInspections.projectId, projectId)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
16224
16874
  const latestByUrl = /* @__PURE__ */ new Map();
16225
16875
  const definitiveByUrl = /* @__PURE__ */ new Map();
16226
16876
  for (const row of allInspections) {
@@ -16263,7 +16913,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16263
16913
  }
16264
16914
  }).run();
16265
16915
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
16266
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
16916
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
16267
16917
  log5.info("inspect.completed", {
16268
16918
  runId,
16269
16919
  projectId,
@@ -16277,7 +16927,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16277
16927
  });
16278
16928
  } catch (err) {
16279
16929
  const errorMsg = err instanceof Error ? err.message : String(err);
16280
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
16930
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
16281
16931
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
16282
16932
  throw err;
16283
16933
  }
@@ -16286,7 +16936,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16286
16936
  // src/commoncrawl-sync.ts
16287
16937
  import crypto23 from "crypto";
16288
16938
  import path10 from "path";
16289
- import { and as and14, eq as eq26, sql as sql9 } from "drizzle-orm";
16939
+ import { and as and15, eq as eq27, sql as sql9 } from "drizzle-orm";
16290
16940
  var log6 = createLogger("CommonCrawlSync");
16291
16941
  var INSERT_CHUNK_SIZE = 1e4;
16292
16942
  function defaultDeps() {
@@ -16312,7 +16962,7 @@ async function executeReleaseSync(db, syncId, opts) {
16312
16962
  phaseDetail: "downloading vertices + edges",
16313
16963
  updatedAt: downloadStartedAt,
16314
16964
  error: null
16315
- }).where(eq26(ccReleaseSyncs.id, syncId)).run();
16965
+ }).where(eq27(ccReleaseSyncs.id, syncId)).run();
16316
16966
  const paths = ccReleasePaths(release);
16317
16967
  const releaseCacheDir = path10.join(deps.cacheDir, release);
16318
16968
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -16335,7 +16985,7 @@ async function executeReleaseSync(db, syncId, opts) {
16335
16985
  vertexSha256: vertex.sha256,
16336
16986
  edgesSha256: edges.sha256,
16337
16987
  updatedAt: downloadFinishedAt
16338
- }).where(eq26(ccReleaseSyncs.id, syncId)).run();
16988
+ }).where(eq27(ccReleaseSyncs.id, syncId)).run();
16339
16989
  const allProjects = db.select().from(projects).all();
16340
16990
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
16341
16991
  let rows = [];
@@ -16351,8 +17001,8 @@ async function executeReleaseSync(db, syncId, opts) {
16351
17001
  }
16352
17002
  const queriedAt = deps.now().toISOString();
16353
17003
  db.transaction((tx) => {
16354
- tx.delete(backlinkDomains).where(eq26(backlinkDomains.releaseSyncId, syncId)).run();
16355
- tx.delete(backlinkSummaries).where(eq26(backlinkSummaries.releaseSyncId, syncId)).run();
17004
+ tx.delete(backlinkDomains).where(eq27(backlinkDomains.releaseSyncId, syncId)).run();
17005
+ tx.delete(backlinkSummaries).where(eq27(backlinkSummaries.releaseSyncId, syncId)).run();
16356
17006
  const expanded = [];
16357
17007
  for (const r of rows) {
16358
17008
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -16411,7 +17061,7 @@ async function executeReleaseSync(db, syncId, opts) {
16411
17061
  domainsDiscovered: rows.length,
16412
17062
  updatedAt: finishedAt,
16413
17063
  error: null
16414
- }).where(eq26(ccReleaseSyncs.id, syncId)).run();
17064
+ }).where(eq27(ccReleaseSyncs.id, syncId)).run();
16415
17065
  log6.info("sync.completed", {
16416
17066
  syncId,
16417
17067
  release,
@@ -16441,7 +17091,7 @@ async function executeReleaseSync(db, syncId, opts) {
16441
17091
  error: errorMsg,
16442
17092
  phaseDetail: null,
16443
17093
  updatedAt: finishedAt
16444
- }).where(eq26(ccReleaseSyncs.id, syncId)).run();
17094
+ }).where(eq27(ccReleaseSyncs.id, syncId)).run();
16445
17095
  log6.error("sync.failed", { syncId, release, error: errorMsg });
16446
17096
  throw err;
16447
17097
  }
@@ -16477,7 +17127,7 @@ function computeSummary(rows) {
16477
17127
  // src/backlink-extract.ts
16478
17128
  import crypto24 from "crypto";
16479
17129
  import fs8 from "fs";
16480
- import { and as and15, desc as desc12, eq as eq27 } from "drizzle-orm";
17130
+ import { and as and16, desc as desc13, eq as eq28 } from "drizzle-orm";
16481
17131
  var log7 = createLogger("BacklinkExtract");
16482
17132
  function defaultDeps2() {
16483
17133
  return {
@@ -16489,13 +17139,13 @@ function defaultDeps2() {
16489
17139
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16490
17140
  const deps = { ...defaultDeps2(), ...opts.deps };
16491
17141
  const startedAt = deps.now().toISOString();
16492
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq27(runs.id, runId)).run();
17142
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq28(runs.id, runId)).run();
16493
17143
  try {
16494
- const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
17144
+ const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
16495
17145
  if (!project) {
16496
17146
  throw new Error(`Project not found: ${projectId}`);
16497
17147
  }
16498
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
17148
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc13(ccReleaseSyncs.createdAt)).limit(1).get();
16499
17149
  if (!sync) {
16500
17150
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
16501
17151
  }
@@ -16523,7 +17173,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16523
17173
  const targetDomain = project.canonicalDomain;
16524
17174
  db.transaction((tx) => {
16525
17175
  tx.delete(backlinkDomains).where(
16526
- and15(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
17176
+ and16(eq28(backlinkDomains.projectId, projectId), eq28(backlinkDomains.release, release))
16527
17177
  ).run();
16528
17178
  if (rows.length > 0) {
16529
17179
  const values = rows.map((r) => ({
@@ -16563,7 +17213,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16563
17213
  }).run();
16564
17214
  });
16565
17215
  const finishedAt = deps.now().toISOString();
16566
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
17216
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq28(runs.id, runId)).run();
16567
17217
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
16568
17218
  } catch (err) {
16569
17219
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -16572,7 +17222,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16572
17222
  status: RunStatuses.failed,
16573
17223
  error: errorMsg,
16574
17224
  finishedAt
16575
- }).where(eq27(runs.id, runId)).run();
17225
+ }).where(eq28(runs.id, runId)).run();
16576
17226
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
16577
17227
  throw err;
16578
17228
  }
@@ -16645,7 +17295,7 @@ var ProviderRegistry = class {
16645
17295
 
16646
17296
  // src/scheduler.ts
16647
17297
  import cron from "node-cron";
16648
- import { eq as eq28 } from "drizzle-orm";
17298
+ import { eq as eq29 } from "drizzle-orm";
16649
17299
  var log8 = createLogger("Scheduler");
16650
17300
  var Scheduler = class {
16651
17301
  db;
@@ -16657,7 +17307,7 @@ var Scheduler = class {
16657
17307
  }
16658
17308
  /** Load all enabled schedules from DB and register cron jobs. */
16659
17309
  start() {
16660
- const allSchedules = this.db.select().from(schedules).where(eq28(schedules.enabled, 1)).all();
17310
+ const allSchedules = this.db.select().from(schedules).where(eq29(schedules.enabled, 1)).all();
16661
17311
  for (const schedule of allSchedules) {
16662
17312
  const missedRunAt = schedule.nextRunAt;
16663
17313
  this.registerCronTask(schedule);
@@ -16682,7 +17332,7 @@ var Scheduler = class {
16682
17332
  this.stopTask(projectId, existing, "Stopped");
16683
17333
  this.tasks.delete(projectId);
16684
17334
  }
16685
- const schedule = this.db.select().from(schedules).where(eq28(schedules.projectId, projectId)).get();
17335
+ const schedule = this.db.select().from(schedules).where(eq29(schedules.projectId, projectId)).get();
16686
17336
  if (schedule && schedule.enabled === 1) {
16687
17337
  this.registerCronTask(schedule);
16688
17338
  }
@@ -16715,14 +17365,14 @@ var Scheduler = class {
16715
17365
  this.db.update(schedules).set({
16716
17366
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
16717
17367
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16718
- }).where(eq28(schedules.id, scheduleId)).run();
17368
+ }).where(eq29(schedules.id, scheduleId)).run();
16719
17369
  const label = schedule.preset ?? cronExpr;
16720
17370
  log8.info("cron.registered", { projectId, schedule: label, timezone });
16721
17371
  }
16722
17372
  triggerRun(scheduleId, projectId) {
16723
17373
  try {
16724
17374
  const now = (/* @__PURE__ */ new Date()).toISOString();
16725
- const currentSchedule = this.db.select().from(schedules).where(eq28(schedules.id, scheduleId)).get();
17375
+ const currentSchedule = this.db.select().from(schedules).where(eq29(schedules.id, scheduleId)).get();
16726
17376
  if (!currentSchedule || currentSchedule.enabled !== 1) {
16727
17377
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
16728
17378
  this.remove(projectId);
@@ -16730,7 +17380,7 @@ var Scheduler = class {
16730
17380
  }
16731
17381
  const task = this.tasks.get(projectId);
16732
17382
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
16733
- const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
17383
+ const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
16734
17384
  if (!project) {
16735
17385
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
16736
17386
  this.remove(projectId);
@@ -16759,7 +17409,7 @@ var Scheduler = class {
16759
17409
  this.db.update(schedules).set({
16760
17410
  nextRunAt,
16761
17411
  updatedAt: now
16762
- }).where(eq28(schedules.id, currentSchedule.id)).run();
17412
+ }).where(eq29(schedules.id, currentSchedule.id)).run();
16763
17413
  return;
16764
17414
  }
16765
17415
  const runId = queueResult.runId;
@@ -16767,7 +17417,7 @@ var Scheduler = class {
16767
17417
  lastRunAt: now,
16768
17418
  nextRunAt,
16769
17419
  updatedAt: now
16770
- }).where(eq28(schedules.id, currentSchedule.id)).run();
17420
+ }).where(eq29(schedules.id, currentSchedule.id)).run();
16771
17421
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
16772
17422
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
16773
17423
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -16779,7 +17429,7 @@ var Scheduler = class {
16779
17429
  };
16780
17430
 
16781
17431
  // src/notifier.ts
16782
- import { eq as eq29, desc as desc13, and as and16, or as or3 } from "drizzle-orm";
17432
+ import { eq as eq30, desc as desc14, and as and17, or as or3 } from "drizzle-orm";
16783
17433
  import crypto25 from "crypto";
16784
17434
  var log9 = createLogger("Notifier");
16785
17435
  var Notifier = class {
@@ -16792,18 +17442,18 @@ var Notifier = class {
16792
17442
  /** Called after a run completes (success, partial, or failed). */
16793
17443
  async onRunCompleted(runId, projectId) {
16794
17444
  log9.info("run.completed", { runId, projectId });
16795
- const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
17445
+ const notifs = this.db.select().from(notifications).where(eq30(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16796
17446
  if (notifs.length === 0) {
16797
17447
  log9.info("notifications.none-enabled", { projectId });
16798
17448
  return;
16799
17449
  }
16800
17450
  log9.info("notifications.found", { projectId, count: notifs.length });
16801
- const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
17451
+ const run = this.db.select().from(runs).where(eq30(runs.id, runId)).get();
16802
17452
  if (!run) {
16803
17453
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
16804
17454
  return;
16805
17455
  }
16806
- const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
17456
+ const project = this.db.select().from(projects).where(eq30(projects.id, projectId)).get();
16807
17457
  if (!project) {
16808
17458
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
16809
17459
  return;
@@ -16850,11 +17500,11 @@ var Notifier = class {
16850
17500
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
16851
17501
  if (highInsights.length > 0) insightEvents.push("insight.high");
16852
17502
  if (insightEvents.length === 0) return;
16853
- const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
17503
+ const notifs = this.db.select().from(notifications).where(eq30(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16854
17504
  if (notifs.length === 0) return;
16855
- const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
17505
+ const run = this.db.select().from(runs).where(eq30(runs.id, runId)).get();
16856
17506
  if (!run) return;
16857
- const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
17507
+ const project = this.db.select().from(projects).where(eq30(projects.id, projectId)).get();
16858
17508
  if (!project) return;
16859
17509
  for (const notif of notifs) {
16860
17510
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -16885,11 +17535,11 @@ var Notifier = class {
16885
17535
  }
16886
17536
  computeTransitions(runId, projectId) {
16887
17537
  const recentRuns = this.db.select().from(runs).where(
16888
- and16(
16889
- eq29(runs.projectId, projectId),
16890
- or3(eq29(runs.status, "completed"), eq29(runs.status, "partial"))
17538
+ and17(
17539
+ eq30(runs.projectId, projectId),
17540
+ or3(eq30(runs.status, "completed"), eq30(runs.status, "partial"))
16891
17541
  )
16892
- ).orderBy(desc13(runs.createdAt)).limit(2).all();
17542
+ ).orderBy(desc14(runs.createdAt)).limit(2).all();
16893
17543
  if (recentRuns.length < 2) return [];
16894
17544
  const currentRunId = recentRuns[0].id;
16895
17545
  const previousRunId = recentRuns[1].id;
@@ -16899,12 +17549,12 @@ var Notifier = class {
16899
17549
  keyword: keywords.keyword,
16900
17550
  provider: querySnapshots.provider,
16901
17551
  citationState: querySnapshots.citationState
16902
- }).from(querySnapshots).leftJoin(keywords, eq29(querySnapshots.keywordId, keywords.id)).where(eq29(querySnapshots.runId, currentRunId)).all();
17552
+ }).from(querySnapshots).leftJoin(keywords, eq30(querySnapshots.keywordId, keywords.id)).where(eq30(querySnapshots.runId, currentRunId)).all();
16903
17553
  const previousSnapshots = this.db.select({
16904
17554
  keywordId: querySnapshots.keywordId,
16905
17555
  provider: querySnapshots.provider,
16906
17556
  citationState: querySnapshots.citationState
16907
- }).from(querySnapshots).where(eq29(querySnapshots.runId, previousRunId)).all();
17557
+ }).from(querySnapshots).where(eq30(querySnapshots.runId, previousRunId)).all();
16908
17558
  const prevMap = /* @__PURE__ */ new Map();
16909
17559
  for (const s of previousSnapshots) {
16910
17560
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -17021,7 +17671,7 @@ var RunCoordinator = class {
17021
17671
 
17022
17672
  // src/agent/session-registry.ts
17023
17673
  import crypto27 from "crypto";
17024
- import { eq as eq31 } from "drizzle-orm";
17674
+ import { eq as eq32 } from "drizzle-orm";
17025
17675
 
17026
17676
  // src/agent/session.ts
17027
17677
  import fs11 from "fs";
@@ -17371,7 +18021,7 @@ function resolveSessionProviderAndModel(config, opts) {
17371
18021
 
17372
18022
  // src/agent/memory-store.ts
17373
18023
  import crypto26 from "crypto";
17374
- import { and as and17, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
18024
+ import { and as and18, desc as desc15, eq as eq31, like as like2, sql as sql10 } from "drizzle-orm";
17375
18025
  var COMPACTION_KEY_PREFIX = "compaction:";
17376
18026
  var COMPACTION_NOTES_PER_SESSION = 3;
17377
18027
  function rowToDto(row) {
@@ -17385,7 +18035,7 @@ function rowToDto(row) {
17385
18035
  };
17386
18036
  }
17387
18037
  function listMemoryEntries(db, projectId, opts = {}) {
17388
- const query = db.select().from(agentMemory).where(eq30(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
18038
+ const query = db.select().from(agentMemory).where(eq31(agentMemory.projectId, projectId)).orderBy(desc15(agentMemory.updatedAt));
17389
18039
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
17390
18040
  return rows.map(rowToDto);
17391
18041
  }
@@ -17416,12 +18066,12 @@ function upsertMemoryEntry(db, args) {
17416
18066
  updatedAt: now
17417
18067
  }
17418
18068
  }).run();
17419
- const row = db.select().from(agentMemory).where(and17(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
18069
+ const row = db.select().from(agentMemory).where(and18(eq31(agentMemory.projectId, args.projectId), eq31(agentMemory.key, args.key))).get();
17420
18070
  if (!row) throw new Error("memory upsert produced no row");
17421
18071
  return rowToDto(row);
17422
18072
  }
17423
18073
  function deleteMemoryEntry(db, projectId, key) {
17424
- const result = db.delete(agentMemory).where(and17(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
18074
+ const result = db.delete(agentMemory).where(and18(eq31(agentMemory.projectId, projectId), eq31(agentMemory.key, key))).run();
17425
18075
  const changes = result.changes ?? 0;
17426
18076
  return changes > 0;
17427
18077
  }
@@ -17450,16 +18100,16 @@ function writeCompactionNote(db, args) {
17450
18100
  }).run();
17451
18101
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
17452
18102
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
17453
- and17(
17454
- eq30(agentMemory.projectId, args.projectId),
18103
+ and18(
18104
+ eq31(agentMemory.projectId, args.projectId),
17455
18105
  like2(agentMemory.key, `${sessionPrefix}%`)
17456
18106
  )
17457
- ).orderBy(desc14(agentMemory.updatedAt)).all();
18107
+ ).orderBy(desc15(agentMemory.updatedAt)).all();
17458
18108
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
17459
18109
  if (stale.length > 0) {
17460
18110
  tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
17461
18111
  }
17462
- const row = tx.select().from(agentMemory).where(and17(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
18112
+ const row = tx.select().from(agentMemory).where(and18(eq31(agentMemory.projectId, args.projectId), eq31(agentMemory.key, key))).get();
17463
18113
  if (row) inserted = rowToDto(row);
17464
18114
  });
17465
18115
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -17641,7 +18291,7 @@ var SessionRegistry = class {
17641
18291
  modelProvider: effectiveProvider,
17642
18292
  modelId: effectiveModelId,
17643
18293
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17644
- }).where(eq31(agentSessions.projectId, projectId)).run();
18294
+ }).where(eq32(agentSessions.projectId, projectId)).run();
17645
18295
  }
17646
18296
  const agent2 = createAeroSession({
17647
18297
  projectName,
@@ -17855,7 +18505,7 @@ ${lines.join("\n")}
17855
18505
  modelProvider: nextProvider,
17856
18506
  modelId: nextModelId,
17857
18507
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17858
- }).where(eq31(agentSessions.projectId, projectId)).run();
18508
+ }).where(eq32(agentSessions.projectId, projectId)).run();
17859
18509
  }
17860
18510
  /** Persist a session's transcript back to the DB. Call after any run settles. */
17861
18511
  save(projectName) {
@@ -18017,11 +18667,11 @@ ${lines.join("\n")}
18017
18667
  return id;
18018
18668
  }
18019
18669
  tryResolveProjectId(projectName) {
18020
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq31(projects.name, projectName)).get();
18670
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq32(projects.name, projectName)).get();
18021
18671
  return row?.id;
18022
18672
  }
18023
18673
  loadRow(projectId) {
18024
- const row = this.opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, projectId)).get();
18674
+ const row = this.opts.db.select().from(agentSessions).where(eq32(agentSessions.projectId, projectId)).get();
18025
18675
  return row ?? null;
18026
18676
  }
18027
18677
  insertRow(params) {
@@ -18040,14 +18690,14 @@ ${lines.join("\n")}
18040
18690
  }
18041
18691
  updateRow(projectId, patch) {
18042
18692
  const now = (/* @__PURE__ */ new Date()).toISOString();
18043
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq31(agentSessions.projectId, projectId)).run();
18693
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq32(agentSessions.projectId, projectId)).run();
18044
18694
  }
18045
18695
  };
18046
18696
 
18047
18697
  // src/agent/agent-routes.ts
18048
- import { eq as eq32 } from "drizzle-orm";
18698
+ import { eq as eq33 } from "drizzle-orm";
18049
18699
  function resolveProject2(db, name) {
18050
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq32(projects.name, name)).get();
18700
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq33(projects.name, name)).get();
18051
18701
  if (!row) throw notFound("project", name);
18052
18702
  return row;
18053
18703
  }
@@ -18056,7 +18706,7 @@ function registerAgentRoutes(app, opts) {
18056
18706
  "/projects/:name/agent/transcript",
18057
18707
  async (request) => {
18058
18708
  const project = resolveProject2(opts.db, request.params.name);
18059
- const row = opts.db.select().from(agentSessions).where(eq32(agentSessions.projectId, project.id)).get();
18709
+ const row = opts.db.select().from(agentSessions).where(eq33(agentSessions.projectId, project.id)).get();
18060
18710
  if (!row) {
18061
18711
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
18062
18712
  }
@@ -18080,7 +18730,7 @@ function registerAgentRoutes(app, opts) {
18080
18730
  async (request) => {
18081
18731
  const project = resolveProject2(opts.db, request.params.name);
18082
18732
  opts.sessionRegistry.reset(project.name);
18083
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(agentSessions.projectId, project.id)).run();
18733
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq33(agentSessions.projectId, project.id)).run();
18084
18734
  return { status: "reset" };
18085
18735
  }
18086
18736
  );
@@ -19102,7 +19752,7 @@ async function createServer(opts) {
19102
19752
  intelligenceService,
19103
19753
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
19104
19754
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
19105
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq33(projects.id, projectId)).get();
19755
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq34(projects.id, projectId)).get();
19106
19756
  if (!project) return;
19107
19757
  sessionRegistry.queueFollowUp(project.name, {
19108
19758
  role: "user",
@@ -19242,7 +19892,7 @@ async function createServer(opts) {
19242
19892
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
19243
19893
  if (opts.config.apiKey) {
19244
19894
  const keyHash = hashApiKey(opts.config.apiKey);
19245
- const existing = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, keyHash)).get();
19895
+ const existing = opts.db.select().from(apiKeys).where(eq34(apiKeys.keyHash, keyHash)).get();
19246
19896
  if (!existing) {
19247
19897
  const prefix = opts.config.apiKey.slice(0, 12);
19248
19898
  opts.db.insert(apiKeys).values({
@@ -19294,7 +19944,7 @@ async function createServer(opts) {
19294
19944
  };
19295
19945
  const getDefaultApiKey = () => {
19296
19946
  if (!opts.config.apiKey) return void 0;
19297
- return opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
19947
+ return opts.db.select().from(apiKeys).where(eq34(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
19298
19948
  };
19299
19949
  const createPasswordSession = (reply) => {
19300
19950
  const key = getDefaultApiKey();
@@ -19351,12 +20001,12 @@ async function createServer(opts) {
19351
20001
  return reply.send({ authenticated: true });
19352
20002
  }
19353
20003
  if (apiKey) {
19354
- const key = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(apiKey))).get();
20004
+ const key = opts.db.select().from(apiKeys).where(eq34(apiKeys.keyHash, hashApiKey(apiKey))).get();
19355
20005
  if (!key || key.revokedAt) {
19356
20006
  const err2 = authInvalid();
19357
20007
  return reply.status(err2.statusCode).send(err2.toJSON());
19358
20008
  }
19359
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq33(apiKeys.id, key.id)).run();
20009
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq34(apiKeys.id, key.id)).run();
19360
20010
  const sessionId = createSession(key.id);
19361
20011
  reply.header("set-cookie", serializeSessionCookie({
19362
20012
  name: SESSION_COOKIE_NAME,