@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.
- package/assets/agent-workspace/skills/aero/references/reporting.md +20 -0
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +1 -1
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +18 -0
- package/dist/{chunk-ALMP3NBQ.js → chunk-24C7RMIS.js} +17 -2
- package/dist/{chunk-HQ47AA6H.js → chunk-P6D3O5JB.js} +898 -248
- package/dist/cli.js +1014 -94
- package/dist/index.js +2 -2
- package/dist/mcp.js +1 -1
- package/package.json +4 -4
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
visibilityStateFromAnswerMentioned,
|
|
65
65
|
windowCutoff,
|
|
66
66
|
wordpressEnvSchema
|
|
67
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
2604
|
-
|
|
2605
|
-
|
|
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(
|
|
3250
|
+
).orderBy(desc6(querySnapshots.createdAt)).limit(limit + 1).all();
|
|
2614
3251
|
const insightMatches = app.db.select().from(insights).where(
|
|
2615
|
-
|
|
2616
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 = [
|
|
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(
|
|
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 = [
|
|
7550
|
-
if (url) conditions.push(
|
|
7551
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 ?
|
|
8315
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
8633
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
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
|
|
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(
|
|
8882
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
8883
|
-
app.db.delete(gaAiReferrals).where(
|
|
8884
|
-
app.db.delete(gaSocialReferrals).where(
|
|
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(
|
|
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
|
-
|
|
8965
|
-
|
|
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
|
-
|
|
8989
|
-
|
|
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
|
-
|
|
9014
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 = [
|
|
9738
|
+
const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
|
|
9090
9739
|
if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
9091
|
-
const aiConditions = [
|
|
9740
|
+
const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
|
|
9092
9741
|
if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
9093
|
-
const socialConditions = [
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
9172
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
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 = [
|
|
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(
|
|
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 = [
|
|
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(
|
|
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(
|
|
9285
|
-
|
|
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(
|
|
9298
|
-
|
|
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(
|
|
9306
|
-
|
|
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(
|
|
9348
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
9349
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
9350
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
9351
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
9366
|
-
|
|
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
|
-
|
|
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(
|
|
9372
|
-
|
|
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
|
-
|
|
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(
|
|
9392
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
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 = [
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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 ?
|
|
11549
|
-
return db.select().from(backlinkSummaries).where(condition).orderBy(
|
|
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 =
|
|
11558
|
-
|
|
11559
|
-
|
|
12206
|
+
const baseDomainCondition = and11(
|
|
12207
|
+
eq22(backlinkDomains.projectId, base.projectId),
|
|
12208
|
+
eq22(backlinkDomains.release, base.release)
|
|
11560
12209
|
);
|
|
11561
|
-
const filteredCondition =
|
|
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(
|
|
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(
|
|
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(
|
|
12290
|
+
}).where(eq22(ccReleaseSyncs.id, existing.id)).run();
|
|
11642
12291
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
11643
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
11738
|
-
|
|
11739
|
-
|
|
12386
|
+
const baseDomainCondition = and11(
|
|
12387
|
+
eq22(backlinkDomains.projectId, project.id),
|
|
12388
|
+
eq22(backlinkDomains.release, targetRelease)
|
|
11740
12389
|
);
|
|
11741
|
-
const domainCondition = excludeCrawlers ?
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
15419
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
15751
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
16355
|
-
tx.delete(backlinkSummaries).where(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
16889
|
-
|
|
16890
|
-
or3(
|
|
17538
|
+
and17(
|
|
17539
|
+
eq30(runs.projectId, projectId),
|
|
17540
|
+
or3(eq30(runs.status, "completed"), eq30(runs.status, "partial"))
|
|
16891
17541
|
)
|
|
16892
|
-
).orderBy(
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
17454
|
-
|
|
18103
|
+
and18(
|
|
18104
|
+
eq31(agentMemory.projectId, args.projectId),
|
|
17455
18105
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
17456
18106
|
)
|
|
17457
|
-
).orderBy(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|