@ainyc/canonry 2.10.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/assets/assets/{index-PhLDQh1e.js → index-Ckr4V5dK.js} +108 -108
- package/assets/index.html +1 -1
- package/dist/{chunk-SZSWQG3J.js → chunk-FCYNFM4B.js} +1055 -267
- package/dist/{chunk-KWQCQMPY.js → chunk-PLI7EOPM.js} +177 -0
- package/dist/{chunk-PYHANJ3B.js → chunk-UM6RDSRJ.js} +327 -0
- package/dist/cli.js +250 -12
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-2ZABHNR4.js → intelligence-service-54F3NGPM.js} +1 -1
- package/dist/mcp.js +62 -11
- package/package.json +9 -9
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ApiClient,
|
|
7
7
|
AppError,
|
|
8
8
|
CcReleaseSyncStatuses,
|
|
9
|
+
CitationStates,
|
|
9
10
|
MemorySources,
|
|
10
11
|
RunKinds,
|
|
11
12
|
RunStatuses,
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
visibilityStateFromAnswerMentioned,
|
|
55
56
|
windowCutoff,
|
|
56
57
|
wordpressEnvSchema
|
|
57
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-PLI7EOPM.js";
|
|
58
59
|
import {
|
|
59
60
|
IntelligenceService,
|
|
60
61
|
agentMemory,
|
|
@@ -65,6 +66,10 @@ import {
|
|
|
65
66
|
backlinkSummaries,
|
|
66
67
|
bingCoverageSnapshots,
|
|
67
68
|
bingUrlInspections,
|
|
69
|
+
buildContentGapRows,
|
|
70
|
+
buildContentSourceRows,
|
|
71
|
+
buildContentTargetRows,
|
|
72
|
+
buildInventory,
|
|
68
73
|
ccReleaseSyncs,
|
|
69
74
|
competitors,
|
|
70
75
|
createLogger,
|
|
@@ -79,6 +84,7 @@ import {
|
|
|
79
84
|
gscUrlInspections,
|
|
80
85
|
healthSnapshots,
|
|
81
86
|
insights,
|
|
87
|
+
isBlogShapedQuery,
|
|
82
88
|
keywords,
|
|
83
89
|
notifications,
|
|
84
90
|
parseJsonColumn,
|
|
@@ -87,7 +93,7 @@ import {
|
|
|
87
93
|
runs,
|
|
88
94
|
schedules,
|
|
89
95
|
usageCounters
|
|
90
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-UM6RDSRJ.js";
|
|
91
97
|
|
|
92
98
|
// src/telemetry.ts
|
|
93
99
|
import crypto from "crypto";
|
|
@@ -167,7 +173,7 @@ import crypto28 from "crypto";
|
|
|
167
173
|
import fs12 from "fs";
|
|
168
174
|
import path14 from "path";
|
|
169
175
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
170
|
-
import { eq as
|
|
176
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
171
177
|
import Fastify from "fastify";
|
|
172
178
|
|
|
173
179
|
// ../api-routes/src/auth.ts
|
|
@@ -2347,6 +2353,659 @@ async function intelligenceRoutes(app) {
|
|
|
2347
2353
|
});
|
|
2348
2354
|
}
|
|
2349
2355
|
|
|
2356
|
+
// ../api-routes/src/composites.ts
|
|
2357
|
+
import { eq as eq12, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
|
|
2358
|
+
var TOP_INSIGHT_LIMIT = 5;
|
|
2359
|
+
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
2360
|
+
var SEARCH_SNIPPET_RADIUS = 80;
|
|
2361
|
+
async function compositeRoutes(app) {
|
|
2362
|
+
app.get("/projects/:name/overview", async (request, reply) => {
|
|
2363
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2364
|
+
const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq12(runs.projectId, project.id)).get();
|
|
2365
|
+
const totalRuns = totalRunsRow?.count ?? 0;
|
|
2366
|
+
const recentRuns = app.db.select().from(runs).where(eq12(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
|
|
2367
|
+
const [latestRunRow, previousRunRow] = recentRuns;
|
|
2368
|
+
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
2369
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
|
|
2370
|
+
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
2371
|
+
const insightRows = app.db.select().from(insights).where(eq12(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
|
|
2372
|
+
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
2373
|
+
const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
|
|
2374
|
+
const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
|
|
2375
|
+
const result = {
|
|
2376
|
+
project: formatProject2(project),
|
|
2377
|
+
latestRun,
|
|
2378
|
+
health,
|
|
2379
|
+
topInsights,
|
|
2380
|
+
keywordCounts,
|
|
2381
|
+
providers,
|
|
2382
|
+
transitions
|
|
2383
|
+
};
|
|
2384
|
+
return reply.send(result);
|
|
2385
|
+
});
|
|
2386
|
+
app.get("/projects/:name/search", async (request, reply) => {
|
|
2387
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2388
|
+
const rawQuery = (request.query.q ?? "").trim();
|
|
2389
|
+
if (rawQuery.length < 2) {
|
|
2390
|
+
throw validationError('"q" must be at least 2 characters');
|
|
2391
|
+
}
|
|
2392
|
+
const limit = clampSearchLimit(request.query.limit);
|
|
2393
|
+
const escaped = escapeLikePattern(rawQuery);
|
|
2394
|
+
const pattern = `%${escaped}%`;
|
|
2395
|
+
const snapshotMatches = app.db.select({
|
|
2396
|
+
id: querySnapshots.id,
|
|
2397
|
+
runId: querySnapshots.runId,
|
|
2398
|
+
keywordId: querySnapshots.keywordId,
|
|
2399
|
+
keywordText: keywords.keyword,
|
|
2400
|
+
provider: querySnapshots.provider,
|
|
2401
|
+
model: querySnapshots.model,
|
|
2402
|
+
citationState: querySnapshots.citationState,
|
|
2403
|
+
answerText: querySnapshots.answerText,
|
|
2404
|
+
citedDomains: querySnapshots.citedDomains,
|
|
2405
|
+
rawResponse: querySnapshots.rawResponse,
|
|
2406
|
+
createdAt: querySnapshots.createdAt
|
|
2407
|
+
}).from(querySnapshots).innerJoin(keywords, eq12(querySnapshots.keywordId, keywords.id)).where(
|
|
2408
|
+
and3(
|
|
2409
|
+
eq12(keywords.projectId, project.id),
|
|
2410
|
+
or2(
|
|
2411
|
+
sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
2412
|
+
sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
2413
|
+
sql3`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
|
|
2414
|
+
like(keywords.keyword, pattern)
|
|
2415
|
+
)
|
|
2416
|
+
)
|
|
2417
|
+
).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
|
|
2418
|
+
const insightMatches = app.db.select().from(insights).where(
|
|
2419
|
+
and3(
|
|
2420
|
+
eq12(insights.projectId, project.id),
|
|
2421
|
+
or2(
|
|
2422
|
+
like(insights.title, pattern),
|
|
2423
|
+
like(insights.keyword, pattern),
|
|
2424
|
+
sql3`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
2425
|
+
sql3`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
2426
|
+
)
|
|
2427
|
+
)
|
|
2428
|
+
).orderBy(desc5(insights.createdAt)).limit(limit + 1).all();
|
|
2429
|
+
const hits = [];
|
|
2430
|
+
for (const row of snapshotMatches) {
|
|
2431
|
+
hits.push(buildSnapshotHit(row, rawQuery));
|
|
2432
|
+
}
|
|
2433
|
+
for (const row of insightMatches) {
|
|
2434
|
+
hits.push(buildInsightHit(row, rawQuery));
|
|
2435
|
+
}
|
|
2436
|
+
hits.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
2437
|
+
const truncated = hits.length > limit;
|
|
2438
|
+
const trimmed = truncated ? hits.slice(0, limit) : hits;
|
|
2439
|
+
const response = {
|
|
2440
|
+
query: rawQuery,
|
|
2441
|
+
totalHits: trimmed.length,
|
|
2442
|
+
truncated,
|
|
2443
|
+
hits: trimmed
|
|
2444
|
+
};
|
|
2445
|
+
return reply.send(response);
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
function clampSearchLimit(raw) {
|
|
2449
|
+
if (!raw) return 25;
|
|
2450
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2451
|
+
if (Number.isNaN(parsed)) return 25;
|
|
2452
|
+
if (parsed < 1) return 1;
|
|
2453
|
+
if (parsed > SEARCH_HIT_HARD_LIMIT) return SEARCH_HIT_HARD_LIMIT;
|
|
2454
|
+
return parsed;
|
|
2455
|
+
}
|
|
2456
|
+
function escapeLikePattern(value) {
|
|
2457
|
+
return value.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
2458
|
+
}
|
|
2459
|
+
function summarizeRun(run) {
|
|
2460
|
+
return {
|
|
2461
|
+
id: run.id,
|
|
2462
|
+
projectId: run.projectId,
|
|
2463
|
+
kind: run.kind,
|
|
2464
|
+
status: run.status,
|
|
2465
|
+
trigger: run.trigger,
|
|
2466
|
+
location: run.location,
|
|
2467
|
+
startedAt: run.startedAt,
|
|
2468
|
+
finishedAt: run.finishedAt,
|
|
2469
|
+
error: parseRunError(run.error),
|
|
2470
|
+
createdAt: run.createdAt
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
function summarizeLatestRun(app, run) {
|
|
2474
|
+
const empty = {
|
|
2475
|
+
keywordCounts: { totalKeywords: 0, citedKeywords: 0, notCitedKeywords: 0, citedRate: 0 },
|
|
2476
|
+
providers: []
|
|
2477
|
+
};
|
|
2478
|
+
if (!run) return empty;
|
|
2479
|
+
const rows = app.db.select({
|
|
2480
|
+
keywordId: querySnapshots.keywordId,
|
|
2481
|
+
provider: querySnapshots.provider,
|
|
2482
|
+
citationState: querySnapshots.citationState
|
|
2483
|
+
}).from(querySnapshots).where(eq12(querySnapshots.runId, run.id)).all();
|
|
2484
|
+
if (rows.length === 0) return empty;
|
|
2485
|
+
const perKeyword = /* @__PURE__ */ new Map();
|
|
2486
|
+
const perProvider = /* @__PURE__ */ new Map();
|
|
2487
|
+
for (const row of rows) {
|
|
2488
|
+
const cited = row.citationState === "cited";
|
|
2489
|
+
if (!perKeyword.has(row.keywordId) || cited) {
|
|
2490
|
+
perKeyword.set(row.keywordId, cited);
|
|
2491
|
+
}
|
|
2492
|
+
const bucket = perProvider.get(row.provider) ?? { cited: 0, total: 0 };
|
|
2493
|
+
bucket.total += 1;
|
|
2494
|
+
if (cited) bucket.cited += 1;
|
|
2495
|
+
perProvider.set(row.provider, bucket);
|
|
2496
|
+
}
|
|
2497
|
+
const totalKeywords = perKeyword.size;
|
|
2498
|
+
let citedKeywords = 0;
|
|
2499
|
+
for (const wasCited of perKeyword.values()) {
|
|
2500
|
+
if (wasCited) citedKeywords += 1;
|
|
2501
|
+
}
|
|
2502
|
+
const notCitedKeywords = totalKeywords - citedKeywords;
|
|
2503
|
+
const citedRate = totalKeywords === 0 ? 0 : Number((citedKeywords / totalKeywords).toFixed(4));
|
|
2504
|
+
const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
|
|
2505
|
+
provider,
|
|
2506
|
+
cited,
|
|
2507
|
+
total,
|
|
2508
|
+
citedRate: total === 0 ? 0 : Number((cited / total).toFixed(4))
|
|
2509
|
+
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
2510
|
+
return {
|
|
2511
|
+
keywordCounts: { totalKeywords, citedKeywords, notCitedKeywords, citedRate },
|
|
2512
|
+
providers
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function summarizeTransitions(app, latest, previous) {
|
|
2516
|
+
const empty = { since: null, gained: 0, lost: 0, emerging: 0 };
|
|
2517
|
+
if (!latest || !previous) return empty;
|
|
2518
|
+
const fetchCited = (runId) => {
|
|
2519
|
+
const rows = app.db.select({
|
|
2520
|
+
keywordId: querySnapshots.keywordId,
|
|
2521
|
+
citationState: querySnapshots.citationState
|
|
2522
|
+
}).from(querySnapshots).where(eq12(querySnapshots.runId, runId)).all();
|
|
2523
|
+
const map = /* @__PURE__ */ new Map();
|
|
2524
|
+
for (const row of rows) {
|
|
2525
|
+
const cited = row.citationState === "cited";
|
|
2526
|
+
if (!map.has(row.keywordId) || cited) map.set(row.keywordId, cited);
|
|
2527
|
+
}
|
|
2528
|
+
return map;
|
|
2529
|
+
};
|
|
2530
|
+
const latestMap = fetchCited(latest.id);
|
|
2531
|
+
const previousMap = fetchCited(previous.id);
|
|
2532
|
+
let gained = 0;
|
|
2533
|
+
let lost = 0;
|
|
2534
|
+
let emerging = 0;
|
|
2535
|
+
for (const [keywordId, latestCited] of latestMap) {
|
|
2536
|
+
const previousCited = previousMap.get(keywordId);
|
|
2537
|
+
if (previousCited === void 0) {
|
|
2538
|
+
if (latestCited) emerging += 1;
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
if (latestCited && !previousCited) gained += 1;
|
|
2542
|
+
else if (!latestCited && previousCited) lost += 1;
|
|
2543
|
+
}
|
|
2544
|
+
return { since: previous.createdAt, gained, lost, emerging };
|
|
2545
|
+
}
|
|
2546
|
+
function mapInsightRow2(r) {
|
|
2547
|
+
return {
|
|
2548
|
+
id: r.id,
|
|
2549
|
+
projectId: r.projectId,
|
|
2550
|
+
runId: r.runId ?? null,
|
|
2551
|
+
type: r.type,
|
|
2552
|
+
severity: r.severity,
|
|
2553
|
+
title: r.title,
|
|
2554
|
+
keyword: r.keyword,
|
|
2555
|
+
provider: r.provider,
|
|
2556
|
+
recommendation: parseJsonColumn(r.recommendation, void 0),
|
|
2557
|
+
cause: parseJsonColumn(r.cause, void 0),
|
|
2558
|
+
dismissed: r.dismissed,
|
|
2559
|
+
createdAt: r.createdAt
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
function mapHealthRow2(r) {
|
|
2563
|
+
return {
|
|
2564
|
+
id: r.id,
|
|
2565
|
+
projectId: r.projectId,
|
|
2566
|
+
runId: r.runId ?? null,
|
|
2567
|
+
overallCitedRate: Number(r.overallCitedRate),
|
|
2568
|
+
totalPairs: r.totalPairs,
|
|
2569
|
+
citedPairs: r.citedPairs,
|
|
2570
|
+
providerBreakdown: parseJsonColumn(r.providerBreakdown, {}),
|
|
2571
|
+
createdAt: r.createdAt,
|
|
2572
|
+
status: "ready"
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
function formatProject2(row) {
|
|
2576
|
+
return {
|
|
2577
|
+
id: row.id,
|
|
2578
|
+
name: row.name,
|
|
2579
|
+
displayName: row.displayName,
|
|
2580
|
+
canonicalDomain: row.canonicalDomain,
|
|
2581
|
+
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
2582
|
+
country: row.country,
|
|
2583
|
+
language: row.language,
|
|
2584
|
+
tags: parseJsonColumn(row.tags, []),
|
|
2585
|
+
labels: parseJsonColumn(row.labels, {}),
|
|
2586
|
+
locations: parseJsonColumn(row.locations, []),
|
|
2587
|
+
defaultLocation: row.defaultLocation,
|
|
2588
|
+
autoExtractBacklinks: row.autoExtractBacklinks === 1,
|
|
2589
|
+
configSource: row.configSource,
|
|
2590
|
+
configRevision: row.configRevision,
|
|
2591
|
+
createdAt: row.createdAt,
|
|
2592
|
+
updatedAt: row.updatedAt
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
function buildSnapshotHit(row, query) {
|
|
2596
|
+
const lower = query.toLowerCase();
|
|
2597
|
+
const keyword = row.keywordText ?? "";
|
|
2598
|
+
const answer = row.answerText ?? "";
|
|
2599
|
+
const cited = row.citedDomains;
|
|
2600
|
+
const raw = row.rawResponse ?? "";
|
|
2601
|
+
let matchedField;
|
|
2602
|
+
let snippet;
|
|
2603
|
+
if (answer.toLowerCase().includes(lower)) {
|
|
2604
|
+
matchedField = "answerText";
|
|
2605
|
+
snippet = makeSnippet(answer, query);
|
|
2606
|
+
} else if (cited.toLowerCase().includes(lower)) {
|
|
2607
|
+
matchedField = "citedDomains";
|
|
2608
|
+
snippet = makeSnippet(cited, query);
|
|
2609
|
+
} else if (raw.toLowerCase().includes(lower)) {
|
|
2610
|
+
matchedField = "searchQueries";
|
|
2611
|
+
snippet = makeSnippet(raw, query);
|
|
2612
|
+
} else {
|
|
2613
|
+
matchedField = "keyword";
|
|
2614
|
+
snippet = keyword;
|
|
2615
|
+
}
|
|
2616
|
+
return {
|
|
2617
|
+
kind: "snapshot",
|
|
2618
|
+
id: row.id,
|
|
2619
|
+
runId: row.runId,
|
|
2620
|
+
keyword,
|
|
2621
|
+
provider: row.provider,
|
|
2622
|
+
model: row.model,
|
|
2623
|
+
citationState: row.citationState,
|
|
2624
|
+
matchedField,
|
|
2625
|
+
snippet,
|
|
2626
|
+
createdAt: row.createdAt
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
function buildInsightHit(row, query) {
|
|
2630
|
+
const lower = query.toLowerCase();
|
|
2631
|
+
const recommendation = row.recommendation ?? "";
|
|
2632
|
+
const cause = row.cause ?? "";
|
|
2633
|
+
let matchedField;
|
|
2634
|
+
let snippet;
|
|
2635
|
+
if (row.title.toLowerCase().includes(lower)) {
|
|
2636
|
+
matchedField = "title";
|
|
2637
|
+
snippet = makeSnippet(row.title, query);
|
|
2638
|
+
} else if (row.keyword.toLowerCase().includes(lower)) {
|
|
2639
|
+
matchedField = "keyword";
|
|
2640
|
+
snippet = row.keyword;
|
|
2641
|
+
} else if (recommendation.toLowerCase().includes(lower)) {
|
|
2642
|
+
matchedField = "recommendation";
|
|
2643
|
+
snippet = makeSnippet(recommendation, query);
|
|
2644
|
+
} else {
|
|
2645
|
+
matchedField = "cause";
|
|
2646
|
+
snippet = makeSnippet(cause, query);
|
|
2647
|
+
}
|
|
2648
|
+
return {
|
|
2649
|
+
kind: "insight",
|
|
2650
|
+
id: row.id,
|
|
2651
|
+
runId: row.runId ?? null,
|
|
2652
|
+
type: row.type,
|
|
2653
|
+
severity: row.severity,
|
|
2654
|
+
title: row.title,
|
|
2655
|
+
keyword: row.keyword,
|
|
2656
|
+
provider: row.provider,
|
|
2657
|
+
matchedField,
|
|
2658
|
+
snippet,
|
|
2659
|
+
dismissed: row.dismissed,
|
|
2660
|
+
createdAt: row.createdAt
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
function makeSnippet(text, query) {
|
|
2664
|
+
if (!text) return "";
|
|
2665
|
+
const needle = query.toLowerCase();
|
|
2666
|
+
const haystack = text.toLowerCase();
|
|
2667
|
+
const idx = haystack.indexOf(needle);
|
|
2668
|
+
if (idx < 0) {
|
|
2669
|
+
return text.length <= SEARCH_SNIPPET_RADIUS * 2 ? text : `${text.slice(0, SEARCH_SNIPPET_RADIUS * 2)}\u2026`;
|
|
2670
|
+
}
|
|
2671
|
+
const start = Math.max(0, idx - SEARCH_SNIPPET_RADIUS);
|
|
2672
|
+
const end = Math.min(text.length, idx + query.length + SEARCH_SNIPPET_RADIUS);
|
|
2673
|
+
const prefix = start === 0 ? "" : "\u2026";
|
|
2674
|
+
const suffix = end === text.length ? "" : "\u2026";
|
|
2675
|
+
return `${prefix}${text.slice(start, end)}${suffix}`;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// ../api-routes/src/content-data.ts
|
|
2679
|
+
import { and as and4, eq as eq13, desc as desc6, inArray as inArray3 } from "drizzle-orm";
|
|
2680
|
+
var RECENT_RUNS_WINDOW = 5;
|
|
2681
|
+
function loadOrchestratorInput(db, project) {
|
|
2682
|
+
const projectId = project.id;
|
|
2683
|
+
const ownDomain = normalizeDomain(project.canonicalDomain);
|
|
2684
|
+
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
2685
|
+
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
|
|
2686
|
+
const trackedKeywords = listKeywords(db, projectId);
|
|
2687
|
+
const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
|
|
2688
|
+
const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
|
|
2689
|
+
const competitorSet = new Set(trackedCompetitors);
|
|
2690
|
+
const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
|
|
2691
|
+
const latestRunId = recentRunIds[0] ?? "";
|
|
2692
|
+
const latestRunTimestamp = latestRunId ? lookupRunTimestamp(db, latestRunId) : "";
|
|
2693
|
+
const candidateQueries = buildCandidateQueries({
|
|
2694
|
+
db,
|
|
2695
|
+
projectId,
|
|
2696
|
+
candidateQueryStrings,
|
|
2697
|
+
recentRunIds,
|
|
2698
|
+
latestRunId,
|
|
2699
|
+
ourDomains,
|
|
2700
|
+
competitorSet
|
|
2701
|
+
});
|
|
2702
|
+
const inventory = buildInventory({
|
|
2703
|
+
gscPages: listGscPagesForProject(db, projectId),
|
|
2704
|
+
ga4LandingPages: listGa4LandingPagesForProject(db, projectId),
|
|
2705
|
+
sitemapUrls: [],
|
|
2706
|
+
wpPosts: []
|
|
2707
|
+
});
|
|
2708
|
+
const gaTrafficByPage = buildGaTrafficByPage(db, projectId);
|
|
2709
|
+
const totalAiReferralSessions = sumAiReferralSessions(db, projectId);
|
|
2710
|
+
return {
|
|
2711
|
+
projectId,
|
|
2712
|
+
ownDomain,
|
|
2713
|
+
competitors: trackedCompetitors,
|
|
2714
|
+
candidateQueries,
|
|
2715
|
+
inventory,
|
|
2716
|
+
wpSchemaAudit: /* @__PURE__ */ new Map(),
|
|
2717
|
+
gaTrafficByPage,
|
|
2718
|
+
totalAiReferralSessions,
|
|
2719
|
+
latestRunId,
|
|
2720
|
+
latestRunTimestamp,
|
|
2721
|
+
inProgressActions: /* @__PURE__ */ new Map()
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
function listKeywords(db, projectId) {
|
|
2725
|
+
const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, projectId)).all();
|
|
2726
|
+
return rows.map((r) => r.text);
|
|
2727
|
+
}
|
|
2728
|
+
function listCompetitorDomains(db, projectId) {
|
|
2729
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
|
|
2730
|
+
return rows.map((r) => r.domain);
|
|
2731
|
+
}
|
|
2732
|
+
function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
|
|
2733
|
+
const rows = db.select({ id: runs.id }).from(runs).where(
|
|
2734
|
+
and4(
|
|
2735
|
+
eq13(runs.projectId, projectId),
|
|
2736
|
+
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
2737
|
+
// Queued/running/failed/cancelled runs may have partial or no
|
|
2738
|
+
// snapshots; including them risks pointing latestRunId at a run with
|
|
2739
|
+
// no usable evidence.
|
|
2740
|
+
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2741
|
+
)
|
|
2742
|
+
).orderBy(desc6(runs.createdAt)).limit(limit).all();
|
|
2743
|
+
return rows.map((r) => r.id);
|
|
2744
|
+
}
|
|
2745
|
+
function lookupRunTimestamp(db, runId) {
|
|
2746
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
|
|
2747
|
+
return row?.createdAt ?? "";
|
|
2748
|
+
}
|
|
2749
|
+
function listGscPagesForProject(db, projectId) {
|
|
2750
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
2751
|
+
return rows.map((r) => r.page);
|
|
2752
|
+
}
|
|
2753
|
+
function listGa4LandingPagesForProject(db, projectId) {
|
|
2754
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2755
|
+
return rows.map((r) => r.landingPage);
|
|
2756
|
+
}
|
|
2757
|
+
function buildGaTrafficByPage(db, projectId) {
|
|
2758
|
+
const rows = db.select({
|
|
2759
|
+
landingPage: gaTrafficSnapshots.landingPage,
|
|
2760
|
+
sessions: gaTrafficSnapshots.sessions
|
|
2761
|
+
}).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2762
|
+
const map = /* @__PURE__ */ new Map();
|
|
2763
|
+
for (const row of rows) {
|
|
2764
|
+
const path15 = extractPath(row.landingPage);
|
|
2765
|
+
if (!path15) continue;
|
|
2766
|
+
map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
|
|
2767
|
+
}
|
|
2768
|
+
return map;
|
|
2769
|
+
}
|
|
2770
|
+
function sumAiReferralSessions(db, projectId) {
|
|
2771
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
|
|
2772
|
+
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
2773
|
+
}
|
|
2774
|
+
function buildCandidateQueries(opts) {
|
|
2775
|
+
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
2776
|
+
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
2777
|
+
}
|
|
2778
|
+
const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, opts.projectId)).all();
|
|
2779
|
+
const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
|
|
2780
|
+
const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
|
|
2781
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
|
|
2782
|
+
const snapshotsByKeyword = /* @__PURE__ */ new Map();
|
|
2783
|
+
for (const row of snapshotRows) {
|
|
2784
|
+
const list = snapshotsByKeyword.get(row.keywordId) ?? [];
|
|
2785
|
+
list.push(row);
|
|
2786
|
+
snapshotsByKeyword.set(row.keywordId, list);
|
|
2787
|
+
}
|
|
2788
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
|
|
2789
|
+
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
2790
|
+
return opts.candidateQueryStrings.map((query) => {
|
|
2791
|
+
const keywordId = keywordIdByText.get(query);
|
|
2792
|
+
const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
|
|
2793
|
+
const gsc = gscByQuery.get(query) ?? null;
|
|
2794
|
+
return aggregateCandidate({
|
|
2795
|
+
query,
|
|
2796
|
+
snapshots: snaps,
|
|
2797
|
+
gsc,
|
|
2798
|
+
ourDomains: opts.ourDomains,
|
|
2799
|
+
competitorSet: opts.competitorSet,
|
|
2800
|
+
latestRunId: opts.latestRunId
|
|
2801
|
+
});
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
function aggregateGscByQuery(rows) {
|
|
2805
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
2806
|
+
for (const r of rows) {
|
|
2807
|
+
const existing = byQuery.get(r.query);
|
|
2808
|
+
const candidate = {
|
|
2809
|
+
// GSC stores `page` as a full URL for url-prefix properties; normalize to
|
|
2810
|
+
// a path so it can be joined against `gaTrafficByPage` (which is keyed by
|
|
2811
|
+
// path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
|
|
2812
|
+
// of whether the page is sourced from GSC or from inventory.
|
|
2813
|
+
page: extractPath(r.page),
|
|
2814
|
+
position: Number(r.position) || 0,
|
|
2815
|
+
impressions: r.impressions,
|
|
2816
|
+
clicks: r.clicks,
|
|
2817
|
+
ctr: Number(r.ctr) || 0
|
|
2818
|
+
};
|
|
2819
|
+
if (!existing) {
|
|
2820
|
+
byQuery.set(r.query, candidate);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
if (candidate.impressions > existing.impressions) {
|
|
2824
|
+
byQuery.set(r.query, candidate);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
return byQuery;
|
|
2828
|
+
}
|
|
2829
|
+
function aggregateCandidate(opts) {
|
|
2830
|
+
const totalSnaps = opts.snapshots.length;
|
|
2831
|
+
if (totalSnaps === 0) {
|
|
2832
|
+
return {
|
|
2833
|
+
...emptyCandidate(opts.query),
|
|
2834
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2835
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2836
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2837
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2838
|
+
gscCtr: opts.gsc?.ctr ?? 0
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
const citedCount = opts.snapshots.filter((s) => s.citationState === CitationStates.cited).length;
|
|
2842
|
+
const ourCitedRate = citedCount / totalSnaps;
|
|
2843
|
+
const recentMissRate = 1 - ourCitedRate;
|
|
2844
|
+
const competitorTally = /* @__PURE__ */ new Map();
|
|
2845
|
+
const competitorGroundingTally = /* @__PURE__ */ new Map();
|
|
2846
|
+
const ourGroundingTally = /* @__PURE__ */ new Map();
|
|
2847
|
+
let ourCitedInLatestRun = false;
|
|
2848
|
+
for (const snap of opts.snapshots) {
|
|
2849
|
+
const isLatestRun = snap.runId === opts.latestRunId;
|
|
2850
|
+
const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
|
|
2851
|
+
for (const domain of competitorOverlap) {
|
|
2852
|
+
const normalized = normalizeDomain(domain);
|
|
2853
|
+
if (!opts.competitorSet.has(normalized)) continue;
|
|
2854
|
+
competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
|
|
2855
|
+
}
|
|
2856
|
+
const grounding = extractGroundingSources(snap.rawResponse);
|
|
2857
|
+
for (const g of grounding) {
|
|
2858
|
+
const domain = normalizeDomain(extractHostFromUri(g.uri));
|
|
2859
|
+
if (!domain) continue;
|
|
2860
|
+
if (opts.ourDomains.has(domain)) {
|
|
2861
|
+
if (isLatestRun) ourCitedInLatestRun = true;
|
|
2862
|
+
recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (!opts.competitorSet.has(domain)) continue;
|
|
2866
|
+
recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
query: opts.query,
|
|
2871
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2872
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2873
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2874
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2875
|
+
gscCtr: opts.gsc?.ctr ?? 0,
|
|
2876
|
+
ourCitedRate,
|
|
2877
|
+
ourCitedInLatestRun,
|
|
2878
|
+
competitorDomains: Array.from(competitorTally.keys()),
|
|
2879
|
+
competitorCitationCount: Array.from(competitorTally.values()).reduce((a, b) => a + b, 0),
|
|
2880
|
+
recentMissRate,
|
|
2881
|
+
ourGroundingUrls: Array.from(ourGroundingTally.values()),
|
|
2882
|
+
competitorGroundingUrls: Array.from(competitorGroundingTally.values()),
|
|
2883
|
+
runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
function recordGroundingHit(tally, g, domain, provider) {
|
|
2887
|
+
const existing = tally.get(g.uri);
|
|
2888
|
+
if (existing) {
|
|
2889
|
+
existing.citationCount += 1;
|
|
2890
|
+
if (provider && !existing.providers.includes(provider)) {
|
|
2891
|
+
existing.providers.push(provider);
|
|
2892
|
+
}
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
tally.set(g.uri, {
|
|
2896
|
+
uri: g.uri,
|
|
2897
|
+
title: g.title,
|
|
2898
|
+
domain,
|
|
2899
|
+
citationCount: 1,
|
|
2900
|
+
providers: provider ? [provider] : []
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
function emptyCandidate(query) {
|
|
2904
|
+
return {
|
|
2905
|
+
query,
|
|
2906
|
+
gscPage: null,
|
|
2907
|
+
gscPosition: null,
|
|
2908
|
+
gscImpressions: 0,
|
|
2909
|
+
gscClicks: 0,
|
|
2910
|
+
gscCtr: 0,
|
|
2911
|
+
ourCitedRate: 0,
|
|
2912
|
+
ourCitedInLatestRun: false,
|
|
2913
|
+
competitorDomains: [],
|
|
2914
|
+
competitorCitationCount: 0,
|
|
2915
|
+
recentMissRate: 0,
|
|
2916
|
+
ourGroundingUrls: [],
|
|
2917
|
+
competitorGroundingUrls: [],
|
|
2918
|
+
runsOfHistory: 0
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
function extractGroundingSources(rawResponse) {
|
|
2922
|
+
if (!rawResponse) return [];
|
|
2923
|
+
try {
|
|
2924
|
+
const parsed = JSON.parse(rawResponse);
|
|
2925
|
+
if (parsed && typeof parsed === "object" && "groundingSources" in parsed) {
|
|
2926
|
+
const grounding = parsed.groundingSources;
|
|
2927
|
+
if (Array.isArray(grounding)) {
|
|
2928
|
+
return grounding.filter(
|
|
2929
|
+
(g) => typeof g === "object" && g !== null && typeof g.uri === "string"
|
|
2930
|
+
).map((g) => ({ uri: g.uri, title: g.title ?? "" }));
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
} catch {
|
|
2934
|
+
}
|
|
2935
|
+
return [];
|
|
2936
|
+
}
|
|
2937
|
+
function extractHostFromUri(uri) {
|
|
2938
|
+
try {
|
|
2939
|
+
return new URL(uri).hostname;
|
|
2940
|
+
} catch {
|
|
2941
|
+
return "";
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
function normalizeDomain(domain) {
|
|
2945
|
+
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
2946
|
+
}
|
|
2947
|
+
function extractPath(url) {
|
|
2948
|
+
if (!url) return "";
|
|
2949
|
+
const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
|
|
2950
|
+
const path15 = match ? match[1] : url.trim();
|
|
2951
|
+
const stripped = path15.replace(/\/+$/, "");
|
|
2952
|
+
return stripped || "/";
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// ../api-routes/src/content.ts
|
|
2956
|
+
async function contentRoutes(app) {
|
|
2957
|
+
app.get("/projects/:name/content/targets", async (request) => {
|
|
2958
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2959
|
+
const includeInProgress = request.query["include-in-progress"] === "true";
|
|
2960
|
+
const limit = parseLimitParam(request.query.limit);
|
|
2961
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2962
|
+
let rows = buildContentTargetRows(input);
|
|
2963
|
+
if (!includeInProgress) {
|
|
2964
|
+
rows = rows.filter((r) => r.existingAction === null);
|
|
2965
|
+
}
|
|
2966
|
+
if (limit !== void 0) {
|
|
2967
|
+
rows = rows.slice(0, limit);
|
|
2968
|
+
}
|
|
2969
|
+
const response = {
|
|
2970
|
+
targets: rows,
|
|
2971
|
+
contextMetrics: {
|
|
2972
|
+
totalAiReferralSessions: input.totalAiReferralSessions,
|
|
2973
|
+
latestRunId: input.latestRunId,
|
|
2974
|
+
runTimestamp: input.latestRunTimestamp
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
return response;
|
|
2978
|
+
});
|
|
2979
|
+
app.get("/projects/:name/content/sources", async (request) => {
|
|
2980
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2981
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2982
|
+
const rows = buildContentSourceRows(input);
|
|
2983
|
+
const response = {
|
|
2984
|
+
sources: rows,
|
|
2985
|
+
latestRunId: input.latestRunId
|
|
2986
|
+
};
|
|
2987
|
+
return response;
|
|
2988
|
+
});
|
|
2989
|
+
app.get("/projects/:name/content/gaps", async (request) => {
|
|
2990
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2991
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2992
|
+
const rows = buildContentGapRows(input);
|
|
2993
|
+
const response = {
|
|
2994
|
+
gaps: rows,
|
|
2995
|
+
latestRunId: input.latestRunId
|
|
2996
|
+
};
|
|
2997
|
+
return response;
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
function parseLimitParam(raw) {
|
|
3001
|
+
if (raw === void 0) return void 0;
|
|
3002
|
+
const parsed = Number(raw);
|
|
3003
|
+
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
|
|
3004
|
+
throw validationError('"limit" must be a non-negative integer');
|
|
3005
|
+
}
|
|
3006
|
+
return parsed;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
2350
3009
|
// ../api-routes/src/openapi.ts
|
|
2351
3010
|
var stringSchema = { type: "string" };
|
|
2352
3011
|
var booleanSchema = { type: "boolean" };
|
|
@@ -4573,6 +5232,77 @@ var routeCatalog = [
|
|
|
4573
5232
|
404: { description: "Project not found." }
|
|
4574
5233
|
}
|
|
4575
5234
|
},
|
|
5235
|
+
// Content opportunity engine
|
|
5236
|
+
{
|
|
5237
|
+
method: "get",
|
|
5238
|
+
path: "/api/v1/projects/{name}/content/targets",
|
|
5239
|
+
summary: "Ranked, action-typed content opportunities",
|
|
5240
|
+
description: "Returns the canonical opportunity list. Each row is `{query, action, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, existingAction?}`. Hides rows with in-progress actions by default; pass `?include-in-progress=true` to include them annotated.",
|
|
5241
|
+
tags: ["content"],
|
|
5242
|
+
parameters: [
|
|
5243
|
+
nameParameter,
|
|
5244
|
+
{ name: "limit", in: "query", description: "Max rows returned.", schema: stringSchema },
|
|
5245
|
+
{ name: "include-in-progress", in: "query", description: "Include rows with in-flight tracked actions.", schema: stringSchema }
|
|
5246
|
+
],
|
|
5247
|
+
responses: {
|
|
5248
|
+
200: { description: "Targets returned." },
|
|
5249
|
+
400: { description: "Invalid limit." },
|
|
5250
|
+
404: { description: "Project not found." }
|
|
5251
|
+
}
|
|
5252
|
+
},
|
|
5253
|
+
{
|
|
5254
|
+
method: "get",
|
|
5255
|
+
path: "/api/v1/projects/{name}/content/sources",
|
|
5256
|
+
summary: "URL-level competitive grounding-source map per query",
|
|
5257
|
+
description: "Returns one row per blog-shaped query containing the grounding URLs the LLM cited. Distinguishes our domain (isOurDomain) from competitor URLs (isCompetitor). Pure DB read \u2014 canonry surfaces URLs but never fetches them.",
|
|
5258
|
+
tags: ["content"],
|
|
5259
|
+
parameters: [nameParameter],
|
|
5260
|
+
responses: {
|
|
5261
|
+
200: { description: "Sources returned." },
|
|
5262
|
+
404: { description: "Project not found." }
|
|
5263
|
+
}
|
|
5264
|
+
},
|
|
5265
|
+
{
|
|
5266
|
+
method: "get",
|
|
5267
|
+
path: "/api/v1/projects/{name}/content/gaps",
|
|
5268
|
+
summary: "Queries where competitors are cited but you are not",
|
|
5269
|
+
description: "Returns gap rows ranked by miss rate then by competitor count. Excludes queries with no competitor citations and queries where our cited rate is 100%.",
|
|
5270
|
+
tags: ["content"],
|
|
5271
|
+
parameters: [nameParameter],
|
|
5272
|
+
responses: {
|
|
5273
|
+
200: { description: "Gaps returned." },
|
|
5274
|
+
404: { description: "Project not found." }
|
|
5275
|
+
}
|
|
5276
|
+
},
|
|
5277
|
+
{
|
|
5278
|
+
method: "get",
|
|
5279
|
+
path: "/api/v1/projects/{name}/overview",
|
|
5280
|
+
summary: "Get a composite overview of project health",
|
|
5281
|
+
description: 'Bundles project info, latest run, top undismissed insights, the latest health snapshot, keyword cited rate, per-provider breakdown, and transitions vs. the previous run. Designed for the "how is project X doing?" question so agents can answer in one call.',
|
|
5282
|
+
tags: ["intelligence"],
|
|
5283
|
+
parameters: [nameParameter],
|
|
5284
|
+
responses: {
|
|
5285
|
+
200: { description: "Overview returned." },
|
|
5286
|
+
404: { description: "Project not found." }
|
|
5287
|
+
}
|
|
5288
|
+
},
|
|
5289
|
+
{
|
|
5290
|
+
method: "get",
|
|
5291
|
+
path: "/api/v1/projects/{name}/search",
|
|
5292
|
+
summary: "Search query snapshots and insights for text",
|
|
5293
|
+
description: "Returns the most recent snapshots and insights whose answer text, cited domains, raw response, or insight title/keyword/recommendation/cause matches the query. Use to find anything mentioning a competitor, term, or URL without paginating snapshots.",
|
|
5294
|
+
tags: ["intelligence"],
|
|
5295
|
+
parameters: [
|
|
5296
|
+
nameParameter,
|
|
5297
|
+
{ name: "q", in: "query", required: true, description: "Search term (>= 2 chars).", schema: stringSchema },
|
|
5298
|
+
{ name: "limit", in: "query", description: "Max combined hits (1-50, default 25).", schema: stringSchema }
|
|
5299
|
+
],
|
|
5300
|
+
responses: {
|
|
5301
|
+
200: { description: "Search hits returned." },
|
|
5302
|
+
400: { description: "Query string missing or too short." },
|
|
5303
|
+
404: { description: "Project not found." }
|
|
5304
|
+
}
|
|
5305
|
+
},
|
|
4576
5306
|
{
|
|
4577
5307
|
method: "get",
|
|
4578
5308
|
path: "/api/v1/backlinks/status",
|
|
@@ -5031,7 +5761,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
5031
5761
|
|
|
5032
5762
|
// ../api-routes/src/schedules.ts
|
|
5033
5763
|
import crypto11 from "crypto";
|
|
5034
|
-
import { eq as
|
|
5764
|
+
import { eq as eq14 } from "drizzle-orm";
|
|
5035
5765
|
async function scheduleRoutes(app, opts) {
|
|
5036
5766
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
5037
5767
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -5074,7 +5804,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5074
5804
|
}
|
|
5075
5805
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5076
5806
|
const enabledInt = enabled === false ? 0 : 1;
|
|
5077
|
-
const existing = app.db.select().from(schedules).where(
|
|
5807
|
+
const existing = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
|
|
5078
5808
|
if (existing) {
|
|
5079
5809
|
app.db.update(schedules).set({
|
|
5080
5810
|
cronExpr,
|
|
@@ -5083,7 +5813,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5083
5813
|
providers: JSON.stringify(providers),
|
|
5084
5814
|
enabled: enabledInt,
|
|
5085
5815
|
updatedAt: now
|
|
5086
|
-
}).where(
|
|
5816
|
+
}).where(eq14(schedules.id, existing.id)).run();
|
|
5087
5817
|
} else {
|
|
5088
5818
|
app.db.insert(schedules).values({
|
|
5089
5819
|
id: crypto11.randomUUID(),
|
|
@@ -5105,12 +5835,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
5105
5835
|
diff: { cronExpr, preset, timezone, providers }
|
|
5106
5836
|
});
|
|
5107
5837
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
5108
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5838
|
+
const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
|
|
5109
5839
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
5110
5840
|
});
|
|
5111
5841
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
5112
5842
|
const project = resolveProject(app.db, request.params.name);
|
|
5113
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5843
|
+
const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
|
|
5114
5844
|
if (!schedule) {
|
|
5115
5845
|
throw notFound("Schedule", request.params.name);
|
|
5116
5846
|
}
|
|
@@ -5118,11 +5848,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
5118
5848
|
});
|
|
5119
5849
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
5120
5850
|
const project = resolveProject(app.db, request.params.name);
|
|
5121
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5851
|
+
const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
|
|
5122
5852
|
if (!schedule) {
|
|
5123
5853
|
throw notFound("Schedule", request.params.name);
|
|
5124
5854
|
}
|
|
5125
|
-
app.db.delete(schedules).where(
|
|
5855
|
+
app.db.delete(schedules).where(eq14(schedules.id, schedule.id)).run();
|
|
5126
5856
|
writeAuditLog(app.db, {
|
|
5127
5857
|
projectId: project.id,
|
|
5128
5858
|
actor: "api",
|
|
@@ -5152,7 +5882,7 @@ function formatSchedule(row) {
|
|
|
5152
5882
|
|
|
5153
5883
|
// ../api-routes/src/notifications.ts
|
|
5154
5884
|
import crypto12 from "crypto";
|
|
5155
|
-
import { eq as
|
|
5885
|
+
import { eq as eq15 } from "drizzle-orm";
|
|
5156
5886
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
5157
5887
|
async function notificationRoutes(app) {
|
|
5158
5888
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -5191,22 +5921,22 @@ async function notificationRoutes(app) {
|
|
|
5191
5921
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
5192
5922
|
});
|
|
5193
5923
|
return reply.status(201).send({
|
|
5194
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
5924
|
+
...formatNotification(app.db.select().from(notifications).where(eq15(notifications.id, id)).get()),
|
|
5195
5925
|
webhookSecret
|
|
5196
5926
|
});
|
|
5197
5927
|
});
|
|
5198
5928
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
5199
5929
|
const project = resolveProject(app.db, request.params.name);
|
|
5200
|
-
const rows = app.db.select().from(notifications).where(
|
|
5930
|
+
const rows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
|
|
5201
5931
|
return reply.send(rows.map(formatNotification));
|
|
5202
5932
|
});
|
|
5203
5933
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
5204
5934
|
const project = resolveProject(app.db, request.params.name);
|
|
5205
|
-
const notification = app.db.select().from(notifications).where(
|
|
5935
|
+
const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
|
|
5206
5936
|
if (!notification || notification.projectId !== project.id) {
|
|
5207
5937
|
throw notFound("Notification", request.params.id);
|
|
5208
5938
|
}
|
|
5209
|
-
app.db.delete(notifications).where(
|
|
5939
|
+
app.db.delete(notifications).where(eq15(notifications.id, notification.id)).run();
|
|
5210
5940
|
writeAuditLog(app.db, {
|
|
5211
5941
|
projectId: project.id,
|
|
5212
5942
|
actor: "api",
|
|
@@ -5218,7 +5948,7 @@ async function notificationRoutes(app) {
|
|
|
5218
5948
|
});
|
|
5219
5949
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
5220
5950
|
const project = resolveProject(app.db, request.params.name);
|
|
5221
|
-
const notification = app.db.select().from(notifications).where(
|
|
5951
|
+
const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
|
|
5222
5952
|
if (!notification || notification.projectId !== project.id) {
|
|
5223
5953
|
throw notFound("Notification", request.params.id);
|
|
5224
5954
|
}
|
|
@@ -5271,7 +6001,7 @@ function formatNotification(row) {
|
|
|
5271
6001
|
|
|
5272
6002
|
// ../api-routes/src/google.ts
|
|
5273
6003
|
import crypto14 from "crypto";
|
|
5274
|
-
import { eq as
|
|
6004
|
+
import { eq as eq16, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
|
|
5275
6005
|
|
|
5276
6006
|
// ../integration-google/src/constants.ts
|
|
5277
6007
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6375,20 +7105,20 @@ async function googleRoutes(app, opts) {
|
|
|
6375
7105
|
if (opts.onGscSyncRequested) {
|
|
6376
7106
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
6377
7107
|
}
|
|
6378
|
-
const run = app.db.select().from(runs).where(
|
|
7108
|
+
const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
6379
7109
|
return run;
|
|
6380
7110
|
});
|
|
6381
7111
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
6382
7112
|
const project = resolveProject(app.db, request.params.name);
|
|
6383
7113
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
6384
7114
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
6385
|
-
const conditions = [
|
|
6386
|
-
if (startDate) conditions.push(
|
|
6387
|
-
else if (cutoffDate) conditions.push(
|
|
6388
|
-
if (endDate) conditions.push(
|
|
6389
|
-
if (query) conditions.push(
|
|
6390
|
-
if (page) conditions.push(
|
|
6391
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
7115
|
+
const conditions = [eq16(gscSearchData.projectId, project.id)];
|
|
7116
|
+
if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
|
|
7117
|
+
else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
|
|
7118
|
+
if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
|
|
7119
|
+
if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
7120
|
+
if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
7121
|
+
const rows = app.db.select().from(gscSearchData).where(and5(...conditions)).orderBy(desc7(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
6392
7122
|
return rows.map((r) => ({
|
|
6393
7123
|
date: r.date,
|
|
6394
7124
|
query: r.query,
|
|
@@ -6460,9 +7190,9 @@ async function googleRoutes(app, opts) {
|
|
|
6460
7190
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
6461
7191
|
const project = resolveProject(app.db, request.params.name);
|
|
6462
7192
|
const { url, limit } = request.query;
|
|
6463
|
-
const conditions = [
|
|
6464
|
-
if (url) conditions.push(
|
|
6465
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
7193
|
+
const conditions = [eq16(gscUrlInspections.projectId, project.id)];
|
|
7194
|
+
if (url) conditions.push(eq16(gscUrlInspections.url, url));
|
|
7195
|
+
const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
6466
7196
|
return rows.map((r) => ({
|
|
6467
7197
|
id: r.id,
|
|
6468
7198
|
url: r.url,
|
|
@@ -6481,7 +7211,7 @@ async function googleRoutes(app, opts) {
|
|
|
6481
7211
|
});
|
|
6482
7212
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
6483
7213
|
const project = resolveProject(app.db, request.params.name);
|
|
6484
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7214
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
6485
7215
|
const byUrl = /* @__PURE__ */ new Map();
|
|
6486
7216
|
for (const row of allInspections) {
|
|
6487
7217
|
const existing = byUrl.get(row.url);
|
|
@@ -6509,7 +7239,7 @@ async function googleRoutes(app, opts) {
|
|
|
6509
7239
|
});
|
|
6510
7240
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
6511
7241
|
const project = resolveProject(app.db, request.params.name);
|
|
6512
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7242
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
6513
7243
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
6514
7244
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6515
7245
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -6606,7 +7336,7 @@ async function googleRoutes(app, opts) {
|
|
|
6606
7336
|
const project = resolveProject(app.db, request.params.name);
|
|
6607
7337
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
6608
7338
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
6609
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
7339
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
|
|
6610
7340
|
return rows.map((r) => ({
|
|
6611
7341
|
date: r.date,
|
|
6612
7342
|
indexed: r.indexed,
|
|
@@ -6666,7 +7396,7 @@ async function googleRoutes(app, opts) {
|
|
|
6666
7396
|
if (opts.onInspectSitemapRequested) {
|
|
6667
7397
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
6668
7398
|
}
|
|
6669
|
-
const run = app.db.select().from(runs).where(
|
|
7399
|
+
const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
6670
7400
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
6671
7401
|
});
|
|
6672
7402
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -6693,7 +7423,7 @@ async function googleRoutes(app, opts) {
|
|
|
6693
7423
|
if (opts.onInspectSitemapRequested) {
|
|
6694
7424
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
6695
7425
|
}
|
|
6696
|
-
const run = app.db.select().from(runs).where(
|
|
7426
|
+
const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
6697
7427
|
return run;
|
|
6698
7428
|
});
|
|
6699
7429
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -6740,7 +7470,7 @@ async function googleRoutes(app, opts) {
|
|
|
6740
7470
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
6741
7471
|
let urlsToNotify = request.body?.urls ?? [];
|
|
6742
7472
|
if (request.body?.allUnindexed) {
|
|
6743
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7473
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
6744
7474
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6745
7475
|
for (const row of allInspections) {
|
|
6746
7476
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6811,7 +7541,7 @@ async function googleRoutes(app, opts) {
|
|
|
6811
7541
|
|
|
6812
7542
|
// ../api-routes/src/bing.ts
|
|
6813
7543
|
import crypto15 from "crypto";
|
|
6814
|
-
import { eq as
|
|
7544
|
+
import { eq as eq17, and as and6, desc as desc8 } from "drizzle-orm";
|
|
6815
7545
|
|
|
6816
7546
|
// ../integration-bing/src/constants.ts
|
|
6817
7547
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7124,7 +7854,7 @@ async function bingRoutes(app, opts) {
|
|
|
7124
7854
|
const store = requireConnectionStore();
|
|
7125
7855
|
const project = resolveProject(app.db, request.params.name);
|
|
7126
7856
|
requireConnection(store, project.canonicalDomain);
|
|
7127
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
7857
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
|
|
7128
7858
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7129
7859
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
7130
7860
|
for (const row of allInspections) {
|
|
@@ -7213,7 +7943,7 @@ async function bingRoutes(app, opts) {
|
|
|
7213
7943
|
const project = resolveProject(app.db, request.params.name);
|
|
7214
7944
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7215
7945
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7216
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
7946
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
|
|
7217
7947
|
return rows.map((r) => ({
|
|
7218
7948
|
date: r.date,
|
|
7219
7949
|
indexed: r.indexed,
|
|
@@ -7225,8 +7955,8 @@ async function bingRoutes(app, opts) {
|
|
|
7225
7955
|
requireConnectionStore();
|
|
7226
7956
|
const project = resolveProject(app.db, request.params.name);
|
|
7227
7957
|
const { url, limit } = request.query;
|
|
7228
|
-
const whereClause = url ?
|
|
7229
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
7958
|
+
const whereClause = url ? and6(eq17(bingUrlInspections.projectId, project.id), eq17(bingUrlInspections.url, url)) : eq17(bingUrlInspections.projectId, project.id);
|
|
7959
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc8(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
7230
7960
|
return filtered.map((r) => ({
|
|
7231
7961
|
id: r.id,
|
|
7232
7962
|
url: r.url,
|
|
@@ -7315,7 +8045,7 @@ async function bingRoutes(app, opts) {
|
|
|
7315
8045
|
anchorCount: result.AnchorCount ?? null,
|
|
7316
8046
|
discoveryDate
|
|
7317
8047
|
}).run();
|
|
7318
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8048
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
|
|
7319
8049
|
return {
|
|
7320
8050
|
id,
|
|
7321
8051
|
url,
|
|
@@ -7331,7 +8061,7 @@ async function bingRoutes(app, opts) {
|
|
|
7331
8061
|
} catch (e) {
|
|
7332
8062
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7333
8063
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
7334
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
8064
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
|
|
7335
8065
|
throw e;
|
|
7336
8066
|
}
|
|
7337
8067
|
});
|
|
@@ -7358,7 +8088,7 @@ async function bingRoutes(app, opts) {
|
|
|
7358
8088
|
} else {
|
|
7359
8089
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
7360
8090
|
}
|
|
7361
|
-
const run = app.db.select().from(runs).where(
|
|
8091
|
+
const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
|
|
7362
8092
|
return run;
|
|
7363
8093
|
});
|
|
7364
8094
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -7370,7 +8100,7 @@ async function bingRoutes(app, opts) {
|
|
|
7370
8100
|
}
|
|
7371
8101
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
7372
8102
|
if (request.body?.allUnindexed) {
|
|
7373
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8103
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
|
|
7374
8104
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7375
8105
|
for (const row of allInspections) {
|
|
7376
8106
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7457,14 +8187,14 @@ async function bingRoutes(app, opts) {
|
|
|
7457
8187
|
import fs from "fs";
|
|
7458
8188
|
import path from "path";
|
|
7459
8189
|
import os from "os";
|
|
7460
|
-
import { eq as
|
|
8190
|
+
import { eq as eq18, and as and7 } from "drizzle-orm";
|
|
7461
8191
|
function getScreenshotDir() {
|
|
7462
8192
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
7463
8193
|
}
|
|
7464
8194
|
async function cdpRoutes(app, opts) {
|
|
7465
8195
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
7466
8196
|
const { snapshotId } = request.params;
|
|
7467
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
8197
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq18(querySnapshots.id, snapshotId)).get();
|
|
7468
8198
|
if (!snapshot?.screenshotPath) {
|
|
7469
8199
|
const err = notFound("Screenshot", snapshotId);
|
|
7470
8200
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7530,7 +8260,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7530
8260
|
async (request, reply) => {
|
|
7531
8261
|
const project = resolveProject(app.db, request.params.name);
|
|
7532
8262
|
const { runId } = request.params;
|
|
7533
|
-
const run = app.db.select().from(runs).where(
|
|
8263
|
+
const run = app.db.select().from(runs).where(and7(eq18(runs.id, runId), eq18(runs.projectId, project.id))).get();
|
|
7534
8264
|
if (!run) {
|
|
7535
8265
|
const err = notFound("Run", runId);
|
|
7536
8266
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7543,8 +8273,8 @@ async function cdpRoutes(app, opts) {
|
|
|
7543
8273
|
citedDomains: querySnapshots.citedDomains,
|
|
7544
8274
|
screenshotPath: querySnapshots.screenshotPath,
|
|
7545
8275
|
rawResponse: querySnapshots.rawResponse
|
|
7546
|
-
}).from(querySnapshots).where(
|
|
7547
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
8276
|
+
}).from(querySnapshots).where(eq18(querySnapshots.runId, runId)).all();
|
|
8277
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq18(keywords.projectId, project.id)).all();
|
|
7548
8278
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
7549
8279
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
7550
8280
|
for (const snap of snapshots) {
|
|
@@ -7627,7 +8357,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7627
8357
|
|
|
7628
8358
|
// ../api-routes/src/ga.ts
|
|
7629
8359
|
import crypto16 from "crypto";
|
|
7630
|
-
import { eq as
|
|
8360
|
+
import { eq as eq19, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
|
|
7631
8361
|
function gaLog(level, action, ctx) {
|
|
7632
8362
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
7633
8363
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -7784,10 +8514,10 @@ async function ga4Routes(app, opts) {
|
|
|
7784
8514
|
if (!saConn && !oauthConn) {
|
|
7785
8515
|
throw notFound("GA4 connection", project.name);
|
|
7786
8516
|
}
|
|
7787
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
7788
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
7789
|
-
app.db.delete(gaAiReferrals).where(
|
|
7790
|
-
app.db.delete(gaSocialReferrals).where(
|
|
8517
|
+
app.db.delete(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8518
|
+
app.db.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
|
|
8519
|
+
app.db.delete(gaAiReferrals).where(eq19(gaAiReferrals.projectId, project.id)).run();
|
|
8520
|
+
app.db.delete(gaSocialReferrals).where(eq19(gaSocialReferrals.projectId, project.id)).run();
|
|
7791
8521
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
7792
8522
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
7793
8523
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -7808,7 +8538,7 @@ async function ga4Routes(app, opts) {
|
|
|
7808
8538
|
if (!connected) {
|
|
7809
8539
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
7810
8540
|
}
|
|
7811
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8541
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
7812
8542
|
return {
|
|
7813
8543
|
connected: true,
|
|
7814
8544
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -7867,10 +8597,10 @@ async function ga4Routes(app, opts) {
|
|
|
7867
8597
|
app.db.transaction((tx) => {
|
|
7868
8598
|
if (syncTraffic) {
|
|
7869
8599
|
tx.delete(gaTrafficSnapshots).where(
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
8600
|
+
and8(
|
|
8601
|
+
eq19(gaTrafficSnapshots.projectId, project.id),
|
|
8602
|
+
sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8603
|
+
sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
7874
8604
|
)
|
|
7875
8605
|
).run();
|
|
7876
8606
|
for (const row of rows) {
|
|
@@ -7889,10 +8619,10 @@ async function ga4Routes(app, opts) {
|
|
|
7889
8619
|
}
|
|
7890
8620
|
if (syncAi) {
|
|
7891
8621
|
tx.delete(gaAiReferrals).where(
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
8622
|
+
and8(
|
|
8623
|
+
eq19(gaAiReferrals.projectId, project.id),
|
|
8624
|
+
sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8625
|
+
sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
7896
8626
|
)
|
|
7897
8627
|
).run();
|
|
7898
8628
|
for (const row of aiReferrals) {
|
|
@@ -7912,10 +8642,10 @@ async function ga4Routes(app, opts) {
|
|
|
7912
8642
|
}
|
|
7913
8643
|
if (syncSocial) {
|
|
7914
8644
|
tx.delete(gaSocialReferrals).where(
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
8645
|
+
and8(
|
|
8646
|
+
eq19(gaSocialReferrals.projectId, project.id),
|
|
8647
|
+
sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
8648
|
+
sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
7919
8649
|
)
|
|
7920
8650
|
).run();
|
|
7921
8651
|
for (const row of socialReferrals) {
|
|
@@ -7934,7 +8664,7 @@ async function ga4Routes(app, opts) {
|
|
|
7934
8664
|
}
|
|
7935
8665
|
}
|
|
7936
8666
|
if (syncSummary) {
|
|
7937
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8667
|
+
tx.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
|
|
7938
8668
|
tx.insert(gaTrafficSummaries).values({
|
|
7939
8669
|
id: crypto16.randomUUID(),
|
|
7940
8670
|
projectId: project.id,
|
|
@@ -7948,7 +8678,7 @@ async function ga4Routes(app, opts) {
|
|
|
7948
8678
|
}).run();
|
|
7949
8679
|
}
|
|
7950
8680
|
});
|
|
7951
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8681
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
|
|
7952
8682
|
const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
|
|
7953
8683
|
gaLog("info", "sync.complete", {
|
|
7954
8684
|
projectId: project.id,
|
|
@@ -7972,7 +8702,7 @@ async function ga4Routes(app, opts) {
|
|
|
7972
8702
|
} catch (e) {
|
|
7973
8703
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7974
8704
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
7975
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
8705
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
|
|
7976
8706
|
throw e;
|
|
7977
8707
|
}
|
|
7978
8708
|
});
|
|
@@ -7983,48 +8713,48 @@ async function ga4Routes(app, opts) {
|
|
|
7983
8713
|
const window = parseWindow(request.query.window);
|
|
7984
8714
|
const cutoff = windowCutoff(window);
|
|
7985
8715
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
7986
|
-
const snapshotConditions = [
|
|
7987
|
-
if (cutoffDate) snapshotConditions.push(
|
|
7988
|
-
const aiConditions = [
|
|
7989
|
-
if (cutoffDate) aiConditions.push(
|
|
7990
|
-
const socialConditions = [
|
|
7991
|
-
if (cutoffDate) socialConditions.push(
|
|
8716
|
+
const snapshotConditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
|
|
8717
|
+
if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8718
|
+
const aiConditions = [eq19(gaAiReferrals.projectId, project.id)];
|
|
8719
|
+
if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8720
|
+
const socialConditions = [eq19(gaSocialReferrals.projectId, project.id)];
|
|
8721
|
+
if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
7992
8722
|
const summaryRow = cutoffDate ? app.db.select({
|
|
7993
|
-
totalSessions:
|
|
7994
|
-
totalOrganicSessions:
|
|
7995
|
-
totalUsers:
|
|
7996
|
-
}).from(gaTrafficSnapshots).where(
|
|
8723
|
+
totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
8724
|
+
totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
8725
|
+
totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
8726
|
+
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get() : app.db.select({
|
|
7997
8727
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
7998
8728
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
7999
8729
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8000
|
-
}).from(gaTrafficSummaries).where(
|
|
8730
|
+
}).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
|
|
8001
8731
|
const summaryMeta = app.db.select({
|
|
8002
8732
|
periodStart: gaTrafficSummaries.periodStart,
|
|
8003
8733
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
8004
|
-
}).from(gaTrafficSummaries).where(
|
|
8734
|
+
}).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
|
|
8005
8735
|
const rows = app.db.select({
|
|
8006
8736
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8007
|
-
sessions:
|
|
8008
|
-
organicSessions:
|
|
8009
|
-
users:
|
|
8010
|
-
}).from(gaTrafficSnapshots).where(
|
|
8737
|
+
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8738
|
+
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8739
|
+
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
8740
|
+
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8011
8741
|
const aiReferrals = app.db.select({
|
|
8012
8742
|
source: gaAiReferrals.source,
|
|
8013
8743
|
medium: gaAiReferrals.medium,
|
|
8014
8744
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8015
|
-
sessions:
|
|
8016
|
-
users:
|
|
8017
|
-
}).from(gaAiReferrals).where(
|
|
8745
|
+
sessions: sql5`SUM(${gaAiReferrals.sessions})`,
|
|
8746
|
+
users: sql5`SUM(${gaAiReferrals.users})`
|
|
8747
|
+
}).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
8018
8748
|
const aiDeduped = app.db.select({
|
|
8019
|
-
sessions:
|
|
8020
|
-
users:
|
|
8749
|
+
sessions: sql5`SUM(max_sessions)`,
|
|
8750
|
+
users: sql5`SUM(max_users)`
|
|
8021
8751
|
}).from(
|
|
8022
|
-
|
|
8752
|
+
sql5`(
|
|
8023
8753
|
SELECT date, source, medium,
|
|
8024
8754
|
MAX(sessions) AS max_sessions,
|
|
8025
8755
|
MAX(users) AS max_users
|
|
8026
8756
|
FROM ga_ai_referrals
|
|
8027
|
-
WHERE project_id = ${project.id}${cutoffDate ?
|
|
8757
|
+
WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
|
|
8028
8758
|
GROUP BY date, source, medium
|
|
8029
8759
|
)`
|
|
8030
8760
|
).get();
|
|
@@ -8032,14 +8762,14 @@ async function ga4Routes(app, opts) {
|
|
|
8032
8762
|
source: gaSocialReferrals.source,
|
|
8033
8763
|
medium: gaSocialReferrals.medium,
|
|
8034
8764
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8035
|
-
sessions:
|
|
8036
|
-
users:
|
|
8037
|
-
}).from(gaSocialReferrals).where(
|
|
8765
|
+
sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
|
|
8766
|
+
users: sql5`SUM(${gaSocialReferrals.users})`
|
|
8767
|
+
}).from(gaSocialReferrals).where(and8(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
8038
8768
|
const socialTotals = app.db.select({
|
|
8039
|
-
sessions:
|
|
8040
|
-
users:
|
|
8041
|
-
}).from(gaSocialReferrals).where(
|
|
8042
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8769
|
+
sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
|
|
8770
|
+
users: sql5`SUM(${gaSocialReferrals.users})`
|
|
8771
|
+
}).from(gaSocialReferrals).where(and8(...socialConditions)).get();
|
|
8772
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8043
8773
|
const total = summaryRow?.totalSessions ?? 0;
|
|
8044
8774
|
return {
|
|
8045
8775
|
totalSessions: total,
|
|
@@ -8086,8 +8816,8 @@ async function ga4Routes(app, opts) {
|
|
|
8086
8816
|
const project = resolveProject(app.db, request.params.name);
|
|
8087
8817
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8088
8818
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8089
|
-
const conditions = [
|
|
8090
|
-
if (cutoffDate) conditions.push(
|
|
8819
|
+
const conditions = [eq19(gaAiReferrals.projectId, project.id)];
|
|
8820
|
+
if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8091
8821
|
const rows = app.db.select({
|
|
8092
8822
|
date: gaAiReferrals.date,
|
|
8093
8823
|
source: gaAiReferrals.source,
|
|
@@ -8095,15 +8825,15 @@ async function ga4Routes(app, opts) {
|
|
|
8095
8825
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8096
8826
|
sessions: gaAiReferrals.sessions,
|
|
8097
8827
|
users: gaAiReferrals.users
|
|
8098
|
-
}).from(gaAiReferrals).where(
|
|
8828
|
+
}).from(gaAiReferrals).where(and8(...conditions)).orderBy(gaAiReferrals.date).all();
|
|
8099
8829
|
return rows;
|
|
8100
8830
|
});
|
|
8101
8831
|
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
8102
8832
|
const project = resolveProject(app.db, request.params.name);
|
|
8103
8833
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8104
8834
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8105
|
-
const conditions = [
|
|
8106
|
-
if (cutoffDate) conditions.push(
|
|
8835
|
+
const conditions = [eq19(gaSocialReferrals.projectId, project.id)];
|
|
8836
|
+
if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
8107
8837
|
const rows = app.db.select({
|
|
8108
8838
|
date: gaSocialReferrals.date,
|
|
8109
8839
|
source: gaSocialReferrals.source,
|
|
@@ -8111,7 +8841,7 @@ async function ga4Routes(app, opts) {
|
|
|
8111
8841
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8112
8842
|
sessions: gaSocialReferrals.sessions,
|
|
8113
8843
|
users: gaSocialReferrals.users
|
|
8114
|
-
}).from(gaSocialReferrals).where(
|
|
8844
|
+
}).from(gaSocialReferrals).where(and8(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
8115
8845
|
return rows;
|
|
8116
8846
|
});
|
|
8117
8847
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -8124,10 +8854,10 @@ async function ga4Routes(app, opts) {
|
|
|
8124
8854
|
d.setDate(d.getDate() - n);
|
|
8125
8855
|
return fmt(d);
|
|
8126
8856
|
};
|
|
8127
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8857
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
|
|
8858
|
+
eq19(gaSocialReferrals.projectId, project.id),
|
|
8859
|
+
sql5`${gaSocialReferrals.date} >= ${from}`,
|
|
8860
|
+
sql5`${gaSocialReferrals.date} < ${to}`
|
|
8131
8861
|
)).get();
|
|
8132
8862
|
const current7d = sumSocial(daysAgo2(7), fmt(today));
|
|
8133
8863
|
const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
|
|
@@ -8136,19 +8866,19 @@ async function ga4Routes(app, opts) {
|
|
|
8136
8866
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8137
8867
|
const sourceCurrent = app.db.select({
|
|
8138
8868
|
source: gaSocialReferrals.source,
|
|
8139
|
-
sessions:
|
|
8140
|
-
}).from(gaSocialReferrals).where(
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8869
|
+
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
8870
|
+
}).from(gaSocialReferrals).where(and8(
|
|
8871
|
+
eq19(gaSocialReferrals.projectId, project.id),
|
|
8872
|
+
sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
8873
|
+
sql5`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
8144
8874
|
)).groupBy(gaSocialReferrals.source).all();
|
|
8145
8875
|
const sourcePrev = app.db.select({
|
|
8146
8876
|
source: gaSocialReferrals.source,
|
|
8147
|
-
sessions:
|
|
8148
|
-
}).from(gaSocialReferrals).where(
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8877
|
+
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
8878
|
+
}).from(gaSocialReferrals).where(and8(
|
|
8879
|
+
eq19(gaSocialReferrals.projectId, project.id),
|
|
8880
|
+
sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
8881
|
+
sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
8152
8882
|
)).groupBy(gaSocialReferrals.source).all();
|
|
8153
8883
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
8154
8884
|
let biggestMover = null;
|
|
@@ -8187,15 +8917,15 @@ async function ga4Routes(app, opts) {
|
|
|
8187
8917
|
return fmt(d);
|
|
8188
8918
|
};
|
|
8189
8919
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8190
|
-
const sumTotal = (from, to) => app.db.select({ sessions:
|
|
8191
|
-
const sumOrganic = (from, to) => app.db.select({ sessions:
|
|
8192
|
-
const sumAi = (from, to) => app.db.select({ sessions:
|
|
8920
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8921
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8922
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
|
|
8193
8923
|
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8194
8924
|
FROM ga_ai_referrals
|
|
8195
8925
|
WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
|
|
8196
8926
|
GROUP BY date, source, medium
|
|
8197
8927
|
)`).get();
|
|
8198
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
8928
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
|
|
8199
8929
|
const todayStr = fmt(today);
|
|
8200
8930
|
const buildTrend = (sum) => {
|
|
8201
8931
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -8204,18 +8934,18 @@ async function ga4Routes(app, opts) {
|
|
|
8204
8934
|
const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
|
|
8205
8935
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
8206
8936
|
};
|
|
8207
|
-
const aiSourceCurrent = app.db.select({ source:
|
|
8937
|
+
const aiSourceCurrent = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
|
|
8208
8938
|
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8209
8939
|
FROM ga_ai_referrals
|
|
8210
8940
|
WHERE project_id = ${project.id} AND date >= ${daysAgo2(7)} AND date < ${todayStr}
|
|
8211
8941
|
GROUP BY date, source, medium
|
|
8212
|
-
)`).groupBy(
|
|
8213
|
-
const aiSourcePrev = app.db.select({ source:
|
|
8942
|
+
)`).groupBy(sql5`source`).all();
|
|
8943
|
+
const aiSourcePrev = app.db.select({ source: sql5`source`, sessions: sql5`COALESCE(SUM(max_sessions), 0)` }).from(sql5`(
|
|
8214
8944
|
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8215
8945
|
FROM ga_ai_referrals
|
|
8216
8946
|
WHERE project_id = ${project.id} AND date >= ${daysAgo2(14)} AND date < ${daysAgo2(7)}
|
|
8217
8947
|
GROUP BY date, source, medium
|
|
8218
|
-
)`).groupBy(
|
|
8948
|
+
)`).groupBy(sql5`source`).all();
|
|
8219
8949
|
const findBiggestMover = (current, prev) => {
|
|
8220
8950
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
8221
8951
|
let mover = null;
|
|
@@ -8230,8 +8960,8 @@ async function ga4Routes(app, opts) {
|
|
|
8230
8960
|
}
|
|
8231
8961
|
return mover;
|
|
8232
8962
|
};
|
|
8233
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
8234
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
8963
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
8964
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
8235
8965
|
return {
|
|
8236
8966
|
total: buildTrend(sumTotal),
|
|
8237
8967
|
organic: buildTrend(sumOrganic),
|
|
@@ -8245,14 +8975,14 @@ async function ga4Routes(app, opts) {
|
|
|
8245
8975
|
const project = resolveProject(app.db, request.params.name);
|
|
8246
8976
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8247
8977
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8248
|
-
const conditions = [
|
|
8249
|
-
if (cutoffDate) conditions.push(
|
|
8978
|
+
const conditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
|
|
8979
|
+
if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8250
8980
|
const rows = app.db.select({
|
|
8251
8981
|
date: gaTrafficSnapshots.date,
|
|
8252
|
-
sessions:
|
|
8253
|
-
organicSessions:
|
|
8254
|
-
users:
|
|
8255
|
-
}).from(gaTrafficSnapshots).where(
|
|
8982
|
+
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8983
|
+
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8984
|
+
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
8985
|
+
}).from(gaTrafficSnapshots).where(and8(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
8256
8986
|
return rows.map((r) => ({
|
|
8257
8987
|
date: r.date,
|
|
8258
8988
|
sessions: r.sessions ?? 0,
|
|
@@ -8265,10 +8995,10 @@ async function ga4Routes(app, opts) {
|
|
|
8265
8995
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8266
8996
|
const trafficPages = app.db.select({
|
|
8267
8997
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8268
|
-
sessions:
|
|
8269
|
-
organicSessions:
|
|
8270
|
-
users:
|
|
8271
|
-
}).from(gaTrafficSnapshots).where(
|
|
8998
|
+
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8999
|
+
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
9000
|
+
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
9001
|
+
}).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8272
9002
|
return {
|
|
8273
9003
|
pages: trafficPages.map((r) => ({
|
|
8274
9004
|
landingPage: r.landingPage,
|
|
@@ -9905,7 +10635,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
9905
10635
|
|
|
9906
10636
|
// ../api-routes/src/backlinks.ts
|
|
9907
10637
|
import crypto18 from "crypto";
|
|
9908
|
-
import { and as
|
|
10638
|
+
import { and as and9, asc as asc2, desc as desc10, eq as eq20, sql as sql6 } from "drizzle-orm";
|
|
9909
10639
|
|
|
9910
10640
|
// ../integration-commoncrawl/src/constants.ts
|
|
9911
10641
|
import os2 from "os";
|
|
@@ -10135,7 +10865,7 @@ async function queryBacklinks(opts) {
|
|
|
10135
10865
|
const reversed = opts.targets.map(reverseDomain);
|
|
10136
10866
|
const targetList = reversed.map(quote).join(", ");
|
|
10137
10867
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
10138
|
-
const
|
|
10868
|
+
const sql11 = `
|
|
10139
10869
|
WITH vertices AS (
|
|
10140
10870
|
SELECT * FROM read_csv(
|
|
10141
10871
|
${quote(opts.vertexPath)},
|
|
@@ -10171,7 +10901,7 @@ async function queryBacklinks(opts) {
|
|
|
10171
10901
|
const conn = await instance.connect();
|
|
10172
10902
|
let rows;
|
|
10173
10903
|
try {
|
|
10174
|
-
const reader = await conn.runAndReadAll(
|
|
10904
|
+
const reader = await conn.runAndReadAll(sql11);
|
|
10175
10905
|
rows = reader.getRowObjects();
|
|
10176
10906
|
} finally {
|
|
10177
10907
|
conn.disconnectSync?.();
|
|
@@ -10303,8 +11033,8 @@ function mapRunRow(row) {
|
|
|
10303
11033
|
};
|
|
10304
11034
|
}
|
|
10305
11035
|
function latestSummaryForProject(db, projectId, release) {
|
|
10306
|
-
const condition = release ?
|
|
10307
|
-
return db.select().from(backlinkSummaries).where(condition).orderBy(
|
|
11036
|
+
const condition = release ? and9(eq20(backlinkSummaries.projectId, projectId), eq20(backlinkSummaries.release, release)) : eq20(backlinkSummaries.projectId, projectId);
|
|
11037
|
+
return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
|
|
10308
11038
|
}
|
|
10309
11039
|
async function backlinksRoutes(app, opts) {
|
|
10310
11040
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
@@ -10333,7 +11063,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10333
11063
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
10334
11064
|
);
|
|
10335
11065
|
}
|
|
10336
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
11066
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.release, release)).get();
|
|
10337
11067
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10338
11068
|
if (existing) {
|
|
10339
11069
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -10344,9 +11074,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
10344
11074
|
phaseDetail: null,
|
|
10345
11075
|
error: null,
|
|
10346
11076
|
updatedAt: now
|
|
10347
|
-
}).where(
|
|
11077
|
+
}).where(eq20(ccReleaseSyncs.id, existing.id)).run();
|
|
10348
11078
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
10349
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
11079
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, existing.id)).get();
|
|
10350
11080
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
10351
11081
|
}
|
|
10352
11082
|
const id = crypto18.randomUUID();
|
|
@@ -10358,15 +11088,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10358
11088
|
updatedAt: now
|
|
10359
11089
|
}).run();
|
|
10360
11090
|
opts.onReleaseSyncRequested(id, release);
|
|
10361
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
11091
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, id)).get();
|
|
10362
11092
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
10363
11093
|
});
|
|
10364
11094
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
10365
|
-
const row = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
11095
|
+
const row = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).limit(1).get();
|
|
10366
11096
|
return reply.send(row ? mapSyncRow(row) : null);
|
|
10367
11097
|
});
|
|
10368
11098
|
app.get("/backlinks/syncs", async (_request, reply) => {
|
|
10369
|
-
const rows = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
11099
|
+
const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc10(ccReleaseSyncs.updatedAt)).all();
|
|
10370
11100
|
return reply.send(rows.map(mapSyncRow));
|
|
10371
11101
|
});
|
|
10372
11102
|
app.get("/backlinks/releases", async (_request, reply) => {
|
|
@@ -10409,7 +11139,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10409
11139
|
createdAt: now
|
|
10410
11140
|
}).run();
|
|
10411
11141
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
10412
|
-
const run = app.db.select().from(runs).where(
|
|
11142
|
+
const run = app.db.select().from(runs).where(eq20(runs.id, runId)).get();
|
|
10413
11143
|
return reply.status(201).send(mapRunRow(run));
|
|
10414
11144
|
});
|
|
10415
11145
|
app.get(
|
|
@@ -10430,15 +11160,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10430
11160
|
}
|
|
10431
11161
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
10432
11162
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
10433
|
-
const domainCondition =
|
|
10434
|
-
|
|
10435
|
-
|
|
11163
|
+
const domainCondition = and9(
|
|
11164
|
+
eq20(backlinkDomains.projectId, project.id),
|
|
11165
|
+
eq20(backlinkDomains.release, targetRelease)
|
|
10436
11166
|
);
|
|
10437
|
-
const totalRow = app.db.select({ count:
|
|
11167
|
+
const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
10438
11168
|
const rows = app.db.select({
|
|
10439
11169
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
10440
11170
|
numHosts: backlinkDomains.numHosts
|
|
10441
|
-
}).from(backlinkDomains).where(domainCondition).orderBy(
|
|
11171
|
+
}).from(backlinkDomains).where(domainCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
10442
11172
|
const response = {
|
|
10443
11173
|
summary: summaryRow ? mapSummaryRow(summaryRow) : null,
|
|
10444
11174
|
total: Number(totalRow?.count ?? 0),
|
|
@@ -10450,7 +11180,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10450
11180
|
"/projects/:name/backlinks/history",
|
|
10451
11181
|
async (request, reply) => {
|
|
10452
11182
|
const project = resolveProject(app.db, request.params.name);
|
|
10453
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
11183
|
+
const rows = app.db.select().from(backlinkSummaries).where(eq20(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
|
|
10454
11184
|
const response = rows.map((r) => ({
|
|
10455
11185
|
release: r.release,
|
|
10456
11186
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -10523,6 +11253,8 @@ async function apiRoutes(app, opts) {
|
|
|
10523
11253
|
await api.register(historyRoutes);
|
|
10524
11254
|
await api.register(analyticsRoutes);
|
|
10525
11255
|
await api.register(intelligenceRoutes);
|
|
11256
|
+
await api.register(compositeRoutes);
|
|
11257
|
+
await api.register(contentRoutes);
|
|
10526
11258
|
await api.register(settingsRoutes, {
|
|
10527
11259
|
providerSummary: opts.providerSummary,
|
|
10528
11260
|
providerAdapters: opts.providerAdapters,
|
|
@@ -12668,7 +13400,7 @@ function hasParsedResponseContent4(rawResponse) {
|
|
|
12668
13400
|
return Array.isArray(nestedResponse.choices) && nestedResponse.choices.length > 0 || Array.isArray(nestedResponse.search_results) && nestedResponse.search_results.length > 0 || Array.isArray(nestedResponse.citations) && nestedResponse.citations.length > 0;
|
|
12669
13401
|
}
|
|
12670
13402
|
function reparseStoredResult4(rawResponse) {
|
|
12671
|
-
const groundingSources =
|
|
13403
|
+
const groundingSources = extractGroundingSources2(rawResponse);
|
|
12672
13404
|
return {
|
|
12673
13405
|
provider: "perplexity",
|
|
12674
13406
|
answerText: extractAnswerText3(rawResponse),
|
|
@@ -12699,7 +13431,7 @@ function extractCitations(rawResponse) {
|
|
|
12699
13431
|
}
|
|
12700
13432
|
return [];
|
|
12701
13433
|
}
|
|
12702
|
-
function
|
|
13434
|
+
function extractGroundingSources2(rawResponse) {
|
|
12703
13435
|
const searchResults = extractSearchResults(rawResponse);
|
|
12704
13436
|
if (searchResults.length > 0) {
|
|
12705
13437
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13044,7 +13776,7 @@ import crypto19 from "crypto";
|
|
|
13044
13776
|
import fs7 from "fs";
|
|
13045
13777
|
import path9 from "path";
|
|
13046
13778
|
import os4 from "os";
|
|
13047
|
-
import { and as
|
|
13779
|
+
import { and as and10, eq as eq21, inArray as inArray4, sql as sql7 } from "drizzle-orm";
|
|
13048
13780
|
|
|
13049
13781
|
// src/citation-utils.ts
|
|
13050
13782
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13296,11 +14028,11 @@ var JobRunner = class {
|
|
|
13296
14028
|
this.registry = registry;
|
|
13297
14029
|
}
|
|
13298
14030
|
recoverStaleRuns() {
|
|
13299
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
14031
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
|
|
13300
14032
|
if (stale.length === 0) return;
|
|
13301
14033
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13302
14034
|
for (const run of stale) {
|
|
13303
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
14035
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq21(runs.id, run.id)).run();
|
|
13304
14036
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13305
14037
|
}
|
|
13306
14038
|
}
|
|
@@ -13328,10 +14060,10 @@ var JobRunner = class {
|
|
|
13328
14060
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13329
14061
|
}
|
|
13330
14062
|
if (existingRun.status === "queued") {
|
|
13331
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14063
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq21(runs.id, runId), eq21(runs.status, "queued"))).run();
|
|
13332
14064
|
}
|
|
13333
14065
|
this.throwIfRunCancelled(runId);
|
|
13334
|
-
const project = this.db.select().from(projects).where(
|
|
14066
|
+
const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13335
14067
|
if (!project) {
|
|
13336
14068
|
throw new Error(`Project ${projectId} not found`);
|
|
13337
14069
|
}
|
|
@@ -13351,8 +14083,8 @@ var JobRunner = class {
|
|
|
13351
14083
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13352
14084
|
}
|
|
13353
14085
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
13354
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
13355
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
14086
|
+
projectKeywords = this.db.select().from(keywords).where(eq21(keywords.projectId, projectId)).all();
|
|
14087
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq21(competitors.projectId, projectId)).all();
|
|
13356
14088
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13357
14089
|
const allDomains = effectiveDomains({
|
|
13358
14090
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13368,7 +14100,7 @@ var JobRunner = class {
|
|
|
13368
14100
|
const todayPeriod = getCurrentUsageDay();
|
|
13369
14101
|
for (const p of activeProviders) {
|
|
13370
14102
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13371
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
14103
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq21(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
13372
14104
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13373
14105
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13374
14106
|
throw new Error(
|
|
@@ -13509,12 +14241,12 @@ var JobRunner = class {
|
|
|
13509
14241
|
const someFailed = providerErrors.size > 0;
|
|
13510
14242
|
if (allFailed) {
|
|
13511
14243
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13512
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
14244
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
|
|
13513
14245
|
} else if (someFailed) {
|
|
13514
14246
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13515
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
14247
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
|
|
13516
14248
|
} else {
|
|
13517
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14249
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
13518
14250
|
}
|
|
13519
14251
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13520
14252
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13549,7 +14281,7 @@ var JobRunner = class {
|
|
|
13549
14281
|
status: "failed",
|
|
13550
14282
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13551
14283
|
error: errorMessage
|
|
13552
|
-
}).where(
|
|
14284
|
+
}).where(eq21(runs.id, runId)).run();
|
|
13553
14285
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13554
14286
|
trackEvent("run.completed", {
|
|
13555
14287
|
status: "failed",
|
|
@@ -13578,7 +14310,7 @@ var JobRunner = class {
|
|
|
13578
14310
|
updatedAt: now
|
|
13579
14311
|
}).onConflictDoUpdate({
|
|
13580
14312
|
target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
|
|
13581
|
-
set: { count:
|
|
14313
|
+
set: { count: sql7`${usageCounters.count} + ${count}`, updatedAt: now }
|
|
13582
14314
|
}).run();
|
|
13583
14315
|
}
|
|
13584
14316
|
flushProviderUsage(projectId, providerDispatchCounts) {
|
|
@@ -13592,7 +14324,7 @@ var JobRunner = class {
|
|
|
13592
14324
|
status: runs.status,
|
|
13593
14325
|
finishedAt: runs.finishedAt,
|
|
13594
14326
|
error: runs.error
|
|
13595
|
-
}).from(runs).where(
|
|
14327
|
+
}).from(runs).where(eq21(runs.id, runId)).get();
|
|
13596
14328
|
}
|
|
13597
14329
|
isRunCancelled(runId) {
|
|
13598
14330
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13608,7 +14340,7 @@ var JobRunner = class {
|
|
|
13608
14340
|
this.db.update(runs).set({
|
|
13609
14341
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13610
14342
|
error: currentRun.error ?? "Cancelled by user"
|
|
13611
|
-
}).where(
|
|
14343
|
+
}).where(eq21(runs.id, runId)).run();
|
|
13612
14344
|
}
|
|
13613
14345
|
trackEvent("run.completed", {
|
|
13614
14346
|
status: "cancelled",
|
|
@@ -13631,7 +14363,7 @@ function getCurrentUsageDay() {
|
|
|
13631
14363
|
|
|
13632
14364
|
// src/gsc-sync.ts
|
|
13633
14365
|
import crypto20 from "crypto";
|
|
13634
|
-
import { eq as
|
|
14366
|
+
import { eq as eq22, and as and11, sql as sql8 } from "drizzle-orm";
|
|
13635
14367
|
var log2 = createLogger("GscSync");
|
|
13636
14368
|
function formatDate2(d) {
|
|
13637
14369
|
return d.toISOString().split("T")[0];
|
|
@@ -13643,13 +14375,13 @@ function daysAgo(n) {
|
|
|
13643
14375
|
}
|
|
13644
14376
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13645
14377
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13646
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14378
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
|
|
13647
14379
|
try {
|
|
13648
14380
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13649
14381
|
if (!googleClientId || !googleClientSecret) {
|
|
13650
14382
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13651
14383
|
}
|
|
13652
|
-
const project = db.select().from(projects).where(
|
|
14384
|
+
const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
13653
14385
|
if (!project) {
|
|
13654
14386
|
throw new Error(`Project not found: ${projectId}`);
|
|
13655
14387
|
}
|
|
@@ -13683,10 +14415,10 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13683
14415
|
});
|
|
13684
14416
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13685
14417
|
db.delete(gscSearchData).where(
|
|
13686
|
-
|
|
13687
|
-
|
|
13688
|
-
|
|
13689
|
-
|
|
14418
|
+
and11(
|
|
14419
|
+
eq22(gscSearchData.projectId, projectId),
|
|
14420
|
+
sql8`${gscSearchData.date} >= ${startDate}`,
|
|
14421
|
+
sql8`${gscSearchData.date} <= ${endDate}`
|
|
13690
14422
|
)
|
|
13691
14423
|
).run();
|
|
13692
14424
|
const batchSize = 500;
|
|
@@ -13751,7 +14483,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13751
14483
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13752
14484
|
}
|
|
13753
14485
|
}
|
|
13754
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14486
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
|
|
13755
14487
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13756
14488
|
for (const row of allInspections) {
|
|
13757
14489
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13772,7 +14504,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13772
14504
|
}
|
|
13773
14505
|
}
|
|
13774
14506
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13775
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14507
|
+
db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13776
14508
|
db.insert(gscCoverageSnapshots).values({
|
|
13777
14509
|
id: crypto20.randomUUID(),
|
|
13778
14510
|
projectId,
|
|
@@ -13783,11 +14515,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13783
14515
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13784
14516
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13785
14517
|
}).run();
|
|
13786
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14518
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
13787
14519
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13788
14520
|
} catch (err) {
|
|
13789
14521
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13790
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14522
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
13791
14523
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13792
14524
|
throw err;
|
|
13793
14525
|
}
|
|
@@ -13795,7 +14527,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13795
14527
|
|
|
13796
14528
|
// src/gsc-inspect-sitemap.ts
|
|
13797
14529
|
import crypto21 from "crypto";
|
|
13798
|
-
import { eq as
|
|
14530
|
+
import { eq as eq23, and as and12 } from "drizzle-orm";
|
|
13799
14531
|
|
|
13800
14532
|
// src/sitemap-parser.ts
|
|
13801
14533
|
var log3 = createLogger("SitemapParser");
|
|
@@ -13916,13 +14648,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
13916
14648
|
var log4 = createLogger("InspectSitemap");
|
|
13917
14649
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13918
14650
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13919
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14651
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
|
|
13920
14652
|
try {
|
|
13921
14653
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13922
14654
|
if (!googleClientId || !googleClientSecret) {
|
|
13923
14655
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13924
14656
|
}
|
|
13925
|
-
const project = db.select().from(projects).where(
|
|
14657
|
+
const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
13926
14658
|
if (!project) {
|
|
13927
14659
|
throw new Error(`Project not found: ${projectId}`);
|
|
13928
14660
|
}
|
|
@@ -13990,7 +14722,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13990
14722
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
13991
14723
|
}
|
|
13992
14724
|
}
|
|
13993
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14725
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
|
|
13994
14726
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13995
14727
|
for (const row of allInspections) {
|
|
13996
14728
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14011,7 +14743,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14011
14743
|
}
|
|
14012
14744
|
}
|
|
14013
14745
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14014
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14746
|
+
db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14015
14747
|
db.insert(gscCoverageSnapshots).values({
|
|
14016
14748
|
id: crypto21.randomUUID(),
|
|
14017
14749
|
projectId,
|
|
@@ -14023,11 +14755,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14023
14755
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14024
14756
|
}).run();
|
|
14025
14757
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14026
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14758
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14027
14759
|
log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14028
14760
|
} catch (err) {
|
|
14029
14761
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14030
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14762
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14031
14763
|
log4.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14032
14764
|
throw err;
|
|
14033
14765
|
}
|
|
@@ -14035,7 +14767,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14035
14767
|
|
|
14036
14768
|
// src/bing-inspect-sitemap.ts
|
|
14037
14769
|
import crypto22 from "crypto";
|
|
14038
|
-
import { eq as
|
|
14770
|
+
import { eq as eq24, desc as desc11 } from "drizzle-orm";
|
|
14039
14771
|
var log5 = createLogger("BingInspectSitemap");
|
|
14040
14772
|
function parseBingDate2(value) {
|
|
14041
14773
|
if (!value) return null;
|
|
@@ -14053,9 +14785,9 @@ function isBlockingIssueType2(issueType) {
|
|
|
14053
14785
|
}
|
|
14054
14786
|
async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
14055
14787
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14056
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
14788
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
|
|
14057
14789
|
try {
|
|
14058
|
-
const project = db.select().from(projects).where(
|
|
14790
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
14059
14791
|
if (!project) {
|
|
14060
14792
|
throw new Error(`Project not found: ${projectId}`);
|
|
14061
14793
|
}
|
|
@@ -14073,7 +14805,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14073
14805
|
if (sitemapUrls.length === 0) {
|
|
14074
14806
|
throw new Error("No URLs found in sitemap");
|
|
14075
14807
|
}
|
|
14076
|
-
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(
|
|
14808
|
+
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).all();
|
|
14077
14809
|
const trackedUrls = new Set(trackedRows.map((r) => r.url));
|
|
14078
14810
|
const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
|
|
14079
14811
|
log5.info("sitemap.diff", {
|
|
@@ -14156,7 +14888,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14156
14888
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14157
14889
|
}
|
|
14158
14890
|
}
|
|
14159
|
-
const allInspections = db.select().from(bingUrlInspections).where(
|
|
14891
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
14160
14892
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14161
14893
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
14162
14894
|
for (const row of allInspections) {
|
|
@@ -14199,7 +14931,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14199
14931
|
}
|
|
14200
14932
|
}).run();
|
|
14201
14933
|
const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
|
|
14202
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14934
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
14203
14935
|
log5.info("inspect.completed", {
|
|
14204
14936
|
runId,
|
|
14205
14937
|
projectId,
|
|
@@ -14213,7 +14945,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14213
14945
|
});
|
|
14214
14946
|
} catch (err) {
|
|
14215
14947
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14216
|
-
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14948
|
+
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
14217
14949
|
log5.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14218
14950
|
throw err;
|
|
14219
14951
|
}
|
|
@@ -14222,7 +14954,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14222
14954
|
// src/commoncrawl-sync.ts
|
|
14223
14955
|
import crypto23 from "crypto";
|
|
14224
14956
|
import path10 from "path";
|
|
14225
|
-
import { and as
|
|
14957
|
+
import { and as and13, eq as eq25, sql as sql9 } from "drizzle-orm";
|
|
14226
14958
|
var log6 = createLogger("CommonCrawlSync");
|
|
14227
14959
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
14228
14960
|
function defaultDeps() {
|
|
@@ -14248,7 +14980,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14248
14980
|
phaseDetail: "downloading vertices + edges",
|
|
14249
14981
|
updatedAt: downloadStartedAt,
|
|
14250
14982
|
error: null
|
|
14251
|
-
}).where(
|
|
14983
|
+
}).where(eq25(ccReleaseSyncs.id, syncId)).run();
|
|
14252
14984
|
const paths = ccReleasePaths(release);
|
|
14253
14985
|
const releaseCacheDir = path10.join(deps.cacheDir, release);
|
|
14254
14986
|
const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
|
|
@@ -14271,7 +15003,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14271
15003
|
vertexSha256: vertex.sha256,
|
|
14272
15004
|
edgesSha256: edges.sha256,
|
|
14273
15005
|
updatedAt: downloadFinishedAt
|
|
14274
|
-
}).where(
|
|
15006
|
+
}).where(eq25(ccReleaseSyncs.id, syncId)).run();
|
|
14275
15007
|
const allProjects = db.select().from(projects).all();
|
|
14276
15008
|
const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
|
|
14277
15009
|
let rows = [];
|
|
@@ -14287,8 +15019,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14287
15019
|
}
|
|
14288
15020
|
const queriedAt = deps.now().toISOString();
|
|
14289
15021
|
db.transaction((tx) => {
|
|
14290
|
-
tx.delete(backlinkDomains).where(
|
|
14291
|
-
tx.delete(backlinkSummaries).where(
|
|
15022
|
+
tx.delete(backlinkDomains).where(eq25(backlinkDomains.releaseSyncId, syncId)).run();
|
|
15023
|
+
tx.delete(backlinkSummaries).where(eq25(backlinkSummaries.releaseSyncId, syncId)).run();
|
|
14292
15024
|
const expanded = [];
|
|
14293
15025
|
for (const r of rows) {
|
|
14294
15026
|
const projectIds = projectsByDomain.get(r.targetDomain);
|
|
@@ -14347,7 +15079,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14347
15079
|
domainsDiscovered: rows.length,
|
|
14348
15080
|
updatedAt: finishedAt,
|
|
14349
15081
|
error: null
|
|
14350
|
-
}).where(
|
|
15082
|
+
}).where(eq25(ccReleaseSyncs.id, syncId)).run();
|
|
14351
15083
|
log6.info("sync.completed", {
|
|
14352
15084
|
syncId,
|
|
14353
15085
|
release,
|
|
@@ -14377,7 +15109,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14377
15109
|
error: errorMsg,
|
|
14378
15110
|
phaseDetail: null,
|
|
14379
15111
|
updatedAt: finishedAt
|
|
14380
|
-
}).where(
|
|
15112
|
+
}).where(eq25(ccReleaseSyncs.id, syncId)).run();
|
|
14381
15113
|
log6.error("sync.failed", { syncId, release, error: errorMsg });
|
|
14382
15114
|
throw err;
|
|
14383
15115
|
}
|
|
@@ -14413,7 +15145,7 @@ function computeSummary(rows) {
|
|
|
14413
15145
|
// src/backlink-extract.ts
|
|
14414
15146
|
import crypto24 from "crypto";
|
|
14415
15147
|
import fs8 from "fs";
|
|
14416
|
-
import { and as
|
|
15148
|
+
import { and as and14, desc as desc12, eq as eq26 } from "drizzle-orm";
|
|
14417
15149
|
var log7 = createLogger("BacklinkExtract");
|
|
14418
15150
|
function defaultDeps2() {
|
|
14419
15151
|
return {
|
|
@@ -14425,13 +15157,13 @@ function defaultDeps2() {
|
|
|
14425
15157
|
async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
14426
15158
|
const deps = { ...defaultDeps2(), ...opts.deps };
|
|
14427
15159
|
const startedAt = deps.now().toISOString();
|
|
14428
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
15160
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
|
|
14429
15161
|
try {
|
|
14430
|
-
const project = db.select().from(projects).where(
|
|
15162
|
+
const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
|
|
14431
15163
|
if (!project) {
|
|
14432
15164
|
throw new Error(`Project not found: ${projectId}`);
|
|
14433
15165
|
}
|
|
14434
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(
|
|
15166
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
14435
15167
|
if (!sync) {
|
|
14436
15168
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
14437
15169
|
}
|
|
@@ -14459,7 +15191,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14459
15191
|
const targetDomain = project.canonicalDomain;
|
|
14460
15192
|
db.transaction((tx) => {
|
|
14461
15193
|
tx.delete(backlinkDomains).where(
|
|
14462
|
-
|
|
15194
|
+
and14(eq26(backlinkDomains.projectId, projectId), eq26(backlinkDomains.release, release))
|
|
14463
15195
|
).run();
|
|
14464
15196
|
if (rows.length > 0) {
|
|
14465
15197
|
const values = rows.map((r) => ({
|
|
@@ -14499,7 +15231,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14499
15231
|
}).run();
|
|
14500
15232
|
});
|
|
14501
15233
|
const finishedAt = deps.now().toISOString();
|
|
14502
|
-
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
15234
|
+
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
|
|
14503
15235
|
log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
|
|
14504
15236
|
} catch (err) {
|
|
14505
15237
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -14508,7 +15240,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14508
15240
|
status: RunStatuses.failed,
|
|
14509
15241
|
error: errorMsg,
|
|
14510
15242
|
finishedAt
|
|
14511
|
-
}).where(
|
|
15243
|
+
}).where(eq26(runs.id, runId)).run();
|
|
14512
15244
|
log7.error("extract.failed", { runId, projectId, error: errorMsg });
|
|
14513
15245
|
throw err;
|
|
14514
15246
|
}
|
|
@@ -14581,7 +15313,7 @@ var ProviderRegistry = class {
|
|
|
14581
15313
|
|
|
14582
15314
|
// src/scheduler.ts
|
|
14583
15315
|
import cron from "node-cron";
|
|
14584
|
-
import { eq as
|
|
15316
|
+
import { eq as eq27 } from "drizzle-orm";
|
|
14585
15317
|
var log8 = createLogger("Scheduler");
|
|
14586
15318
|
var Scheduler = class {
|
|
14587
15319
|
db;
|
|
@@ -14593,7 +15325,7 @@ var Scheduler = class {
|
|
|
14593
15325
|
}
|
|
14594
15326
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14595
15327
|
start() {
|
|
14596
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
15328
|
+
const allSchedules = this.db.select().from(schedules).where(eq27(schedules.enabled, 1)).all();
|
|
14597
15329
|
for (const schedule of allSchedules) {
|
|
14598
15330
|
const missedRunAt = schedule.nextRunAt;
|
|
14599
15331
|
this.registerCronTask(schedule);
|
|
@@ -14618,7 +15350,7 @@ var Scheduler = class {
|
|
|
14618
15350
|
this.stopTask(projectId, existing, "Stopped");
|
|
14619
15351
|
this.tasks.delete(projectId);
|
|
14620
15352
|
}
|
|
14621
|
-
const schedule = this.db.select().from(schedules).where(
|
|
15353
|
+
const schedule = this.db.select().from(schedules).where(eq27(schedules.projectId, projectId)).get();
|
|
14622
15354
|
if (schedule && schedule.enabled === 1) {
|
|
14623
15355
|
this.registerCronTask(schedule);
|
|
14624
15356
|
}
|
|
@@ -14651,14 +15383,14 @@ var Scheduler = class {
|
|
|
14651
15383
|
this.db.update(schedules).set({
|
|
14652
15384
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14653
15385
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14654
|
-
}).where(
|
|
15386
|
+
}).where(eq27(schedules.id, scheduleId)).run();
|
|
14655
15387
|
const label = schedule.preset ?? cronExpr;
|
|
14656
15388
|
log8.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14657
15389
|
}
|
|
14658
15390
|
triggerRun(scheduleId, projectId) {
|
|
14659
15391
|
try {
|
|
14660
15392
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14661
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
15393
|
+
const currentSchedule = this.db.select().from(schedules).where(eq27(schedules.id, scheduleId)).get();
|
|
14662
15394
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14663
15395
|
log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14664
15396
|
this.remove(projectId);
|
|
@@ -14666,7 +15398,7 @@ var Scheduler = class {
|
|
|
14666
15398
|
}
|
|
14667
15399
|
const task = this.tasks.get(projectId);
|
|
14668
15400
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14669
|
-
const project = this.db.select().from(projects).where(
|
|
15401
|
+
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
14670
15402
|
if (!project) {
|
|
14671
15403
|
log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14672
15404
|
this.remove(projectId);
|
|
@@ -14695,7 +15427,7 @@ var Scheduler = class {
|
|
|
14695
15427
|
this.db.update(schedules).set({
|
|
14696
15428
|
nextRunAt,
|
|
14697
15429
|
updatedAt: now
|
|
14698
|
-
}).where(
|
|
15430
|
+
}).where(eq27(schedules.id, currentSchedule.id)).run();
|
|
14699
15431
|
return;
|
|
14700
15432
|
}
|
|
14701
15433
|
const runId = queueResult.runId;
|
|
@@ -14703,7 +15435,7 @@ var Scheduler = class {
|
|
|
14703
15435
|
lastRunAt: now,
|
|
14704
15436
|
nextRunAt,
|
|
14705
15437
|
updatedAt: now
|
|
14706
|
-
}).where(
|
|
15438
|
+
}).where(eq27(schedules.id, currentSchedule.id)).run();
|
|
14707
15439
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14708
15440
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14709
15441
|
log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14715,7 +15447,7 @@ var Scheduler = class {
|
|
|
14715
15447
|
};
|
|
14716
15448
|
|
|
14717
15449
|
// src/notifier.ts
|
|
14718
|
-
import { eq as
|
|
15450
|
+
import { eq as eq28, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
|
|
14719
15451
|
import crypto25 from "crypto";
|
|
14720
15452
|
var log9 = createLogger("Notifier");
|
|
14721
15453
|
var Notifier = class {
|
|
@@ -14728,18 +15460,18 @@ var Notifier = class {
|
|
|
14728
15460
|
/** Called after a run completes (success, partial, or failed). */
|
|
14729
15461
|
async onRunCompleted(runId, projectId) {
|
|
14730
15462
|
log9.info("run.completed", { runId, projectId });
|
|
14731
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15463
|
+
const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14732
15464
|
if (notifs.length === 0) {
|
|
14733
15465
|
log9.info("notifications.none-enabled", { projectId });
|
|
14734
15466
|
return;
|
|
14735
15467
|
}
|
|
14736
15468
|
log9.info("notifications.found", { projectId, count: notifs.length });
|
|
14737
|
-
const run = this.db.select().from(runs).where(
|
|
15469
|
+
const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
14738
15470
|
if (!run) {
|
|
14739
15471
|
log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14740
15472
|
return;
|
|
14741
15473
|
}
|
|
14742
|
-
const project = this.db.select().from(projects).where(
|
|
15474
|
+
const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
|
|
14743
15475
|
if (!project) {
|
|
14744
15476
|
log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14745
15477
|
return;
|
|
@@ -14786,11 +15518,11 @@ var Notifier = class {
|
|
|
14786
15518
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14787
15519
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14788
15520
|
if (insightEvents.length === 0) return;
|
|
14789
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15521
|
+
const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14790
15522
|
if (notifs.length === 0) return;
|
|
14791
|
-
const run = this.db.select().from(runs).where(
|
|
15523
|
+
const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
14792
15524
|
if (!run) return;
|
|
14793
|
-
const project = this.db.select().from(projects).where(
|
|
15525
|
+
const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
|
|
14794
15526
|
if (!project) return;
|
|
14795
15527
|
for (const notif of notifs) {
|
|
14796
15528
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -14821,11 +15553,11 @@ var Notifier = class {
|
|
|
14821
15553
|
}
|
|
14822
15554
|
computeTransitions(runId, projectId) {
|
|
14823
15555
|
const recentRuns = this.db.select().from(runs).where(
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
15556
|
+
and15(
|
|
15557
|
+
eq28(runs.projectId, projectId),
|
|
15558
|
+
or3(eq28(runs.status, "completed"), eq28(runs.status, "partial"))
|
|
14827
15559
|
)
|
|
14828
|
-
).orderBy(
|
|
15560
|
+
).orderBy(desc13(runs.createdAt)).limit(2).all();
|
|
14829
15561
|
if (recentRuns.length < 2) return [];
|
|
14830
15562
|
const currentRunId = recentRuns[0].id;
|
|
14831
15563
|
const previousRunId = recentRuns[1].id;
|
|
@@ -14835,12 +15567,12 @@ var Notifier = class {
|
|
|
14835
15567
|
keyword: keywords.keyword,
|
|
14836
15568
|
provider: querySnapshots.provider,
|
|
14837
15569
|
citationState: querySnapshots.citationState
|
|
14838
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
15570
|
+
}).from(querySnapshots).leftJoin(keywords, eq28(querySnapshots.keywordId, keywords.id)).where(eq28(querySnapshots.runId, currentRunId)).all();
|
|
14839
15571
|
const previousSnapshots = this.db.select({
|
|
14840
15572
|
keywordId: querySnapshots.keywordId,
|
|
14841
15573
|
provider: querySnapshots.provider,
|
|
14842
15574
|
citationState: querySnapshots.citationState
|
|
14843
|
-
}).from(querySnapshots).where(
|
|
15575
|
+
}).from(querySnapshots).where(eq28(querySnapshots.runId, previousRunId)).all();
|
|
14844
15576
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14845
15577
|
for (const s of previousSnapshots) {
|
|
14846
15578
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14957,7 +15689,7 @@ var RunCoordinator = class {
|
|
|
14957
15689
|
|
|
14958
15690
|
// src/agent/session-registry.ts
|
|
14959
15691
|
import crypto27 from "crypto";
|
|
14960
|
-
import { eq as
|
|
15692
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
14961
15693
|
|
|
14962
15694
|
// src/agent/session.ts
|
|
14963
15695
|
import fs11 from "fs";
|
|
@@ -15177,7 +15909,7 @@ import { Type as Type2 } from "@sinclair/typebox";
|
|
|
15177
15909
|
|
|
15178
15910
|
// src/agent/memory-store.ts
|
|
15179
15911
|
import crypto26 from "crypto";
|
|
15180
|
-
import { and as
|
|
15912
|
+
import { and as and16, desc as desc14, eq as eq29, like as like2, sql as sql10 } from "drizzle-orm";
|
|
15181
15913
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
15182
15914
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
15183
15915
|
function rowToDto(row) {
|
|
@@ -15191,7 +15923,7 @@ function rowToDto(row) {
|
|
|
15191
15923
|
};
|
|
15192
15924
|
}
|
|
15193
15925
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
15194
|
-
const query = db.select().from(agentMemory).where(
|
|
15926
|
+
const query = db.select().from(agentMemory).where(eq29(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
|
|
15195
15927
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
15196
15928
|
return rows.map(rowToDto);
|
|
15197
15929
|
}
|
|
@@ -15222,12 +15954,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
15222
15954
|
updatedAt: now
|
|
15223
15955
|
}
|
|
15224
15956
|
}).run();
|
|
15225
|
-
const row = db.select().from(agentMemory).where(
|
|
15957
|
+
const row = db.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, args.key))).get();
|
|
15226
15958
|
if (!row) throw new Error("memory upsert produced no row");
|
|
15227
15959
|
return rowToDto(row);
|
|
15228
15960
|
}
|
|
15229
15961
|
function deleteMemoryEntry(db, projectId, key) {
|
|
15230
|
-
const result = db.delete(agentMemory).where(
|
|
15962
|
+
const result = db.delete(agentMemory).where(and16(eq29(agentMemory.projectId, projectId), eq29(agentMemory.key, key))).run();
|
|
15231
15963
|
const changes = result.changes ?? 0;
|
|
15232
15964
|
return changes > 0;
|
|
15233
15965
|
}
|
|
@@ -15256,16 +15988,16 @@ function writeCompactionNote(db, args) {
|
|
|
15256
15988
|
}).run();
|
|
15257
15989
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
15258
15990
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15991
|
+
and16(
|
|
15992
|
+
eq29(agentMemory.projectId, args.projectId),
|
|
15993
|
+
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
15262
15994
|
)
|
|
15263
|
-
).orderBy(
|
|
15995
|
+
).orderBy(desc14(agentMemory.updatedAt)).all();
|
|
15264
15996
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
15265
15997
|
if (stale.length > 0) {
|
|
15266
|
-
tx.delete(agentMemory).where(
|
|
15998
|
+
tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
|
|
15267
15999
|
}
|
|
15268
|
-
const row = tx.select().from(agentMemory).where(
|
|
16000
|
+
const row = tx.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, key))).get();
|
|
15269
16001
|
if (row) inserted = rowToDto(row);
|
|
15270
16002
|
});
|
|
15271
16003
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -15395,6 +16127,59 @@ function buildListCompetitorsTool(ctx) {
|
|
|
15395
16127
|
}
|
|
15396
16128
|
};
|
|
15397
16129
|
}
|
|
16130
|
+
var ContentTargetsSchema = Type2.Object({
|
|
16131
|
+
limit: Type2.Optional(
|
|
16132
|
+
Type2.Number({
|
|
16133
|
+
description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
|
|
16134
|
+
})
|
|
16135
|
+
),
|
|
16136
|
+
includeInProgress: Type2.Optional(
|
|
16137
|
+
Type2.Boolean({
|
|
16138
|
+
description: "Include rows that already have an in-flight tracked action. Default false."
|
|
16139
|
+
})
|
|
16140
|
+
)
|
|
16141
|
+
});
|
|
16142
|
+
function buildGetContentTargetsTool(ctx) {
|
|
16143
|
+
return {
|
|
16144
|
+
name: "get_content_targets",
|
|
16145
|
+
label: "Get content targets",
|
|
16146
|
+
description: "Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence}`. Use this to recommend which post the user should write/refresh next.",
|
|
16147
|
+
parameters: ContentTargetsSchema,
|
|
16148
|
+
execute: async (_toolCallId, params) => {
|
|
16149
|
+
const response = await ctx.client.getContentTargets(ctx.projectName, {
|
|
16150
|
+
limit: params.limit,
|
|
16151
|
+
includeInProgress: params.includeInProgress === true
|
|
16152
|
+
});
|
|
16153
|
+
return textResult2(response);
|
|
16154
|
+
}
|
|
16155
|
+
};
|
|
16156
|
+
}
|
|
16157
|
+
var ContentSourcesSchema = Type2.Object({});
|
|
16158
|
+
function buildGetContentSourcesTool(ctx) {
|
|
16159
|
+
return {
|
|
16160
|
+
name: "get_grounding_sources",
|
|
16161
|
+
label: "Get grounding sources",
|
|
16162
|
+
description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors), with citation count and providers. Read this to understand what specific competitor URL is winning a query.",
|
|
16163
|
+
parameters: ContentSourcesSchema,
|
|
16164
|
+
execute: async () => {
|
|
16165
|
+
const response = await ctx.client.getContentSources(ctx.projectName);
|
|
16166
|
+
return textResult2(response);
|
|
16167
|
+
}
|
|
16168
|
+
};
|
|
16169
|
+
}
|
|
16170
|
+
var ContentGapsSchema = Type2.Object({});
|
|
16171
|
+
function buildGetContentGapsTool(ctx) {
|
|
16172
|
+
return {
|
|
16173
|
+
name: "get_content_gaps",
|
|
16174
|
+
label: "Get content gaps",
|
|
16175
|
+
description: 'Queries where competitors are cited but our domain is not. Ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use `get_content_targets` for action-typed recommendations on the same data.',
|
|
16176
|
+
parameters: ContentGapsSchema,
|
|
16177
|
+
execute: async () => {
|
|
16178
|
+
const response = await ctx.client.getContentGaps(ctx.projectName);
|
|
16179
|
+
return textResult2(response);
|
|
16180
|
+
}
|
|
16181
|
+
};
|
|
16182
|
+
}
|
|
15398
16183
|
var RunDetailSchema = Type2.Object({
|
|
15399
16184
|
runId: Type2.String({
|
|
15400
16185
|
description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
|
|
@@ -15472,7 +16257,10 @@ function buildReadTools(ctx) {
|
|
|
15472
16257
|
buildListCompetitorsTool(ctx),
|
|
15473
16258
|
buildGetRunTool(ctx),
|
|
15474
16259
|
buildRecallTool(ctx),
|
|
15475
|
-
buildListBacklinksTool(ctx)
|
|
16260
|
+
buildListBacklinksTool(ctx),
|
|
16261
|
+
buildGetContentTargetsTool(ctx),
|
|
16262
|
+
buildGetContentSourcesTool(ctx),
|
|
16263
|
+
buildGetContentGapsTool(ctx)
|
|
15476
16264
|
];
|
|
15477
16265
|
}
|
|
15478
16266
|
var RunSweepSchema = Type2.Object({
|
|
@@ -15941,7 +16729,7 @@ var SessionRegistry = class {
|
|
|
15941
16729
|
modelProvider: effectiveProvider,
|
|
15942
16730
|
modelId: effectiveModelId,
|
|
15943
16731
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15944
|
-
}).where(
|
|
16732
|
+
}).where(eq30(agentSessions.projectId, projectId)).run();
|
|
15945
16733
|
}
|
|
15946
16734
|
const agent2 = createAeroSession({
|
|
15947
16735
|
projectName,
|
|
@@ -16159,7 +16947,7 @@ ${lines.join("\n")}
|
|
|
16159
16947
|
modelProvider: nextProvider,
|
|
16160
16948
|
modelId: nextModelId,
|
|
16161
16949
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16162
|
-
}).where(
|
|
16950
|
+
}).where(eq30(agentSessions.projectId, projectId)).run();
|
|
16163
16951
|
}
|
|
16164
16952
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
16165
16953
|
save(projectName) {
|
|
@@ -16321,11 +17109,11 @@ ${lines.join("\n")}
|
|
|
16321
17109
|
return id;
|
|
16322
17110
|
}
|
|
16323
17111
|
tryResolveProjectId(projectName) {
|
|
16324
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
17112
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq30(projects.name, projectName)).get();
|
|
16325
17113
|
return row?.id;
|
|
16326
17114
|
}
|
|
16327
17115
|
loadRow(projectId) {
|
|
16328
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
17116
|
+
const row = this.opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, projectId)).get();
|
|
16329
17117
|
return row ?? null;
|
|
16330
17118
|
}
|
|
16331
17119
|
insertRow(params) {
|
|
@@ -16344,14 +17132,14 @@ ${lines.join("\n")}
|
|
|
16344
17132
|
}
|
|
16345
17133
|
updateRow(projectId, patch) {
|
|
16346
17134
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16347
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
17135
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq30(agentSessions.projectId, projectId)).run();
|
|
16348
17136
|
}
|
|
16349
17137
|
};
|
|
16350
17138
|
|
|
16351
17139
|
// src/agent/agent-routes.ts
|
|
16352
|
-
import { eq as
|
|
17140
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
16353
17141
|
function resolveProject2(db, name) {
|
|
16354
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
17142
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq31(projects.name, name)).get();
|
|
16355
17143
|
if (!row) throw notFound("project", name);
|
|
16356
17144
|
return row;
|
|
16357
17145
|
}
|
|
@@ -16360,7 +17148,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16360
17148
|
"/projects/:name/agent/transcript",
|
|
16361
17149
|
async (request) => {
|
|
16362
17150
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16363
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
17151
|
+
const row = opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, project.id)).get();
|
|
16364
17152
|
if (!row) {
|
|
16365
17153
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
16366
17154
|
}
|
|
@@ -16384,7 +17172,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16384
17172
|
async (request) => {
|
|
16385
17173
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16386
17174
|
opts.sessionRegistry.reset(project.name);
|
|
16387
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
17175
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(agentSessions.projectId, project.id)).run();
|
|
16388
17176
|
return { status: "reset" };
|
|
16389
17177
|
}
|
|
16390
17178
|
);
|
|
@@ -16688,7 +17476,7 @@ var SnapshotService = class {
|
|
|
16688
17476
|
}
|
|
16689
17477
|
async createReport(input) {
|
|
16690
17478
|
const companyName = input.companyName.trim();
|
|
16691
|
-
const domain =
|
|
17479
|
+
const domain = normalizeDomain2(input.domain);
|
|
16692
17480
|
const manualPhrases = normalizeStringList(input.phrases ?? []);
|
|
16693
17481
|
const manualCompetitors = normalizeStringList(input.competitors ?? []);
|
|
16694
17482
|
const providers = this.registry.getAll();
|
|
@@ -17128,7 +17916,7 @@ function extractCompetitorsFromResponse(ctx) {
|
|
|
17128
17916
|
const targetDomain = extractHostname2(ctx.targetDomain);
|
|
17129
17917
|
for (const hint of ctx.manualCompetitors) {
|
|
17130
17918
|
if (isDomainLike(hint)) {
|
|
17131
|
-
const normalizedHint =
|
|
17919
|
+
const normalizedHint = normalizeDomain2(hint);
|
|
17132
17920
|
if (domainMatches2(normalizedHint, targetDomain)) continue;
|
|
17133
17921
|
if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
|
|
17134
17922
|
competitors2.add(normalizedHint);
|
|
@@ -17187,7 +17975,7 @@ function uniqueStrings2(values) {
|
|
|
17187
17975
|
values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
|
|
17188
17976
|
)];
|
|
17189
17977
|
}
|
|
17190
|
-
function
|
|
17978
|
+
function normalizeDomain2(value) {
|
|
17191
17979
|
const trimmed = value.trim();
|
|
17192
17980
|
if (!trimmed) return trimmed;
|
|
17193
17981
|
try {
|
|
@@ -17198,15 +17986,15 @@ function normalizeDomain(value) {
|
|
|
17198
17986
|
}
|
|
17199
17987
|
}
|
|
17200
17988
|
function extractHostname2(value) {
|
|
17201
|
-
return
|
|
17989
|
+
return normalizeDomain2(value);
|
|
17202
17990
|
}
|
|
17203
17991
|
function domainMatches2(candidate, target) {
|
|
17204
|
-
const normalizedCandidate =
|
|
17205
|
-
const normalizedTarget =
|
|
17992
|
+
const normalizedCandidate = normalizeDomain2(candidate);
|
|
17993
|
+
const normalizedTarget = normalizeDomain2(target);
|
|
17206
17994
|
return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
|
|
17207
17995
|
}
|
|
17208
17996
|
function isDomainLike(value) {
|
|
17209
|
-
const normalized =
|
|
17997
|
+
const normalized = normalizeDomain2(value);
|
|
17210
17998
|
return normalized.includes(".") && !normalized.includes(" ");
|
|
17211
17999
|
}
|
|
17212
18000
|
function clipText(value, length) {
|
|
@@ -17406,7 +18194,7 @@ async function createServer(opts) {
|
|
|
17406
18194
|
intelligenceService,
|
|
17407
18195
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
17408
18196
|
async ({ runId, projectId, insightCount, criticalOrHigh }) => {
|
|
17409
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
18197
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq32(projects.id, projectId)).get();
|
|
17410
18198
|
if (!project) return;
|
|
17411
18199
|
sessionRegistry.queueFollowUp(project.name, {
|
|
17412
18200
|
role: "user",
|
|
@@ -17546,7 +18334,7 @@ async function createServer(opts) {
|
|
|
17546
18334
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
17547
18335
|
if (opts.config.apiKey) {
|
|
17548
18336
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
17549
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
18337
|
+
const existing = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, keyHash)).get();
|
|
17550
18338
|
if (!existing) {
|
|
17551
18339
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
17552
18340
|
opts.db.insert(apiKeys).values({
|
|
@@ -17598,7 +18386,7 @@ async function createServer(opts) {
|
|
|
17598
18386
|
};
|
|
17599
18387
|
const getDefaultApiKey = () => {
|
|
17600
18388
|
if (!opts.config.apiKey) return void 0;
|
|
17601
|
-
return opts.db.select().from(apiKeys).where(
|
|
18389
|
+
return opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
17602
18390
|
};
|
|
17603
18391
|
const createPasswordSession = (reply) => {
|
|
17604
18392
|
const key = getDefaultApiKey();
|
|
@@ -17655,12 +18443,12 @@ async function createServer(opts) {
|
|
|
17655
18443
|
return reply.send({ authenticated: true });
|
|
17656
18444
|
}
|
|
17657
18445
|
if (apiKey) {
|
|
17658
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18446
|
+
const key = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
17659
18447
|
if (!key || key.revokedAt) {
|
|
17660
18448
|
const err2 = authInvalid();
|
|
17661
18449
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
17662
18450
|
}
|
|
17663
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18451
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(apiKeys.id, key.id)).run();
|
|
17664
18452
|
const sessionId = createSession(key.id);
|
|
17665
18453
|
reply.header("set-cookie", serializeSessionCookie({
|
|
17666
18454
|
name: SESSION_COOKIE_NAME,
|