@ainyc/canonry 2.10.1 → 2.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/assets/assets/{index-PhLDQh1e.js → index-VW6VB3cO.js} +99 -99
- package/assets/index.html +1 -1
- package/dist/{chunk-SZSWQG3J.js → chunk-FAP76VXF.js} +643 -207
- package/dist/{chunk-PYHANJ3B.js → chunk-UM6RDSRJ.js} +327 -0
- package/dist/{chunk-KWQCQMPY.js → chunk-Z3BWDCBJ.js} +126 -0
- package/dist/cli.js +136 -4
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-2ZABHNR4.js → intelligence-service-54F3NGPM.js} +1 -1
- package/dist/mcp.js +1 -1
- 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-Z3BWDCBJ.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 eq31 } from "drizzle-orm";
|
|
171
177
|
import Fastify from "fastify";
|
|
172
178
|
|
|
173
179
|
// ../api-routes/src/auth.ts
|
|
@@ -2347,6 +2353,337 @@ async function intelligenceRoutes(app) {
|
|
|
2347
2353
|
});
|
|
2348
2354
|
}
|
|
2349
2355
|
|
|
2356
|
+
// ../api-routes/src/content-data.ts
|
|
2357
|
+
import { and as and3, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
|
|
2358
|
+
var RECENT_RUNS_WINDOW = 5;
|
|
2359
|
+
function loadOrchestratorInput(db, project) {
|
|
2360
|
+
const projectId = project.id;
|
|
2361
|
+
const ownDomain = normalizeDomain(project.canonicalDomain);
|
|
2362
|
+
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
2363
|
+
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
|
|
2364
|
+
const trackedKeywords = listKeywords(db, projectId);
|
|
2365
|
+
const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
|
|
2366
|
+
const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
|
|
2367
|
+
const competitorSet = new Set(trackedCompetitors);
|
|
2368
|
+
const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
|
|
2369
|
+
const latestRunId = recentRunIds[0] ?? "";
|
|
2370
|
+
const latestRunTimestamp = latestRunId ? lookupRunTimestamp(db, latestRunId) : "";
|
|
2371
|
+
const candidateQueries = buildCandidateQueries({
|
|
2372
|
+
db,
|
|
2373
|
+
projectId,
|
|
2374
|
+
candidateQueryStrings,
|
|
2375
|
+
recentRunIds,
|
|
2376
|
+
latestRunId,
|
|
2377
|
+
ourDomains,
|
|
2378
|
+
competitorSet
|
|
2379
|
+
});
|
|
2380
|
+
const inventory = buildInventory({
|
|
2381
|
+
gscPages: listGscPagesForProject(db, projectId),
|
|
2382
|
+
ga4LandingPages: listGa4LandingPagesForProject(db, projectId),
|
|
2383
|
+
sitemapUrls: [],
|
|
2384
|
+
wpPosts: []
|
|
2385
|
+
});
|
|
2386
|
+
const gaTrafficByPage = buildGaTrafficByPage(db, projectId);
|
|
2387
|
+
const totalAiReferralSessions = sumAiReferralSessions(db, projectId);
|
|
2388
|
+
return {
|
|
2389
|
+
projectId,
|
|
2390
|
+
ownDomain,
|
|
2391
|
+
competitors: trackedCompetitors,
|
|
2392
|
+
candidateQueries,
|
|
2393
|
+
inventory,
|
|
2394
|
+
wpSchemaAudit: /* @__PURE__ */ new Map(),
|
|
2395
|
+
gaTrafficByPage,
|
|
2396
|
+
totalAiReferralSessions,
|
|
2397
|
+
latestRunId,
|
|
2398
|
+
latestRunTimestamp,
|
|
2399
|
+
inProgressActions: /* @__PURE__ */ new Map()
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
function listKeywords(db, projectId) {
|
|
2403
|
+
const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, projectId)).all();
|
|
2404
|
+
return rows.map((r) => r.text);
|
|
2405
|
+
}
|
|
2406
|
+
function listCompetitorDomains(db, projectId) {
|
|
2407
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, projectId)).all();
|
|
2408
|
+
return rows.map((r) => r.domain);
|
|
2409
|
+
}
|
|
2410
|
+
function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
|
|
2411
|
+
const rows = db.select({ id: runs.id }).from(runs).where(
|
|
2412
|
+
and3(
|
|
2413
|
+
eq12(runs.projectId, projectId),
|
|
2414
|
+
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
2415
|
+
// Queued/running/failed/cancelled runs may have partial or no
|
|
2416
|
+
// snapshots; including them risks pointing latestRunId at a run with
|
|
2417
|
+
// no usable evidence.
|
|
2418
|
+
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2419
|
+
)
|
|
2420
|
+
).orderBy(desc5(runs.createdAt)).limit(limit).all();
|
|
2421
|
+
return rows.map((r) => r.id);
|
|
2422
|
+
}
|
|
2423
|
+
function lookupRunTimestamp(db, runId) {
|
|
2424
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq12(runs.id, runId)).get();
|
|
2425
|
+
return row?.createdAt ?? "";
|
|
2426
|
+
}
|
|
2427
|
+
function listGscPagesForProject(db, projectId) {
|
|
2428
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq12(gscSearchData.projectId, projectId)).all();
|
|
2429
|
+
return rows.map((r) => r.page);
|
|
2430
|
+
}
|
|
2431
|
+
function listGa4LandingPagesForProject(db, projectId) {
|
|
2432
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2433
|
+
return rows.map((r) => r.landingPage);
|
|
2434
|
+
}
|
|
2435
|
+
function buildGaTrafficByPage(db, projectId) {
|
|
2436
|
+
const rows = db.select({
|
|
2437
|
+
landingPage: gaTrafficSnapshots.landingPage,
|
|
2438
|
+
sessions: gaTrafficSnapshots.sessions
|
|
2439
|
+
}).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2440
|
+
const map = /* @__PURE__ */ new Map();
|
|
2441
|
+
for (const row of rows) {
|
|
2442
|
+
const path15 = extractPath(row.landingPage);
|
|
2443
|
+
if (!path15) continue;
|
|
2444
|
+
map.set(path15, (map.get(path15) ?? 0) + (row.sessions ?? 0));
|
|
2445
|
+
}
|
|
2446
|
+
return map;
|
|
2447
|
+
}
|
|
2448
|
+
function sumAiReferralSessions(db, projectId) {
|
|
2449
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq12(gaAiReferrals.projectId, projectId)).all();
|
|
2450
|
+
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
2451
|
+
}
|
|
2452
|
+
function buildCandidateQueries(opts) {
|
|
2453
|
+
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
2454
|
+
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
2455
|
+
}
|
|
2456
|
+
const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, opts.projectId)).all();
|
|
2457
|
+
const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
|
|
2458
|
+
const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
|
|
2459
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
|
|
2460
|
+
const snapshotsByKeyword = /* @__PURE__ */ new Map();
|
|
2461
|
+
for (const row of snapshotRows) {
|
|
2462
|
+
const list = snapshotsByKeyword.get(row.keywordId) ?? [];
|
|
2463
|
+
list.push(row);
|
|
2464
|
+
snapshotsByKeyword.set(row.keywordId, list);
|
|
2465
|
+
}
|
|
2466
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, opts.projectId)).all();
|
|
2467
|
+
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
2468
|
+
return opts.candidateQueryStrings.map((query) => {
|
|
2469
|
+
const keywordId = keywordIdByText.get(query);
|
|
2470
|
+
const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
|
|
2471
|
+
const gsc = gscByQuery.get(query) ?? null;
|
|
2472
|
+
return aggregateCandidate({
|
|
2473
|
+
query,
|
|
2474
|
+
snapshots: snaps,
|
|
2475
|
+
gsc,
|
|
2476
|
+
ourDomains: opts.ourDomains,
|
|
2477
|
+
competitorSet: opts.competitorSet,
|
|
2478
|
+
latestRunId: opts.latestRunId
|
|
2479
|
+
});
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
function aggregateGscByQuery(rows) {
|
|
2483
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
2484
|
+
for (const r of rows) {
|
|
2485
|
+
const existing = byQuery.get(r.query);
|
|
2486
|
+
const candidate = {
|
|
2487
|
+
// GSC stores `page` as a full URL for url-prefix properties; normalize to
|
|
2488
|
+
// a path so it can be joined against `gaTrafficByPage` (which is keyed by
|
|
2489
|
+
// path) and so `ourBestPage.url` / `targetRef` stay consistent regardless
|
|
2490
|
+
// of whether the page is sourced from GSC or from inventory.
|
|
2491
|
+
page: extractPath(r.page),
|
|
2492
|
+
position: Number(r.position) || 0,
|
|
2493
|
+
impressions: r.impressions,
|
|
2494
|
+
clicks: r.clicks,
|
|
2495
|
+
ctr: Number(r.ctr) || 0
|
|
2496
|
+
};
|
|
2497
|
+
if (!existing) {
|
|
2498
|
+
byQuery.set(r.query, candidate);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (candidate.impressions > existing.impressions) {
|
|
2502
|
+
byQuery.set(r.query, candidate);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return byQuery;
|
|
2506
|
+
}
|
|
2507
|
+
function aggregateCandidate(opts) {
|
|
2508
|
+
const totalSnaps = opts.snapshots.length;
|
|
2509
|
+
if (totalSnaps === 0) {
|
|
2510
|
+
return {
|
|
2511
|
+
...emptyCandidate(opts.query),
|
|
2512
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2513
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2514
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2515
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2516
|
+
gscCtr: opts.gsc?.ctr ?? 0
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
const citedCount = opts.snapshots.filter((s) => s.citationState === CitationStates.cited).length;
|
|
2520
|
+
const ourCitedRate = citedCount / totalSnaps;
|
|
2521
|
+
const recentMissRate = 1 - ourCitedRate;
|
|
2522
|
+
const competitorTally = /* @__PURE__ */ new Map();
|
|
2523
|
+
const competitorGroundingTally = /* @__PURE__ */ new Map();
|
|
2524
|
+
const ourGroundingTally = /* @__PURE__ */ new Map();
|
|
2525
|
+
let ourCitedInLatestRun = false;
|
|
2526
|
+
for (const snap of opts.snapshots) {
|
|
2527
|
+
const isLatestRun = snap.runId === opts.latestRunId;
|
|
2528
|
+
const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
|
|
2529
|
+
for (const domain of competitorOverlap) {
|
|
2530
|
+
const normalized = normalizeDomain(domain);
|
|
2531
|
+
if (!opts.competitorSet.has(normalized)) continue;
|
|
2532
|
+
competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
|
|
2533
|
+
}
|
|
2534
|
+
const grounding = extractGroundingSources(snap.rawResponse);
|
|
2535
|
+
for (const g of grounding) {
|
|
2536
|
+
const domain = normalizeDomain(extractHostFromUri(g.uri));
|
|
2537
|
+
if (!domain) continue;
|
|
2538
|
+
if (opts.ourDomains.has(domain)) {
|
|
2539
|
+
if (isLatestRun) ourCitedInLatestRun = true;
|
|
2540
|
+
recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (!opts.competitorSet.has(domain)) continue;
|
|
2544
|
+
recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return {
|
|
2548
|
+
query: opts.query,
|
|
2549
|
+
gscPage: opts.gsc?.page ?? null,
|
|
2550
|
+
gscPosition: opts.gsc ? opts.gsc.position : null,
|
|
2551
|
+
gscImpressions: opts.gsc?.impressions ?? 0,
|
|
2552
|
+
gscClicks: opts.gsc?.clicks ?? 0,
|
|
2553
|
+
gscCtr: opts.gsc?.ctr ?? 0,
|
|
2554
|
+
ourCitedRate,
|
|
2555
|
+
ourCitedInLatestRun,
|
|
2556
|
+
competitorDomains: Array.from(competitorTally.keys()),
|
|
2557
|
+
competitorCitationCount: Array.from(competitorTally.values()).reduce((a, b) => a + b, 0),
|
|
2558
|
+
recentMissRate,
|
|
2559
|
+
ourGroundingUrls: Array.from(ourGroundingTally.values()),
|
|
2560
|
+
competitorGroundingUrls: Array.from(competitorGroundingTally.values()),
|
|
2561
|
+
runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
function recordGroundingHit(tally, g, domain, provider) {
|
|
2565
|
+
const existing = tally.get(g.uri);
|
|
2566
|
+
if (existing) {
|
|
2567
|
+
existing.citationCount += 1;
|
|
2568
|
+
if (provider && !existing.providers.includes(provider)) {
|
|
2569
|
+
existing.providers.push(provider);
|
|
2570
|
+
}
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
tally.set(g.uri, {
|
|
2574
|
+
uri: g.uri,
|
|
2575
|
+
title: g.title,
|
|
2576
|
+
domain,
|
|
2577
|
+
citationCount: 1,
|
|
2578
|
+
providers: provider ? [provider] : []
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
function emptyCandidate(query) {
|
|
2582
|
+
return {
|
|
2583
|
+
query,
|
|
2584
|
+
gscPage: null,
|
|
2585
|
+
gscPosition: null,
|
|
2586
|
+
gscImpressions: 0,
|
|
2587
|
+
gscClicks: 0,
|
|
2588
|
+
gscCtr: 0,
|
|
2589
|
+
ourCitedRate: 0,
|
|
2590
|
+
ourCitedInLatestRun: false,
|
|
2591
|
+
competitorDomains: [],
|
|
2592
|
+
competitorCitationCount: 0,
|
|
2593
|
+
recentMissRate: 0,
|
|
2594
|
+
ourGroundingUrls: [],
|
|
2595
|
+
competitorGroundingUrls: [],
|
|
2596
|
+
runsOfHistory: 0
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
function extractGroundingSources(rawResponse) {
|
|
2600
|
+
if (!rawResponse) return [];
|
|
2601
|
+
try {
|
|
2602
|
+
const parsed = JSON.parse(rawResponse);
|
|
2603
|
+
if (parsed && typeof parsed === "object" && "groundingSources" in parsed) {
|
|
2604
|
+
const grounding = parsed.groundingSources;
|
|
2605
|
+
if (Array.isArray(grounding)) {
|
|
2606
|
+
return grounding.filter(
|
|
2607
|
+
(g) => typeof g === "object" && g !== null && typeof g.uri === "string"
|
|
2608
|
+
).map((g) => ({ uri: g.uri, title: g.title ?? "" }));
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
2613
|
+
return [];
|
|
2614
|
+
}
|
|
2615
|
+
function extractHostFromUri(uri) {
|
|
2616
|
+
try {
|
|
2617
|
+
return new URL(uri).hostname;
|
|
2618
|
+
} catch {
|
|
2619
|
+
return "";
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function normalizeDomain(domain) {
|
|
2623
|
+
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
2624
|
+
}
|
|
2625
|
+
function extractPath(url) {
|
|
2626
|
+
if (!url) return "";
|
|
2627
|
+
const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
|
|
2628
|
+
const path15 = match ? match[1] : url.trim();
|
|
2629
|
+
const stripped = path15.replace(/\/+$/, "");
|
|
2630
|
+
return stripped || "/";
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// ../api-routes/src/content.ts
|
|
2634
|
+
async function contentRoutes(app) {
|
|
2635
|
+
app.get("/projects/:name/content/targets", async (request) => {
|
|
2636
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2637
|
+
const includeInProgress = request.query["include-in-progress"] === "true";
|
|
2638
|
+
const limit = parseLimitParam(request.query.limit);
|
|
2639
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2640
|
+
let rows = buildContentTargetRows(input);
|
|
2641
|
+
if (!includeInProgress) {
|
|
2642
|
+
rows = rows.filter((r) => r.existingAction === null);
|
|
2643
|
+
}
|
|
2644
|
+
if (limit !== void 0) {
|
|
2645
|
+
rows = rows.slice(0, limit);
|
|
2646
|
+
}
|
|
2647
|
+
const response = {
|
|
2648
|
+
targets: rows,
|
|
2649
|
+
contextMetrics: {
|
|
2650
|
+
totalAiReferralSessions: input.totalAiReferralSessions,
|
|
2651
|
+
latestRunId: input.latestRunId,
|
|
2652
|
+
runTimestamp: input.latestRunTimestamp
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
return response;
|
|
2656
|
+
});
|
|
2657
|
+
app.get("/projects/:name/content/sources", async (request) => {
|
|
2658
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2659
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2660
|
+
const rows = buildContentSourceRows(input);
|
|
2661
|
+
const response = {
|
|
2662
|
+
sources: rows,
|
|
2663
|
+
latestRunId: input.latestRunId
|
|
2664
|
+
};
|
|
2665
|
+
return response;
|
|
2666
|
+
});
|
|
2667
|
+
app.get("/projects/:name/content/gaps", async (request) => {
|
|
2668
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2669
|
+
const input = loadOrchestratorInput(app.db, project);
|
|
2670
|
+
const rows = buildContentGapRows(input);
|
|
2671
|
+
const response = {
|
|
2672
|
+
gaps: rows,
|
|
2673
|
+
latestRunId: input.latestRunId
|
|
2674
|
+
};
|
|
2675
|
+
return response;
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
function parseLimitParam(raw) {
|
|
2679
|
+
if (raw === void 0) return void 0;
|
|
2680
|
+
const parsed = Number(raw);
|
|
2681
|
+
if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
|
|
2682
|
+
throw validationError('"limit" must be a non-negative integer');
|
|
2683
|
+
}
|
|
2684
|
+
return parsed;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2350
2687
|
// ../api-routes/src/openapi.ts
|
|
2351
2688
|
var stringSchema = { type: "string" };
|
|
2352
2689
|
var booleanSchema = { type: "boolean" };
|
|
@@ -4573,6 +4910,48 @@ var routeCatalog = [
|
|
|
4573
4910
|
404: { description: "Project not found." }
|
|
4574
4911
|
}
|
|
4575
4912
|
},
|
|
4913
|
+
// Content opportunity engine
|
|
4914
|
+
{
|
|
4915
|
+
method: "get",
|
|
4916
|
+
path: "/api/v1/projects/{name}/content/targets",
|
|
4917
|
+
summary: "Ranked, action-typed content opportunities",
|
|
4918
|
+
description: "Returns the canonical opportunity list. Each row is `{query, action, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence, existingAction?}`. Hides rows with in-progress actions by default; pass `?include-in-progress=true` to include them annotated.",
|
|
4919
|
+
tags: ["content"],
|
|
4920
|
+
parameters: [
|
|
4921
|
+
nameParameter,
|
|
4922
|
+
{ name: "limit", in: "query", description: "Max rows returned.", schema: stringSchema },
|
|
4923
|
+
{ name: "include-in-progress", in: "query", description: "Include rows with in-flight tracked actions.", schema: stringSchema }
|
|
4924
|
+
],
|
|
4925
|
+
responses: {
|
|
4926
|
+
200: { description: "Targets returned." },
|
|
4927
|
+
400: { description: "Invalid limit." },
|
|
4928
|
+
404: { description: "Project not found." }
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
{
|
|
4932
|
+
method: "get",
|
|
4933
|
+
path: "/api/v1/projects/{name}/content/sources",
|
|
4934
|
+
summary: "URL-level competitive grounding-source map per query",
|
|
4935
|
+
description: "Returns one row per blog-shaped query containing the grounding URLs the LLM cited. Distinguishes our domain (isOurDomain) from competitor URLs (isCompetitor). Pure DB read \u2014 canonry surfaces URLs but never fetches them.",
|
|
4936
|
+
tags: ["content"],
|
|
4937
|
+
parameters: [nameParameter],
|
|
4938
|
+
responses: {
|
|
4939
|
+
200: { description: "Sources returned." },
|
|
4940
|
+
404: { description: "Project not found." }
|
|
4941
|
+
}
|
|
4942
|
+
},
|
|
4943
|
+
{
|
|
4944
|
+
method: "get",
|
|
4945
|
+
path: "/api/v1/projects/{name}/content/gaps",
|
|
4946
|
+
summary: "Queries where competitors are cited but you are not",
|
|
4947
|
+
description: "Returns gap rows ranked by miss rate then by competitor count. Excludes queries with no competitor citations and queries where our cited rate is 100%.",
|
|
4948
|
+
tags: ["content"],
|
|
4949
|
+
parameters: [nameParameter],
|
|
4950
|
+
responses: {
|
|
4951
|
+
200: { description: "Gaps returned." },
|
|
4952
|
+
404: { description: "Project not found." }
|
|
4953
|
+
}
|
|
4954
|
+
},
|
|
4576
4955
|
{
|
|
4577
4956
|
method: "get",
|
|
4578
4957
|
path: "/api/v1/backlinks/status",
|
|
@@ -5031,7 +5410,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
5031
5410
|
|
|
5032
5411
|
// ../api-routes/src/schedules.ts
|
|
5033
5412
|
import crypto11 from "crypto";
|
|
5034
|
-
import { eq as
|
|
5413
|
+
import { eq as eq13 } from "drizzle-orm";
|
|
5035
5414
|
async function scheduleRoutes(app, opts) {
|
|
5036
5415
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
5037
5416
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -5074,7 +5453,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5074
5453
|
}
|
|
5075
5454
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5076
5455
|
const enabledInt = enabled === false ? 0 : 1;
|
|
5077
|
-
const existing = app.db.select().from(schedules).where(
|
|
5456
|
+
const existing = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5078
5457
|
if (existing) {
|
|
5079
5458
|
app.db.update(schedules).set({
|
|
5080
5459
|
cronExpr,
|
|
@@ -5083,7 +5462,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5083
5462
|
providers: JSON.stringify(providers),
|
|
5084
5463
|
enabled: enabledInt,
|
|
5085
5464
|
updatedAt: now
|
|
5086
|
-
}).where(
|
|
5465
|
+
}).where(eq13(schedules.id, existing.id)).run();
|
|
5087
5466
|
} else {
|
|
5088
5467
|
app.db.insert(schedules).values({
|
|
5089
5468
|
id: crypto11.randomUUID(),
|
|
@@ -5105,12 +5484,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
5105
5484
|
diff: { cronExpr, preset, timezone, providers }
|
|
5106
5485
|
});
|
|
5107
5486
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
5108
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5487
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5109
5488
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
5110
5489
|
});
|
|
5111
5490
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
5112
5491
|
const project = resolveProject(app.db, request.params.name);
|
|
5113
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5492
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5114
5493
|
if (!schedule) {
|
|
5115
5494
|
throw notFound("Schedule", request.params.name);
|
|
5116
5495
|
}
|
|
@@ -5118,11 +5497,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
5118
5497
|
});
|
|
5119
5498
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
5120
5499
|
const project = resolveProject(app.db, request.params.name);
|
|
5121
|
-
const schedule = app.db.select().from(schedules).where(
|
|
5500
|
+
const schedule = app.db.select().from(schedules).where(eq13(schedules.projectId, project.id)).get();
|
|
5122
5501
|
if (!schedule) {
|
|
5123
5502
|
throw notFound("Schedule", request.params.name);
|
|
5124
5503
|
}
|
|
5125
|
-
app.db.delete(schedules).where(
|
|
5504
|
+
app.db.delete(schedules).where(eq13(schedules.id, schedule.id)).run();
|
|
5126
5505
|
writeAuditLog(app.db, {
|
|
5127
5506
|
projectId: project.id,
|
|
5128
5507
|
actor: "api",
|
|
@@ -5152,7 +5531,7 @@ function formatSchedule(row) {
|
|
|
5152
5531
|
|
|
5153
5532
|
// ../api-routes/src/notifications.ts
|
|
5154
5533
|
import crypto12 from "crypto";
|
|
5155
|
-
import { eq as
|
|
5534
|
+
import { eq as eq14 } from "drizzle-orm";
|
|
5156
5535
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
5157
5536
|
async function notificationRoutes(app) {
|
|
5158
5537
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -5191,22 +5570,22 @@ async function notificationRoutes(app) {
|
|
|
5191
5570
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
5192
5571
|
});
|
|
5193
5572
|
return reply.status(201).send({
|
|
5194
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
5573
|
+
...formatNotification(app.db.select().from(notifications).where(eq14(notifications.id, id)).get()),
|
|
5195
5574
|
webhookSecret
|
|
5196
5575
|
});
|
|
5197
5576
|
});
|
|
5198
5577
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
5199
5578
|
const project = resolveProject(app.db, request.params.name);
|
|
5200
|
-
const rows = app.db.select().from(notifications).where(
|
|
5579
|
+
const rows = app.db.select().from(notifications).where(eq14(notifications.projectId, project.id)).all();
|
|
5201
5580
|
return reply.send(rows.map(formatNotification));
|
|
5202
5581
|
});
|
|
5203
5582
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
5204
5583
|
const project = resolveProject(app.db, request.params.name);
|
|
5205
|
-
const notification = app.db.select().from(notifications).where(
|
|
5584
|
+
const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
|
|
5206
5585
|
if (!notification || notification.projectId !== project.id) {
|
|
5207
5586
|
throw notFound("Notification", request.params.id);
|
|
5208
5587
|
}
|
|
5209
|
-
app.db.delete(notifications).where(
|
|
5588
|
+
app.db.delete(notifications).where(eq14(notifications.id, notification.id)).run();
|
|
5210
5589
|
writeAuditLog(app.db, {
|
|
5211
5590
|
projectId: project.id,
|
|
5212
5591
|
actor: "api",
|
|
@@ -5218,7 +5597,7 @@ async function notificationRoutes(app) {
|
|
|
5218
5597
|
});
|
|
5219
5598
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
5220
5599
|
const project = resolveProject(app.db, request.params.name);
|
|
5221
|
-
const notification = app.db.select().from(notifications).where(
|
|
5600
|
+
const notification = app.db.select().from(notifications).where(eq14(notifications.id, request.params.id)).get();
|
|
5222
5601
|
if (!notification || notification.projectId !== project.id) {
|
|
5223
5602
|
throw notFound("Notification", request.params.id);
|
|
5224
5603
|
}
|
|
@@ -5271,7 +5650,7 @@ function formatNotification(row) {
|
|
|
5271
5650
|
|
|
5272
5651
|
// ../api-routes/src/google.ts
|
|
5273
5652
|
import crypto14 from "crypto";
|
|
5274
|
-
import { eq as
|
|
5653
|
+
import { eq as eq15, and as and4, desc as desc6, sql as sql3 } from "drizzle-orm";
|
|
5275
5654
|
|
|
5276
5655
|
// ../integration-google/src/constants.ts
|
|
5277
5656
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -6375,20 +6754,20 @@ async function googleRoutes(app, opts) {
|
|
|
6375
6754
|
if (opts.onGscSyncRequested) {
|
|
6376
6755
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
6377
6756
|
}
|
|
6378
|
-
const run = app.db.select().from(runs).where(
|
|
6757
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6379
6758
|
return run;
|
|
6380
6759
|
});
|
|
6381
6760
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
6382
6761
|
const project = resolveProject(app.db, request.params.name);
|
|
6383
6762
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
6384
6763
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
6385
|
-
const conditions = [
|
|
6764
|
+
const conditions = [eq15(gscSearchData.projectId, project.id)];
|
|
6386
6765
|
if (startDate) conditions.push(sql3`${gscSearchData.date} >= ${startDate}`);
|
|
6387
6766
|
else if (cutoffDate) conditions.push(sql3`${gscSearchData.date} >= ${cutoffDate}`);
|
|
6388
6767
|
if (endDate) conditions.push(sql3`${gscSearchData.date} <= ${endDate}`);
|
|
6389
6768
|
if (query) conditions.push(sql3`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
6390
6769
|
if (page) conditions.push(sql3`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
6391
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
6770
|
+
const rows = app.db.select().from(gscSearchData).where(and4(...conditions)).orderBy(desc6(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
6392
6771
|
return rows.map((r) => ({
|
|
6393
6772
|
date: r.date,
|
|
6394
6773
|
query: r.query,
|
|
@@ -6460,9 +6839,9 @@ async function googleRoutes(app, opts) {
|
|
|
6460
6839
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
6461
6840
|
const project = resolveProject(app.db, request.params.name);
|
|
6462
6841
|
const { url, limit } = request.query;
|
|
6463
|
-
const conditions = [
|
|
6464
|
-
if (url) conditions.push(
|
|
6465
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
6842
|
+
const conditions = [eq15(gscUrlInspections.projectId, project.id)];
|
|
6843
|
+
if (url) conditions.push(eq15(gscUrlInspections.url, url));
|
|
6844
|
+
const rows = app.db.select().from(gscUrlInspections).where(and4(...conditions)).orderBy(desc6(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
6466
6845
|
return rows.map((r) => ({
|
|
6467
6846
|
id: r.id,
|
|
6468
6847
|
url: r.url,
|
|
@@ -6481,7 +6860,7 @@ async function googleRoutes(app, opts) {
|
|
|
6481
6860
|
});
|
|
6482
6861
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
6483
6862
|
const project = resolveProject(app.db, request.params.name);
|
|
6484
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
6863
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6485
6864
|
const byUrl = /* @__PURE__ */ new Map();
|
|
6486
6865
|
for (const row of allInspections) {
|
|
6487
6866
|
const existing = byUrl.get(row.url);
|
|
@@ -6509,7 +6888,7 @@ async function googleRoutes(app, opts) {
|
|
|
6509
6888
|
});
|
|
6510
6889
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
6511
6890
|
const project = resolveProject(app.db, request.params.name);
|
|
6512
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
6891
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6513
6892
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
6514
6893
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6515
6894
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -6606,7 +6985,7 @@ async function googleRoutes(app, opts) {
|
|
|
6606
6985
|
const project = resolveProject(app.db, request.params.name);
|
|
6607
6986
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
6608
6987
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
6609
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
6988
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, project.id)).orderBy(desc6(gscCoverageSnapshots.date)).limit(limit).all();
|
|
6610
6989
|
return rows.map((r) => ({
|
|
6611
6990
|
date: r.date,
|
|
6612
6991
|
indexed: r.indexed,
|
|
@@ -6666,7 +7045,7 @@ async function googleRoutes(app, opts) {
|
|
|
6666
7045
|
if (opts.onInspectSitemapRequested) {
|
|
6667
7046
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
6668
7047
|
}
|
|
6669
|
-
const run = app.db.select().from(runs).where(
|
|
7048
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6670
7049
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
6671
7050
|
});
|
|
6672
7051
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -6693,7 +7072,7 @@ async function googleRoutes(app, opts) {
|
|
|
6693
7072
|
if (opts.onInspectSitemapRequested) {
|
|
6694
7073
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
6695
7074
|
}
|
|
6696
|
-
const run = app.db.select().from(runs).where(
|
|
7075
|
+
const run = app.db.select().from(runs).where(eq15(runs.id, runId)).get();
|
|
6697
7076
|
return run;
|
|
6698
7077
|
});
|
|
6699
7078
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -6740,7 +7119,7 @@ async function googleRoutes(app, opts) {
|
|
|
6740
7119
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
6741
7120
|
let urlsToNotify = request.body?.urls ?? [];
|
|
6742
7121
|
if (request.body?.allUnindexed) {
|
|
6743
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7122
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, project.id)).orderBy(desc6(gscUrlInspections.inspectedAt)).all();
|
|
6744
7123
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6745
7124
|
for (const row of allInspections) {
|
|
6746
7125
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6811,7 +7190,7 @@ async function googleRoutes(app, opts) {
|
|
|
6811
7190
|
|
|
6812
7191
|
// ../api-routes/src/bing.ts
|
|
6813
7192
|
import crypto15 from "crypto";
|
|
6814
|
-
import { eq as
|
|
7193
|
+
import { eq as eq16, and as and5, desc as desc7 } from "drizzle-orm";
|
|
6815
7194
|
|
|
6816
7195
|
// ../integration-bing/src/constants.ts
|
|
6817
7196
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -7124,7 +7503,7 @@ async function bingRoutes(app, opts) {
|
|
|
7124
7503
|
const store = requireConnectionStore();
|
|
7125
7504
|
const project = resolveProject(app.db, request.params.name);
|
|
7126
7505
|
requireConnection(store, project.canonicalDomain);
|
|
7127
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
7506
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
|
|
7128
7507
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7129
7508
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
7130
7509
|
for (const row of allInspections) {
|
|
@@ -7213,7 +7592,7 @@ async function bingRoutes(app, opts) {
|
|
|
7213
7592
|
const project = resolveProject(app.db, request.params.name);
|
|
7214
7593
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7215
7594
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7216
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
7595
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, project.id)).orderBy(desc7(bingCoverageSnapshots.date)).limit(limit).all();
|
|
7217
7596
|
return rows.map((r) => ({
|
|
7218
7597
|
date: r.date,
|
|
7219
7598
|
indexed: r.indexed,
|
|
@@ -7225,8 +7604,8 @@ async function bingRoutes(app, opts) {
|
|
|
7225
7604
|
requireConnectionStore();
|
|
7226
7605
|
const project = resolveProject(app.db, request.params.name);
|
|
7227
7606
|
const { url, limit } = request.query;
|
|
7228
|
-
const whereClause = url ?
|
|
7229
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
7607
|
+
const whereClause = url ? and5(eq16(bingUrlInspections.projectId, project.id), eq16(bingUrlInspections.url, url)) : eq16(bingUrlInspections.projectId, project.id);
|
|
7608
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc7(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
7230
7609
|
return filtered.map((r) => ({
|
|
7231
7610
|
id: r.id,
|
|
7232
7611
|
url: r.url,
|
|
@@ -7315,7 +7694,7 @@ async function bingRoutes(app, opts) {
|
|
|
7315
7694
|
anchorCount: result.AnchorCount ?? null,
|
|
7316
7695
|
discoveryDate
|
|
7317
7696
|
}).run();
|
|
7318
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
7697
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq16(runs.id, runId)).run();
|
|
7319
7698
|
return {
|
|
7320
7699
|
id,
|
|
7321
7700
|
url,
|
|
@@ -7331,7 +7710,7 @@ async function bingRoutes(app, opts) {
|
|
|
7331
7710
|
} catch (e) {
|
|
7332
7711
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7333
7712
|
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(
|
|
7713
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
|
|
7335
7714
|
throw e;
|
|
7336
7715
|
}
|
|
7337
7716
|
});
|
|
@@ -7358,7 +7737,7 @@ async function bingRoutes(app, opts) {
|
|
|
7358
7737
|
} else {
|
|
7359
7738
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
7360
7739
|
}
|
|
7361
|
-
const run = app.db.select().from(runs).where(
|
|
7740
|
+
const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
|
|
7362
7741
|
return run;
|
|
7363
7742
|
});
|
|
7364
7743
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -7370,7 +7749,7 @@ async function bingRoutes(app, opts) {
|
|
|
7370
7749
|
}
|
|
7371
7750
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
7372
7751
|
if (request.body?.allUnindexed) {
|
|
7373
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
7752
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq16(bingUrlInspections.projectId, project.id)).orderBy(desc7(bingUrlInspections.inspectedAt)).all();
|
|
7374
7753
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7375
7754
|
for (const row of allInspections) {
|
|
7376
7755
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7457,14 +7836,14 @@ async function bingRoutes(app, opts) {
|
|
|
7457
7836
|
import fs from "fs";
|
|
7458
7837
|
import path from "path";
|
|
7459
7838
|
import os from "os";
|
|
7460
|
-
import { eq as
|
|
7839
|
+
import { eq as eq17, and as and6 } from "drizzle-orm";
|
|
7461
7840
|
function getScreenshotDir() {
|
|
7462
7841
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
7463
7842
|
}
|
|
7464
7843
|
async function cdpRoutes(app, opts) {
|
|
7465
7844
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
7466
7845
|
const { snapshotId } = request.params;
|
|
7467
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
7846
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq17(querySnapshots.id, snapshotId)).get();
|
|
7468
7847
|
if (!snapshot?.screenshotPath) {
|
|
7469
7848
|
const err = notFound("Screenshot", snapshotId);
|
|
7470
7849
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7530,7 +7909,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7530
7909
|
async (request, reply) => {
|
|
7531
7910
|
const project = resolveProject(app.db, request.params.name);
|
|
7532
7911
|
const { runId } = request.params;
|
|
7533
|
-
const run = app.db.select().from(runs).where(
|
|
7912
|
+
const run = app.db.select().from(runs).where(and6(eq17(runs.id, runId), eq17(runs.projectId, project.id))).get();
|
|
7534
7913
|
if (!run) {
|
|
7535
7914
|
const err = notFound("Run", runId);
|
|
7536
7915
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -7543,8 +7922,8 @@ async function cdpRoutes(app, opts) {
|
|
|
7543
7922
|
citedDomains: querySnapshots.citedDomains,
|
|
7544
7923
|
screenshotPath: querySnapshots.screenshotPath,
|
|
7545
7924
|
rawResponse: querySnapshots.rawResponse
|
|
7546
|
-
}).from(querySnapshots).where(
|
|
7547
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
7925
|
+
}).from(querySnapshots).where(eq17(querySnapshots.runId, runId)).all();
|
|
7926
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq17(keywords.projectId, project.id)).all();
|
|
7548
7927
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
7549
7928
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
7550
7929
|
for (const snap of snapshots) {
|
|
@@ -7627,7 +8006,7 @@ async function cdpRoutes(app, opts) {
|
|
|
7627
8006
|
|
|
7628
8007
|
// ../api-routes/src/ga.ts
|
|
7629
8008
|
import crypto16 from "crypto";
|
|
7630
|
-
import { eq as
|
|
8009
|
+
import { eq as eq18, desc as desc8, and as and7, sql as sql4 } from "drizzle-orm";
|
|
7631
8010
|
function gaLog(level, action, ctx) {
|
|
7632
8011
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
7633
8012
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -7784,10 +8163,10 @@ async function ga4Routes(app, opts) {
|
|
|
7784
8163
|
if (!saConn && !oauthConn) {
|
|
7785
8164
|
throw notFound("GA4 connection", project.name);
|
|
7786
8165
|
}
|
|
7787
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
7788
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
7789
|
-
app.db.delete(gaAiReferrals).where(
|
|
7790
|
-
app.db.delete(gaSocialReferrals).where(
|
|
8166
|
+
app.db.delete(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8167
|
+
app.db.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
|
|
8168
|
+
app.db.delete(gaAiReferrals).where(eq18(gaAiReferrals.projectId, project.id)).run();
|
|
8169
|
+
app.db.delete(gaSocialReferrals).where(eq18(gaSocialReferrals.projectId, project.id)).run();
|
|
7791
8170
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
7792
8171
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
7793
8172
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -7808,7 +8187,7 @@ async function ga4Routes(app, opts) {
|
|
|
7808
8187
|
if (!connected) {
|
|
7809
8188
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
7810
8189
|
}
|
|
7811
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8190
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
7812
8191
|
return {
|
|
7813
8192
|
connected: true,
|
|
7814
8193
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -7867,8 +8246,8 @@ async function ga4Routes(app, opts) {
|
|
|
7867
8246
|
app.db.transaction((tx) => {
|
|
7868
8247
|
if (syncTraffic) {
|
|
7869
8248
|
tx.delete(gaTrafficSnapshots).where(
|
|
7870
|
-
|
|
7871
|
-
|
|
8249
|
+
and7(
|
|
8250
|
+
eq18(gaTrafficSnapshots.projectId, project.id),
|
|
7872
8251
|
sql4`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
7873
8252
|
sql4`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
7874
8253
|
)
|
|
@@ -7889,8 +8268,8 @@ async function ga4Routes(app, opts) {
|
|
|
7889
8268
|
}
|
|
7890
8269
|
if (syncAi) {
|
|
7891
8270
|
tx.delete(gaAiReferrals).where(
|
|
7892
|
-
|
|
7893
|
-
|
|
8271
|
+
and7(
|
|
8272
|
+
eq18(gaAiReferrals.projectId, project.id),
|
|
7894
8273
|
sql4`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
7895
8274
|
sql4`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
7896
8275
|
)
|
|
@@ -7912,8 +8291,8 @@ async function ga4Routes(app, opts) {
|
|
|
7912
8291
|
}
|
|
7913
8292
|
if (syncSocial) {
|
|
7914
8293
|
tx.delete(gaSocialReferrals).where(
|
|
7915
|
-
|
|
7916
|
-
|
|
8294
|
+
and7(
|
|
8295
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
7917
8296
|
sql4`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
7918
8297
|
sql4`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
7919
8298
|
)
|
|
@@ -7934,7 +8313,7 @@ async function ga4Routes(app, opts) {
|
|
|
7934
8313
|
}
|
|
7935
8314
|
}
|
|
7936
8315
|
if (syncSummary) {
|
|
7937
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8316
|
+
tx.delete(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).run();
|
|
7938
8317
|
tx.insert(gaTrafficSummaries).values({
|
|
7939
8318
|
id: crypto16.randomUUID(),
|
|
7940
8319
|
projectId: project.id,
|
|
@@ -7948,7 +8327,7 @@ async function ga4Routes(app, opts) {
|
|
|
7948
8327
|
}).run();
|
|
7949
8328
|
}
|
|
7950
8329
|
});
|
|
7951
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8330
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
|
|
7952
8331
|
const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
|
|
7953
8332
|
gaLog("info", "sync.complete", {
|
|
7954
8333
|
projectId: project.id,
|
|
@@ -7972,7 +8351,7 @@ async function ga4Routes(app, opts) {
|
|
|
7972
8351
|
} catch (e) {
|
|
7973
8352
|
const msg = e instanceof Error ? e.message : String(e);
|
|
7974
8353
|
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(
|
|
8354
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
7976
8355
|
throw e;
|
|
7977
8356
|
}
|
|
7978
8357
|
});
|
|
@@ -7983,38 +8362,38 @@ async function ga4Routes(app, opts) {
|
|
|
7983
8362
|
const window = parseWindow(request.query.window);
|
|
7984
8363
|
const cutoff = windowCutoff(window);
|
|
7985
8364
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
7986
|
-
const snapshotConditions = [
|
|
8365
|
+
const snapshotConditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
|
|
7987
8366
|
if (cutoffDate) snapshotConditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
7988
|
-
const aiConditions = [
|
|
8367
|
+
const aiConditions = [eq18(gaAiReferrals.projectId, project.id)];
|
|
7989
8368
|
if (cutoffDate) aiConditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
7990
|
-
const socialConditions = [
|
|
8369
|
+
const socialConditions = [eq18(gaSocialReferrals.projectId, project.id)];
|
|
7991
8370
|
if (cutoffDate) socialConditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
7992
8371
|
const summaryRow = cutoffDate ? app.db.select({
|
|
7993
8372
|
totalSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
7994
8373
|
totalOrganicSessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
7995
8374
|
totalUsers: sql4`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
7996
|
-
}).from(gaTrafficSnapshots).where(
|
|
8375
|
+
}).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).get() : app.db.select({
|
|
7997
8376
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
7998
8377
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
7999
8378
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8000
|
-
}).from(gaTrafficSummaries).where(
|
|
8379
|
+
}).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
|
|
8001
8380
|
const summaryMeta = app.db.select({
|
|
8002
8381
|
periodStart: gaTrafficSummaries.periodStart,
|
|
8003
8382
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
8004
|
-
}).from(gaTrafficSummaries).where(
|
|
8383
|
+
}).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).get();
|
|
8005
8384
|
const rows = app.db.select({
|
|
8006
8385
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
8007
8386
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8008
8387
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8009
8388
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8010
|
-
}).from(gaTrafficSnapshots).where(
|
|
8389
|
+
}).from(gaTrafficSnapshots).where(and7(...snapshotConditions)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
8011
8390
|
const aiReferrals = app.db.select({
|
|
8012
8391
|
source: gaAiReferrals.source,
|
|
8013
8392
|
medium: gaAiReferrals.medium,
|
|
8014
8393
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8015
8394
|
sessions: sql4`SUM(${gaAiReferrals.sessions})`,
|
|
8016
8395
|
users: sql4`SUM(${gaAiReferrals.users})`
|
|
8017
|
-
}).from(gaAiReferrals).where(
|
|
8396
|
+
}).from(gaAiReferrals).where(and7(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
|
|
8018
8397
|
const aiDeduped = app.db.select({
|
|
8019
8398
|
sessions: sql4`SUM(max_sessions)`,
|
|
8020
8399
|
users: sql4`SUM(max_users)`
|
|
@@ -8034,12 +8413,12 @@ async function ga4Routes(app, opts) {
|
|
|
8034
8413
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8035
8414
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
|
|
8036
8415
|
users: sql4`SUM(${gaSocialReferrals.users})`
|
|
8037
|
-
}).from(gaSocialReferrals).where(
|
|
8416
|
+
}).from(gaSocialReferrals).where(and7(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql4`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
8038
8417
|
const socialTotals = app.db.select({
|
|
8039
8418
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`,
|
|
8040
8419
|
users: sql4`SUM(${gaSocialReferrals.users})`
|
|
8041
|
-
}).from(gaSocialReferrals).where(
|
|
8042
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8420
|
+
}).from(gaSocialReferrals).where(and7(...socialConditions)).get();
|
|
8421
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq18(gaTrafficSummaries.projectId, project.id)).orderBy(desc8(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8043
8422
|
const total = summaryRow?.totalSessions ?? 0;
|
|
8044
8423
|
return {
|
|
8045
8424
|
totalSessions: total,
|
|
@@ -8086,7 +8465,7 @@ async function ga4Routes(app, opts) {
|
|
|
8086
8465
|
const project = resolveProject(app.db, request.params.name);
|
|
8087
8466
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8088
8467
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8089
|
-
const conditions = [
|
|
8468
|
+
const conditions = [eq18(gaAiReferrals.projectId, project.id)];
|
|
8090
8469
|
if (cutoffDate) conditions.push(sql4`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8091
8470
|
const rows = app.db.select({
|
|
8092
8471
|
date: gaAiReferrals.date,
|
|
@@ -8095,14 +8474,14 @@ async function ga4Routes(app, opts) {
|
|
|
8095
8474
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
8096
8475
|
sessions: gaAiReferrals.sessions,
|
|
8097
8476
|
users: gaAiReferrals.users
|
|
8098
|
-
}).from(gaAiReferrals).where(
|
|
8477
|
+
}).from(gaAiReferrals).where(and7(...conditions)).orderBy(gaAiReferrals.date).all();
|
|
8099
8478
|
return rows;
|
|
8100
8479
|
});
|
|
8101
8480
|
app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
|
|
8102
8481
|
const project = resolveProject(app.db, request.params.name);
|
|
8103
8482
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8104
8483
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8105
|
-
const conditions = [
|
|
8484
|
+
const conditions = [eq18(gaSocialReferrals.projectId, project.id)];
|
|
8106
8485
|
if (cutoffDate) conditions.push(sql4`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
8107
8486
|
const rows = app.db.select({
|
|
8108
8487
|
date: gaSocialReferrals.date,
|
|
@@ -8111,7 +8490,7 @@ async function ga4Routes(app, opts) {
|
|
|
8111
8490
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
8112
8491
|
sessions: gaSocialReferrals.sessions,
|
|
8113
8492
|
users: gaSocialReferrals.users
|
|
8114
|
-
}).from(gaSocialReferrals).where(
|
|
8493
|
+
}).from(gaSocialReferrals).where(and7(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
8115
8494
|
return rows;
|
|
8116
8495
|
});
|
|
8117
8496
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -8124,8 +8503,8 @@ async function ga4Routes(app, opts) {
|
|
|
8124
8503
|
d.setDate(d.getDate() - n);
|
|
8125
8504
|
return fmt(d);
|
|
8126
8505
|
};
|
|
8127
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
8128
|
-
|
|
8506
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(
|
|
8507
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8129
8508
|
sql4`${gaSocialReferrals.date} >= ${from}`,
|
|
8130
8509
|
sql4`${gaSocialReferrals.date} < ${to}`
|
|
8131
8510
|
)).get();
|
|
@@ -8137,16 +8516,16 @@ async function ga4Routes(app, opts) {
|
|
|
8137
8516
|
const sourceCurrent = app.db.select({
|
|
8138
8517
|
source: gaSocialReferrals.source,
|
|
8139
8518
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`
|
|
8140
|
-
}).from(gaSocialReferrals).where(
|
|
8141
|
-
|
|
8519
|
+
}).from(gaSocialReferrals).where(and7(
|
|
8520
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8142
8521
|
sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
8143
8522
|
sql4`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
8144
8523
|
)).groupBy(gaSocialReferrals.source).all();
|
|
8145
8524
|
const sourcePrev = app.db.select({
|
|
8146
8525
|
source: gaSocialReferrals.source,
|
|
8147
8526
|
sessions: sql4`SUM(${gaSocialReferrals.sessions})`
|
|
8148
|
-
}).from(gaSocialReferrals).where(
|
|
8149
|
-
|
|
8527
|
+
}).from(gaSocialReferrals).where(and7(
|
|
8528
|
+
eq18(gaSocialReferrals.projectId, project.id),
|
|
8150
8529
|
sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
8151
8530
|
sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
8152
8531
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -8187,15 +8566,15 @@ async function ga4Routes(app, opts) {
|
|
|
8187
8566
|
return fmt(d);
|
|
8188
8567
|
};
|
|
8189
8568
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
8190
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
8191
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
8569
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8570
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and7(eq18(gaTrafficSnapshots.projectId, project.id), sql4`${gaTrafficSnapshots.date} >= ${from}`, sql4`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
8192
8571
|
const sumAi = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(max_sessions), 0)` }).from(sql4`(
|
|
8193
8572
|
SELECT date, source, medium, MAX(sessions) AS max_sessions
|
|
8194
8573
|
FROM ga_ai_referrals
|
|
8195
8574
|
WHERE project_id = ${project.id} AND date >= ${from} AND date < ${to}
|
|
8196
8575
|
GROUP BY date, source, medium
|
|
8197
8576
|
)`).get();
|
|
8198
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
8577
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql4`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${from}`, sql4`${gaSocialReferrals.date} < ${to}`)).get();
|
|
8199
8578
|
const todayStr = fmt(today);
|
|
8200
8579
|
const buildTrend = (sum) => {
|
|
8201
8580
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -8230,8 +8609,8 @@ async function ga4Routes(app, opts) {
|
|
|
8230
8609
|
}
|
|
8231
8610
|
return mover;
|
|
8232
8611
|
};
|
|
8233
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
8234
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
8612
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql4`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
8613
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql4`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and7(eq18(gaSocialReferrals.projectId, project.id), sql4`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql4`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
8235
8614
|
return {
|
|
8236
8615
|
total: buildTrend(sumTotal),
|
|
8237
8616
|
organic: buildTrend(sumOrganic),
|
|
@@ -8245,14 +8624,14 @@ async function ga4Routes(app, opts) {
|
|
|
8245
8624
|
const project = resolveProject(app.db, request.params.name);
|
|
8246
8625
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8247
8626
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8248
|
-
const conditions = [
|
|
8627
|
+
const conditions = [eq18(gaTrafficSnapshots.projectId, project.id)];
|
|
8249
8628
|
if (cutoffDate) conditions.push(sql4`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8250
8629
|
const rows = app.db.select({
|
|
8251
8630
|
date: gaTrafficSnapshots.date,
|
|
8252
8631
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8253
8632
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8254
8633
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8255
|
-
}).from(gaTrafficSnapshots).where(
|
|
8634
|
+
}).from(gaTrafficSnapshots).where(and7(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
8256
8635
|
return rows.map((r) => ({
|
|
8257
8636
|
date: r.date,
|
|
8258
8637
|
sessions: r.sessions ?? 0,
|
|
@@ -8268,7 +8647,7 @@ async function ga4Routes(app, opts) {
|
|
|
8268
8647
|
sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
|
|
8269
8648
|
organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
8270
8649
|
users: sql4`SUM(${gaTrafficSnapshots.users})`
|
|
8271
|
-
}).from(gaTrafficSnapshots).where(
|
|
8650
|
+
}).from(gaTrafficSnapshots).where(eq18(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.landingPage).orderBy(sql4`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
8272
8651
|
return {
|
|
8273
8652
|
pages: trafficPages.map((r) => ({
|
|
8274
8653
|
landingPage: r.landingPage,
|
|
@@ -9905,7 +10284,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
9905
10284
|
|
|
9906
10285
|
// ../api-routes/src/backlinks.ts
|
|
9907
10286
|
import crypto18 from "crypto";
|
|
9908
|
-
import { and as
|
|
10287
|
+
import { and as and8, asc as asc2, desc as desc9, eq as eq19, sql as sql5 } from "drizzle-orm";
|
|
9909
10288
|
|
|
9910
10289
|
// ../integration-commoncrawl/src/constants.ts
|
|
9911
10290
|
import os2 from "os";
|
|
@@ -10303,8 +10682,8 @@ function mapRunRow(row) {
|
|
|
10303
10682
|
};
|
|
10304
10683
|
}
|
|
10305
10684
|
function latestSummaryForProject(db, projectId, release) {
|
|
10306
|
-
const condition = release ?
|
|
10307
|
-
return db.select().from(backlinkSummaries).where(condition).orderBy(
|
|
10685
|
+
const condition = release ? and8(eq19(backlinkSummaries.projectId, projectId), eq19(backlinkSummaries.release, release)) : eq19(backlinkSummaries.projectId, projectId);
|
|
10686
|
+
return db.select().from(backlinkSummaries).where(condition).orderBy(desc9(backlinkSummaries.queriedAt)).limit(1).get();
|
|
10308
10687
|
}
|
|
10309
10688
|
async function backlinksRoutes(app, opts) {
|
|
10310
10689
|
app.get("/backlinks/status", async (_request, reply) => {
|
|
@@ -10333,7 +10712,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10333
10712
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
10334
10713
|
);
|
|
10335
10714
|
}
|
|
10336
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
10715
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.release, release)).get();
|
|
10337
10716
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10338
10717
|
if (existing) {
|
|
10339
10718
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -10344,9 +10723,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
10344
10723
|
phaseDetail: null,
|
|
10345
10724
|
error: null,
|
|
10346
10725
|
updatedAt: now
|
|
10347
|
-
}).where(
|
|
10726
|
+
}).where(eq19(ccReleaseSyncs.id, existing.id)).run();
|
|
10348
10727
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
10349
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
10728
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, existing.id)).get();
|
|
10350
10729
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
10351
10730
|
}
|
|
10352
10731
|
const id = crypto18.randomUUID();
|
|
@@ -10358,15 +10737,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10358
10737
|
updatedAt: now
|
|
10359
10738
|
}).run();
|
|
10360
10739
|
opts.onReleaseSyncRequested(id, release);
|
|
10361
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
10740
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq19(ccReleaseSyncs.id, id)).get();
|
|
10362
10741
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
10363
10742
|
});
|
|
10364
10743
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
10365
|
-
const row = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
10744
|
+
const row = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).limit(1).get();
|
|
10366
10745
|
return reply.send(row ? mapSyncRow(row) : null);
|
|
10367
10746
|
});
|
|
10368
10747
|
app.get("/backlinks/syncs", async (_request, reply) => {
|
|
10369
|
-
const rows = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
10748
|
+
const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc9(ccReleaseSyncs.updatedAt)).all();
|
|
10370
10749
|
return reply.send(rows.map(mapSyncRow));
|
|
10371
10750
|
});
|
|
10372
10751
|
app.get("/backlinks/releases", async (_request, reply) => {
|
|
@@ -10409,7 +10788,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10409
10788
|
createdAt: now
|
|
10410
10789
|
}).run();
|
|
10411
10790
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
10412
|
-
const run = app.db.select().from(runs).where(
|
|
10791
|
+
const run = app.db.select().from(runs).where(eq19(runs.id, runId)).get();
|
|
10413
10792
|
return reply.status(201).send(mapRunRow(run));
|
|
10414
10793
|
});
|
|
10415
10794
|
app.get(
|
|
@@ -10430,15 +10809,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
10430
10809
|
}
|
|
10431
10810
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
10432
10811
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
10433
|
-
const domainCondition =
|
|
10434
|
-
|
|
10435
|
-
|
|
10812
|
+
const domainCondition = and8(
|
|
10813
|
+
eq19(backlinkDomains.projectId, project.id),
|
|
10814
|
+
eq19(backlinkDomains.release, targetRelease)
|
|
10436
10815
|
);
|
|
10437
10816
|
const totalRow = app.db.select({ count: sql5`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
10438
10817
|
const rows = app.db.select({
|
|
10439
10818
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
10440
10819
|
numHosts: backlinkDomains.numHosts
|
|
10441
|
-
}).from(backlinkDomains).where(domainCondition).orderBy(
|
|
10820
|
+
}).from(backlinkDomains).where(domainCondition).orderBy(desc9(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
10442
10821
|
const response = {
|
|
10443
10822
|
summary: summaryRow ? mapSummaryRow(summaryRow) : null,
|
|
10444
10823
|
total: Number(totalRow?.count ?? 0),
|
|
@@ -10450,7 +10829,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
10450
10829
|
"/projects/:name/backlinks/history",
|
|
10451
10830
|
async (request, reply) => {
|
|
10452
10831
|
const project = resolveProject(app.db, request.params.name);
|
|
10453
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
10832
|
+
const rows = app.db.select().from(backlinkSummaries).where(eq19(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
|
|
10454
10833
|
const response = rows.map((r) => ({
|
|
10455
10834
|
release: r.release,
|
|
10456
10835
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -10523,6 +10902,7 @@ async function apiRoutes(app, opts) {
|
|
|
10523
10902
|
await api.register(historyRoutes);
|
|
10524
10903
|
await api.register(analyticsRoutes);
|
|
10525
10904
|
await api.register(intelligenceRoutes);
|
|
10905
|
+
await api.register(contentRoutes);
|
|
10526
10906
|
await api.register(settingsRoutes, {
|
|
10527
10907
|
providerSummary: opts.providerSummary,
|
|
10528
10908
|
providerAdapters: opts.providerAdapters,
|
|
@@ -12668,7 +13048,7 @@ function hasParsedResponseContent4(rawResponse) {
|
|
|
12668
13048
|
return Array.isArray(nestedResponse.choices) && nestedResponse.choices.length > 0 || Array.isArray(nestedResponse.search_results) && nestedResponse.search_results.length > 0 || Array.isArray(nestedResponse.citations) && nestedResponse.citations.length > 0;
|
|
12669
13049
|
}
|
|
12670
13050
|
function reparseStoredResult4(rawResponse) {
|
|
12671
|
-
const groundingSources =
|
|
13051
|
+
const groundingSources = extractGroundingSources2(rawResponse);
|
|
12672
13052
|
return {
|
|
12673
13053
|
provider: "perplexity",
|
|
12674
13054
|
answerText: extractAnswerText3(rawResponse),
|
|
@@ -12699,7 +13079,7 @@ function extractCitations(rawResponse) {
|
|
|
12699
13079
|
}
|
|
12700
13080
|
return [];
|
|
12701
13081
|
}
|
|
12702
|
-
function
|
|
13082
|
+
function extractGroundingSources2(rawResponse) {
|
|
12703
13083
|
const searchResults = extractSearchResults(rawResponse);
|
|
12704
13084
|
if (searchResults.length > 0) {
|
|
12705
13085
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13044,7 +13424,7 @@ import crypto19 from "crypto";
|
|
|
13044
13424
|
import fs7 from "fs";
|
|
13045
13425
|
import path9 from "path";
|
|
13046
13426
|
import os4 from "os";
|
|
13047
|
-
import { and as
|
|
13427
|
+
import { and as and9, eq as eq20, inArray as inArray4, sql as sql6 } from "drizzle-orm";
|
|
13048
13428
|
|
|
13049
13429
|
// src/citation-utils.ts
|
|
13050
13430
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -13296,11 +13676,11 @@ var JobRunner = class {
|
|
|
13296
13676
|
this.registry = registry;
|
|
13297
13677
|
}
|
|
13298
13678
|
recoverStaleRuns() {
|
|
13299
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
13679
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
|
|
13300
13680
|
if (stale.length === 0) return;
|
|
13301
13681
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13302
13682
|
for (const run of stale) {
|
|
13303
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
13683
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq20(runs.id, run.id)).run();
|
|
13304
13684
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
13305
13685
|
}
|
|
13306
13686
|
}
|
|
@@ -13328,10 +13708,10 @@ var JobRunner = class {
|
|
|
13328
13708
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
13329
13709
|
}
|
|
13330
13710
|
if (existingRun.status === "queued") {
|
|
13331
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
13711
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and9(eq20(runs.id, runId), eq20(runs.status, "queued"))).run();
|
|
13332
13712
|
}
|
|
13333
13713
|
this.throwIfRunCancelled(runId);
|
|
13334
|
-
const project = this.db.select().from(projects).where(
|
|
13714
|
+
const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
|
|
13335
13715
|
if (!project) {
|
|
13336
13716
|
throw new Error(`Project ${projectId} not found`);
|
|
13337
13717
|
}
|
|
@@ -13351,8 +13731,8 @@ var JobRunner = class {
|
|
|
13351
13731
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
13352
13732
|
}
|
|
13353
13733
|
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(
|
|
13734
|
+
projectKeywords = this.db.select().from(keywords).where(eq20(keywords.projectId, projectId)).all();
|
|
13735
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq20(competitors.projectId, projectId)).all();
|
|
13356
13736
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
13357
13737
|
const allDomains = effectiveDomains({
|
|
13358
13738
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -13368,7 +13748,7 @@ var JobRunner = class {
|
|
|
13368
13748
|
const todayPeriod = getCurrentUsageDay();
|
|
13369
13749
|
for (const p of activeProviders) {
|
|
13370
13750
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
13371
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
13751
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq20(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
13372
13752
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
13373
13753
|
if (providerUsage + queriesPerProvider > limit) {
|
|
13374
13754
|
throw new Error(
|
|
@@ -13509,12 +13889,12 @@ var JobRunner = class {
|
|
|
13509
13889
|
const someFailed = providerErrors.size > 0;
|
|
13510
13890
|
if (allFailed) {
|
|
13511
13891
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13512
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13892
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
|
|
13513
13893
|
} else if (someFailed) {
|
|
13514
13894
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
13515
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
13895
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq20(runs.id, runId)).run();
|
|
13516
13896
|
} else {
|
|
13517
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
13897
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
13518
13898
|
}
|
|
13519
13899
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13520
13900
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -13549,7 +13929,7 @@ var JobRunner = class {
|
|
|
13549
13929
|
status: "failed",
|
|
13550
13930
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13551
13931
|
error: errorMessage
|
|
13552
|
-
}).where(
|
|
13932
|
+
}).where(eq20(runs.id, runId)).run();
|
|
13553
13933
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
13554
13934
|
trackEvent("run.completed", {
|
|
13555
13935
|
status: "failed",
|
|
@@ -13592,7 +13972,7 @@ var JobRunner = class {
|
|
|
13592
13972
|
status: runs.status,
|
|
13593
13973
|
finishedAt: runs.finishedAt,
|
|
13594
13974
|
error: runs.error
|
|
13595
|
-
}).from(runs).where(
|
|
13975
|
+
}).from(runs).where(eq20(runs.id, runId)).get();
|
|
13596
13976
|
}
|
|
13597
13977
|
isRunCancelled(runId) {
|
|
13598
13978
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -13608,7 +13988,7 @@ var JobRunner = class {
|
|
|
13608
13988
|
this.db.update(runs).set({
|
|
13609
13989
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13610
13990
|
error: currentRun.error ?? "Cancelled by user"
|
|
13611
|
-
}).where(
|
|
13991
|
+
}).where(eq20(runs.id, runId)).run();
|
|
13612
13992
|
}
|
|
13613
13993
|
trackEvent("run.completed", {
|
|
13614
13994
|
status: "cancelled",
|
|
@@ -13631,7 +14011,7 @@ function getCurrentUsageDay() {
|
|
|
13631
14011
|
|
|
13632
14012
|
// src/gsc-sync.ts
|
|
13633
14013
|
import crypto20 from "crypto";
|
|
13634
|
-
import { eq as
|
|
14014
|
+
import { eq as eq21, and as and10, sql as sql7 } from "drizzle-orm";
|
|
13635
14015
|
var log2 = createLogger("GscSync");
|
|
13636
14016
|
function formatDate2(d) {
|
|
13637
14017
|
return d.toISOString().split("T")[0];
|
|
@@ -13643,13 +14023,13 @@ function daysAgo(n) {
|
|
|
13643
14023
|
}
|
|
13644
14024
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
13645
14025
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13646
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14026
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq21(runs.id, runId)).run();
|
|
13647
14027
|
try {
|
|
13648
14028
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13649
14029
|
if (!googleClientId || !googleClientSecret) {
|
|
13650
14030
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13651
14031
|
}
|
|
13652
|
-
const project = db.select().from(projects).where(
|
|
14032
|
+
const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
13653
14033
|
if (!project) {
|
|
13654
14034
|
throw new Error(`Project not found: ${projectId}`);
|
|
13655
14035
|
}
|
|
@@ -13683,8 +14063,8 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13683
14063
|
});
|
|
13684
14064
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
13685
14065
|
db.delete(gscSearchData).where(
|
|
13686
|
-
|
|
13687
|
-
|
|
14066
|
+
and10(
|
|
14067
|
+
eq21(gscSearchData.projectId, projectId),
|
|
13688
14068
|
sql7`${gscSearchData.date} >= ${startDate}`,
|
|
13689
14069
|
sql7`${gscSearchData.date} <= ${endDate}`
|
|
13690
14070
|
)
|
|
@@ -13751,7 +14131,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13751
14131
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
13752
14132
|
}
|
|
13753
14133
|
}
|
|
13754
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14134
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, projectId)).all();
|
|
13755
14135
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13756
14136
|
for (const row of allInspections) {
|
|
13757
14137
|
const existing = latestByUrl.get(row.url);
|
|
@@ -13772,7 +14152,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13772
14152
|
}
|
|
13773
14153
|
}
|
|
13774
14154
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
13775
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14155
|
+
db.delete(gscCoverageSnapshots).where(and10(eq21(gscCoverageSnapshots.projectId, projectId), eq21(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
13776
14156
|
db.insert(gscCoverageSnapshots).values({
|
|
13777
14157
|
id: crypto20.randomUUID(),
|
|
13778
14158
|
projectId,
|
|
@@ -13783,11 +14163,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13783
14163
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
13784
14164
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13785
14165
|
}).run();
|
|
13786
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14166
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
13787
14167
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
13788
14168
|
} catch (err) {
|
|
13789
14169
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
13790
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14170
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
|
|
13791
14171
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
13792
14172
|
throw err;
|
|
13793
14173
|
}
|
|
@@ -13795,7 +14175,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
13795
14175
|
|
|
13796
14176
|
// src/gsc-inspect-sitemap.ts
|
|
13797
14177
|
import crypto21 from "crypto";
|
|
13798
|
-
import { eq as
|
|
14178
|
+
import { eq as eq22, and as and11 } from "drizzle-orm";
|
|
13799
14179
|
|
|
13800
14180
|
// src/sitemap-parser.ts
|
|
13801
14181
|
var log3 = createLogger("SitemapParser");
|
|
@@ -13916,13 +14296,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
13916
14296
|
var log4 = createLogger("InspectSitemap");
|
|
13917
14297
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
13918
14298
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13919
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
14299
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
|
|
13920
14300
|
try {
|
|
13921
14301
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
13922
14302
|
if (!googleClientId || !googleClientSecret) {
|
|
13923
14303
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
13924
14304
|
}
|
|
13925
|
-
const project = db.select().from(projects).where(
|
|
14305
|
+
const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
13926
14306
|
if (!project) {
|
|
13927
14307
|
throw new Error(`Project not found: ${projectId}`);
|
|
13928
14308
|
}
|
|
@@ -13990,7 +14370,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
13990
14370
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
13991
14371
|
}
|
|
13992
14372
|
}
|
|
13993
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
14373
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
|
|
13994
14374
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
13995
14375
|
for (const row of allInspections) {
|
|
13996
14376
|
const existing = latestByUrl.get(row.url);
|
|
@@ -14011,7 +14391,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14011
14391
|
}
|
|
14012
14392
|
}
|
|
14013
14393
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14014
|
-
db.delete(gscCoverageSnapshots).where(
|
|
14394
|
+
db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
14015
14395
|
db.insert(gscCoverageSnapshots).values({
|
|
14016
14396
|
id: crypto21.randomUUID(),
|
|
14017
14397
|
projectId,
|
|
@@ -14023,11 +14403,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14023
14403
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14024
14404
|
}).run();
|
|
14025
14405
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
14026
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14406
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
14027
14407
|
log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
14028
14408
|
} catch (err) {
|
|
14029
14409
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
14030
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
14410
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
14031
14411
|
log4.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14032
14412
|
throw err;
|
|
14033
14413
|
}
|
|
@@ -14035,7 +14415,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
14035
14415
|
|
|
14036
14416
|
// src/bing-inspect-sitemap.ts
|
|
14037
14417
|
import crypto22 from "crypto";
|
|
14038
|
-
import { eq as
|
|
14418
|
+
import { eq as eq23, desc as desc10 } from "drizzle-orm";
|
|
14039
14419
|
var log5 = createLogger("BingInspectSitemap");
|
|
14040
14420
|
function parseBingDate2(value) {
|
|
14041
14421
|
if (!value) return null;
|
|
@@ -14053,9 +14433,9 @@ function isBlockingIssueType2(issueType) {
|
|
|
14053
14433
|
}
|
|
14054
14434
|
async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
14055
14435
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14056
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
14436
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq23(runs.id, runId)).run();
|
|
14057
14437
|
try {
|
|
14058
|
-
const project = db.select().from(projects).where(
|
|
14438
|
+
const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
14059
14439
|
if (!project) {
|
|
14060
14440
|
throw new Error(`Project not found: ${projectId}`);
|
|
14061
14441
|
}
|
|
@@ -14073,7 +14453,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14073
14453
|
if (sitemapUrls.length === 0) {
|
|
14074
14454
|
throw new Error("No URLs found in sitemap");
|
|
14075
14455
|
}
|
|
14076
|
-
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(
|
|
14456
|
+
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).all();
|
|
14077
14457
|
const trackedUrls = new Set(trackedRows.map((r) => r.url));
|
|
14078
14458
|
const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
|
|
14079
14459
|
log5.info("sitemap.diff", {
|
|
@@ -14156,7 +14536,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14156
14536
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
14157
14537
|
}
|
|
14158
14538
|
}
|
|
14159
|
-
const allInspections = db.select().from(bingUrlInspections).where(
|
|
14539
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, projectId)).orderBy(desc10(bingUrlInspections.inspectedAt)).all();
|
|
14160
14540
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
14161
14541
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
14162
14542
|
for (const row of allInspections) {
|
|
@@ -14199,7 +14579,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14199
14579
|
}
|
|
14200
14580
|
}).run();
|
|
14201
14581
|
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(
|
|
14582
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14203
14583
|
log5.info("inspect.completed", {
|
|
14204
14584
|
runId,
|
|
14205
14585
|
projectId,
|
|
@@ -14213,7 +14593,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14213
14593
|
});
|
|
14214
14594
|
} catch (err) {
|
|
14215
14595
|
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(
|
|
14596
|
+
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
14217
14597
|
log5.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
14218
14598
|
throw err;
|
|
14219
14599
|
}
|
|
@@ -14222,7 +14602,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
14222
14602
|
// src/commoncrawl-sync.ts
|
|
14223
14603
|
import crypto23 from "crypto";
|
|
14224
14604
|
import path10 from "path";
|
|
14225
|
-
import { and as
|
|
14605
|
+
import { and as and12, eq as eq24, sql as sql8 } from "drizzle-orm";
|
|
14226
14606
|
var log6 = createLogger("CommonCrawlSync");
|
|
14227
14607
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
14228
14608
|
function defaultDeps() {
|
|
@@ -14248,7 +14628,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14248
14628
|
phaseDetail: "downloading vertices + edges",
|
|
14249
14629
|
updatedAt: downloadStartedAt,
|
|
14250
14630
|
error: null
|
|
14251
|
-
}).where(
|
|
14631
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14252
14632
|
const paths = ccReleasePaths(release);
|
|
14253
14633
|
const releaseCacheDir = path10.join(deps.cacheDir, release);
|
|
14254
14634
|
const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
|
|
@@ -14271,7 +14651,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14271
14651
|
vertexSha256: vertex.sha256,
|
|
14272
14652
|
edgesSha256: edges.sha256,
|
|
14273
14653
|
updatedAt: downloadFinishedAt
|
|
14274
|
-
}).where(
|
|
14654
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14275
14655
|
const allProjects = db.select().from(projects).all();
|
|
14276
14656
|
const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
|
|
14277
14657
|
let rows = [];
|
|
@@ -14287,8 +14667,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14287
14667
|
}
|
|
14288
14668
|
const queriedAt = deps.now().toISOString();
|
|
14289
14669
|
db.transaction((tx) => {
|
|
14290
|
-
tx.delete(backlinkDomains).where(
|
|
14291
|
-
tx.delete(backlinkSummaries).where(
|
|
14670
|
+
tx.delete(backlinkDomains).where(eq24(backlinkDomains.releaseSyncId, syncId)).run();
|
|
14671
|
+
tx.delete(backlinkSummaries).where(eq24(backlinkSummaries.releaseSyncId, syncId)).run();
|
|
14292
14672
|
const expanded = [];
|
|
14293
14673
|
for (const r of rows) {
|
|
14294
14674
|
const projectIds = projectsByDomain.get(r.targetDomain);
|
|
@@ -14347,7 +14727,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14347
14727
|
domainsDiscovered: rows.length,
|
|
14348
14728
|
updatedAt: finishedAt,
|
|
14349
14729
|
error: null
|
|
14350
|
-
}).where(
|
|
14730
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14351
14731
|
log6.info("sync.completed", {
|
|
14352
14732
|
syncId,
|
|
14353
14733
|
release,
|
|
@@ -14377,7 +14757,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
14377
14757
|
error: errorMsg,
|
|
14378
14758
|
phaseDetail: null,
|
|
14379
14759
|
updatedAt: finishedAt
|
|
14380
|
-
}).where(
|
|
14760
|
+
}).where(eq24(ccReleaseSyncs.id, syncId)).run();
|
|
14381
14761
|
log6.error("sync.failed", { syncId, release, error: errorMsg });
|
|
14382
14762
|
throw err;
|
|
14383
14763
|
}
|
|
@@ -14413,7 +14793,7 @@ function computeSummary(rows) {
|
|
|
14413
14793
|
// src/backlink-extract.ts
|
|
14414
14794
|
import crypto24 from "crypto";
|
|
14415
14795
|
import fs8 from "fs";
|
|
14416
|
-
import { and as
|
|
14796
|
+
import { and as and13, desc as desc11, eq as eq25 } from "drizzle-orm";
|
|
14417
14797
|
var log7 = createLogger("BacklinkExtract");
|
|
14418
14798
|
function defaultDeps2() {
|
|
14419
14799
|
return {
|
|
@@ -14425,13 +14805,13 @@ function defaultDeps2() {
|
|
|
14425
14805
|
async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
14426
14806
|
const deps = { ...defaultDeps2(), ...opts.deps };
|
|
14427
14807
|
const startedAt = deps.now().toISOString();
|
|
14428
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
14808
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
|
|
14429
14809
|
try {
|
|
14430
|
-
const project = db.select().from(projects).where(
|
|
14810
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
14431
14811
|
if (!project) {
|
|
14432
14812
|
throw new Error(`Project not found: ${projectId}`);
|
|
14433
14813
|
}
|
|
14434
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(
|
|
14814
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc11(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
14435
14815
|
if (!sync) {
|
|
14436
14816
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
14437
14817
|
}
|
|
@@ -14459,7 +14839,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14459
14839
|
const targetDomain = project.canonicalDomain;
|
|
14460
14840
|
db.transaction((tx) => {
|
|
14461
14841
|
tx.delete(backlinkDomains).where(
|
|
14462
|
-
|
|
14842
|
+
and13(eq25(backlinkDomains.projectId, projectId), eq25(backlinkDomains.release, release))
|
|
14463
14843
|
).run();
|
|
14464
14844
|
if (rows.length > 0) {
|
|
14465
14845
|
const values = rows.map((r) => ({
|
|
@@ -14499,7 +14879,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14499
14879
|
}).run();
|
|
14500
14880
|
});
|
|
14501
14881
|
const finishedAt = deps.now().toISOString();
|
|
14502
|
-
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
14882
|
+
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq25(runs.id, runId)).run();
|
|
14503
14883
|
log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
|
|
14504
14884
|
} catch (err) {
|
|
14505
14885
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -14508,7 +14888,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
14508
14888
|
status: RunStatuses.failed,
|
|
14509
14889
|
error: errorMsg,
|
|
14510
14890
|
finishedAt
|
|
14511
|
-
}).where(
|
|
14891
|
+
}).where(eq25(runs.id, runId)).run();
|
|
14512
14892
|
log7.error("extract.failed", { runId, projectId, error: errorMsg });
|
|
14513
14893
|
throw err;
|
|
14514
14894
|
}
|
|
@@ -14581,7 +14961,7 @@ var ProviderRegistry = class {
|
|
|
14581
14961
|
|
|
14582
14962
|
// src/scheduler.ts
|
|
14583
14963
|
import cron from "node-cron";
|
|
14584
|
-
import { eq as
|
|
14964
|
+
import { eq as eq26 } from "drizzle-orm";
|
|
14585
14965
|
var log8 = createLogger("Scheduler");
|
|
14586
14966
|
var Scheduler = class {
|
|
14587
14967
|
db;
|
|
@@ -14593,7 +14973,7 @@ var Scheduler = class {
|
|
|
14593
14973
|
}
|
|
14594
14974
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
14595
14975
|
start() {
|
|
14596
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14976
|
+
const allSchedules = this.db.select().from(schedules).where(eq26(schedules.enabled, 1)).all();
|
|
14597
14977
|
for (const schedule of allSchedules) {
|
|
14598
14978
|
const missedRunAt = schedule.nextRunAt;
|
|
14599
14979
|
this.registerCronTask(schedule);
|
|
@@ -14618,7 +14998,7 @@ var Scheduler = class {
|
|
|
14618
14998
|
this.stopTask(projectId, existing, "Stopped");
|
|
14619
14999
|
this.tasks.delete(projectId);
|
|
14620
15000
|
}
|
|
14621
|
-
const schedule = this.db.select().from(schedules).where(
|
|
15001
|
+
const schedule = this.db.select().from(schedules).where(eq26(schedules.projectId, projectId)).get();
|
|
14622
15002
|
if (schedule && schedule.enabled === 1) {
|
|
14623
15003
|
this.registerCronTask(schedule);
|
|
14624
15004
|
}
|
|
@@ -14651,14 +15031,14 @@ var Scheduler = class {
|
|
|
14651
15031
|
this.db.update(schedules).set({
|
|
14652
15032
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
14653
15033
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14654
|
-
}).where(
|
|
15034
|
+
}).where(eq26(schedules.id, scheduleId)).run();
|
|
14655
15035
|
const label = schedule.preset ?? cronExpr;
|
|
14656
15036
|
log8.info("cron.registered", { projectId, schedule: label, timezone });
|
|
14657
15037
|
}
|
|
14658
15038
|
triggerRun(scheduleId, projectId) {
|
|
14659
15039
|
try {
|
|
14660
15040
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14661
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
15041
|
+
const currentSchedule = this.db.select().from(schedules).where(eq26(schedules.id, scheduleId)).get();
|
|
14662
15042
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
14663
15043
|
log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
14664
15044
|
this.remove(projectId);
|
|
@@ -14666,7 +15046,7 @@ var Scheduler = class {
|
|
|
14666
15046
|
}
|
|
14667
15047
|
const task = this.tasks.get(projectId);
|
|
14668
15048
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
14669
|
-
const project = this.db.select().from(projects).where(
|
|
15049
|
+
const project = this.db.select().from(projects).where(eq26(projects.id, projectId)).get();
|
|
14670
15050
|
if (!project) {
|
|
14671
15051
|
log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
14672
15052
|
this.remove(projectId);
|
|
@@ -14695,7 +15075,7 @@ var Scheduler = class {
|
|
|
14695
15075
|
this.db.update(schedules).set({
|
|
14696
15076
|
nextRunAt,
|
|
14697
15077
|
updatedAt: now
|
|
14698
|
-
}).where(
|
|
15078
|
+
}).where(eq26(schedules.id, currentSchedule.id)).run();
|
|
14699
15079
|
return;
|
|
14700
15080
|
}
|
|
14701
15081
|
const runId = queueResult.runId;
|
|
@@ -14703,7 +15083,7 @@ var Scheduler = class {
|
|
|
14703
15083
|
lastRunAt: now,
|
|
14704
15084
|
nextRunAt,
|
|
14705
15085
|
updatedAt: now
|
|
14706
|
-
}).where(
|
|
15086
|
+
}).where(eq26(schedules.id, currentSchedule.id)).run();
|
|
14707
15087
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
14708
15088
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
14709
15089
|
log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -14715,7 +15095,7 @@ var Scheduler = class {
|
|
|
14715
15095
|
};
|
|
14716
15096
|
|
|
14717
15097
|
// src/notifier.ts
|
|
14718
|
-
import { eq as
|
|
15098
|
+
import { eq as eq27, desc as desc12, and as and14, or as or2 } from "drizzle-orm";
|
|
14719
15099
|
import crypto25 from "crypto";
|
|
14720
15100
|
var log9 = createLogger("Notifier");
|
|
14721
15101
|
var Notifier = class {
|
|
@@ -14728,18 +15108,18 @@ var Notifier = class {
|
|
|
14728
15108
|
/** Called after a run completes (success, partial, or failed). */
|
|
14729
15109
|
async onRunCompleted(runId, projectId) {
|
|
14730
15110
|
log9.info("run.completed", { runId, projectId });
|
|
14731
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15111
|
+
const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14732
15112
|
if (notifs.length === 0) {
|
|
14733
15113
|
log9.info("notifications.none-enabled", { projectId });
|
|
14734
15114
|
return;
|
|
14735
15115
|
}
|
|
14736
15116
|
log9.info("notifications.found", { projectId, count: notifs.length });
|
|
14737
|
-
const run = this.db.select().from(runs).where(
|
|
15117
|
+
const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
14738
15118
|
if (!run) {
|
|
14739
15119
|
log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
14740
15120
|
return;
|
|
14741
15121
|
}
|
|
14742
|
-
const project = this.db.select().from(projects).where(
|
|
15122
|
+
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
14743
15123
|
if (!project) {
|
|
14744
15124
|
log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
14745
15125
|
return;
|
|
@@ -14786,11 +15166,11 @@ var Notifier = class {
|
|
|
14786
15166
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
14787
15167
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
14788
15168
|
if (insightEvents.length === 0) return;
|
|
14789
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15169
|
+
const notifs = this.db.select().from(notifications).where(eq27(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
14790
15170
|
if (notifs.length === 0) return;
|
|
14791
|
-
const run = this.db.select().from(runs).where(
|
|
15171
|
+
const run = this.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
14792
15172
|
if (!run) return;
|
|
14793
|
-
const project = this.db.select().from(projects).where(
|
|
15173
|
+
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
14794
15174
|
if (!project) return;
|
|
14795
15175
|
for (const notif of notifs) {
|
|
14796
15176
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -14821,11 +15201,11 @@ var Notifier = class {
|
|
|
14821
15201
|
}
|
|
14822
15202
|
computeTransitions(runId, projectId) {
|
|
14823
15203
|
const recentRuns = this.db.select().from(runs).where(
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
or2(
|
|
15204
|
+
and14(
|
|
15205
|
+
eq27(runs.projectId, projectId),
|
|
15206
|
+
or2(eq27(runs.status, "completed"), eq27(runs.status, "partial"))
|
|
14827
15207
|
)
|
|
14828
|
-
).orderBy(
|
|
15208
|
+
).orderBy(desc12(runs.createdAt)).limit(2).all();
|
|
14829
15209
|
if (recentRuns.length < 2) return [];
|
|
14830
15210
|
const currentRunId = recentRuns[0].id;
|
|
14831
15211
|
const previousRunId = recentRuns[1].id;
|
|
@@ -14835,12 +15215,12 @@ var Notifier = class {
|
|
|
14835
15215
|
keyword: keywords.keyword,
|
|
14836
15216
|
provider: querySnapshots.provider,
|
|
14837
15217
|
citationState: querySnapshots.citationState
|
|
14838
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
15218
|
+
}).from(querySnapshots).leftJoin(keywords, eq27(querySnapshots.keywordId, keywords.id)).where(eq27(querySnapshots.runId, currentRunId)).all();
|
|
14839
15219
|
const previousSnapshots = this.db.select({
|
|
14840
15220
|
keywordId: querySnapshots.keywordId,
|
|
14841
15221
|
provider: querySnapshots.provider,
|
|
14842
15222
|
citationState: querySnapshots.citationState
|
|
14843
|
-
}).from(querySnapshots).where(
|
|
15223
|
+
}).from(querySnapshots).where(eq27(querySnapshots.runId, previousRunId)).all();
|
|
14844
15224
|
const prevMap = /* @__PURE__ */ new Map();
|
|
14845
15225
|
for (const s of previousSnapshots) {
|
|
14846
15226
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -14957,7 +15337,7 @@ var RunCoordinator = class {
|
|
|
14957
15337
|
|
|
14958
15338
|
// src/agent/session-registry.ts
|
|
14959
15339
|
import crypto27 from "crypto";
|
|
14960
|
-
import { eq as
|
|
15340
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
14961
15341
|
|
|
14962
15342
|
// src/agent/session.ts
|
|
14963
15343
|
import fs11 from "fs";
|
|
@@ -15177,7 +15557,7 @@ import { Type as Type2 } from "@sinclair/typebox";
|
|
|
15177
15557
|
|
|
15178
15558
|
// src/agent/memory-store.ts
|
|
15179
15559
|
import crypto26 from "crypto";
|
|
15180
|
-
import { and as
|
|
15560
|
+
import { and as and15, desc as desc13, eq as eq28, like, sql as sql9 } from "drizzle-orm";
|
|
15181
15561
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
15182
15562
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
15183
15563
|
function rowToDto(row) {
|
|
@@ -15191,7 +15571,7 @@ function rowToDto(row) {
|
|
|
15191
15571
|
};
|
|
15192
15572
|
}
|
|
15193
15573
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
15194
|
-
const query = db.select().from(agentMemory).where(
|
|
15574
|
+
const query = db.select().from(agentMemory).where(eq28(agentMemory.projectId, projectId)).orderBy(desc13(agentMemory.updatedAt));
|
|
15195
15575
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
15196
15576
|
return rows.map(rowToDto);
|
|
15197
15577
|
}
|
|
@@ -15222,12 +15602,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
15222
15602
|
updatedAt: now
|
|
15223
15603
|
}
|
|
15224
15604
|
}).run();
|
|
15225
|
-
const row = db.select().from(agentMemory).where(
|
|
15605
|
+
const row = db.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, args.key))).get();
|
|
15226
15606
|
if (!row) throw new Error("memory upsert produced no row");
|
|
15227
15607
|
return rowToDto(row);
|
|
15228
15608
|
}
|
|
15229
15609
|
function deleteMemoryEntry(db, projectId, key) {
|
|
15230
|
-
const result = db.delete(agentMemory).where(
|
|
15610
|
+
const result = db.delete(agentMemory).where(and15(eq28(agentMemory.projectId, projectId), eq28(agentMemory.key, key))).run();
|
|
15231
15611
|
const changes = result.changes ?? 0;
|
|
15232
15612
|
return changes > 0;
|
|
15233
15613
|
}
|
|
@@ -15256,16 +15636,16 @@ function writeCompactionNote(db, args) {
|
|
|
15256
15636
|
}).run();
|
|
15257
15637
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
15258
15638
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
15259
|
-
|
|
15260
|
-
|
|
15639
|
+
and15(
|
|
15640
|
+
eq28(agentMemory.projectId, args.projectId),
|
|
15261
15641
|
like(agentMemory.key, `${sessionPrefix}%`)
|
|
15262
15642
|
)
|
|
15263
|
-
).orderBy(
|
|
15643
|
+
).orderBy(desc13(agentMemory.updatedAt)).all();
|
|
15264
15644
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
15265
15645
|
if (stale.length > 0) {
|
|
15266
15646
|
tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
|
|
15267
15647
|
}
|
|
15268
|
-
const row = tx.select().from(agentMemory).where(
|
|
15648
|
+
const row = tx.select().from(agentMemory).where(and15(eq28(agentMemory.projectId, args.projectId), eq28(agentMemory.key, key))).get();
|
|
15269
15649
|
if (row) inserted = rowToDto(row);
|
|
15270
15650
|
});
|
|
15271
15651
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -15395,6 +15775,59 @@ function buildListCompetitorsTool(ctx) {
|
|
|
15395
15775
|
}
|
|
15396
15776
|
};
|
|
15397
15777
|
}
|
|
15778
|
+
var ContentTargetsSchema = Type2.Object({
|
|
15779
|
+
limit: Type2.Optional(
|
|
15780
|
+
Type2.Number({
|
|
15781
|
+
description: "Max rows. Defaults to all. Use a small number (3\u201310) when summarizing for the user."
|
|
15782
|
+
})
|
|
15783
|
+
),
|
|
15784
|
+
includeInProgress: Type2.Optional(
|
|
15785
|
+
Type2.Boolean({
|
|
15786
|
+
description: "Include rows that already have an in-flight tracked action. Default false."
|
|
15787
|
+
})
|
|
15788
|
+
)
|
|
15789
|
+
});
|
|
15790
|
+
function buildGetContentTargetsTool(ctx) {
|
|
15791
|
+
return {
|
|
15792
|
+
name: "get_content_targets",
|
|
15793
|
+
label: "Get content targets",
|
|
15794
|
+
description: "Ranked, action-typed content opportunities. Each row is `{query, action \u2208 create|expand|refresh|add-schema, ourBestPage?, winningCompetitor?, score, scoreBreakdown, drivers[], demandSource, actionConfidence}`. Use this to recommend which post the user should write/refresh next.",
|
|
15795
|
+
parameters: ContentTargetsSchema,
|
|
15796
|
+
execute: async (_toolCallId, params) => {
|
|
15797
|
+
const response = await ctx.client.getContentTargets(ctx.projectName, {
|
|
15798
|
+
limit: params.limit,
|
|
15799
|
+
includeInProgress: params.includeInProgress === true
|
|
15800
|
+
});
|
|
15801
|
+
return textResult2(response);
|
|
15802
|
+
}
|
|
15803
|
+
};
|
|
15804
|
+
}
|
|
15805
|
+
var ContentSourcesSchema = Type2.Object({});
|
|
15806
|
+
function buildGetContentSourcesTool(ctx) {
|
|
15807
|
+
return {
|
|
15808
|
+
name: "get_grounding_sources",
|
|
15809
|
+
label: "Get grounding sources",
|
|
15810
|
+
description: "URL-level competitive grounding-source map. Per query, lists every URL the LLM cited (our domain vs competitors), with citation count and providers. Read this to understand what specific competitor URL is winning a query.",
|
|
15811
|
+
parameters: ContentSourcesSchema,
|
|
15812
|
+
execute: async () => {
|
|
15813
|
+
const response = await ctx.client.getContentSources(ctx.projectName);
|
|
15814
|
+
return textResult2(response);
|
|
15815
|
+
}
|
|
15816
|
+
};
|
|
15817
|
+
}
|
|
15818
|
+
var ContentGapsSchema = Type2.Object({});
|
|
15819
|
+
function buildGetContentGapsTool(ctx) {
|
|
15820
|
+
return {
|
|
15821
|
+
name: "get_content_gaps",
|
|
15822
|
+
label: "Get content gaps",
|
|
15823
|
+
description: 'Queries where competitors are cited but our domain is not. Ranked by miss rate. The blunt-instrument view of "what competitors are winning that we are not." Use `get_content_targets` for action-typed recommendations on the same data.',
|
|
15824
|
+
parameters: ContentGapsSchema,
|
|
15825
|
+
execute: async () => {
|
|
15826
|
+
const response = await ctx.client.getContentGaps(ctx.projectName);
|
|
15827
|
+
return textResult2(response);
|
|
15828
|
+
}
|
|
15829
|
+
};
|
|
15830
|
+
}
|
|
15398
15831
|
var RunDetailSchema = Type2.Object({
|
|
15399
15832
|
runId: Type2.String({
|
|
15400
15833
|
description: "Run id (UUID) to fetch. Typically obtained from get_status runs[].id."
|
|
@@ -15472,7 +15905,10 @@ function buildReadTools(ctx) {
|
|
|
15472
15905
|
buildListCompetitorsTool(ctx),
|
|
15473
15906
|
buildGetRunTool(ctx),
|
|
15474
15907
|
buildRecallTool(ctx),
|
|
15475
|
-
buildListBacklinksTool(ctx)
|
|
15908
|
+
buildListBacklinksTool(ctx),
|
|
15909
|
+
buildGetContentTargetsTool(ctx),
|
|
15910
|
+
buildGetContentSourcesTool(ctx),
|
|
15911
|
+
buildGetContentGapsTool(ctx)
|
|
15476
15912
|
];
|
|
15477
15913
|
}
|
|
15478
15914
|
var RunSweepSchema = Type2.Object({
|
|
@@ -15941,7 +16377,7 @@ var SessionRegistry = class {
|
|
|
15941
16377
|
modelProvider: effectiveProvider,
|
|
15942
16378
|
modelId: effectiveModelId,
|
|
15943
16379
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15944
|
-
}).where(
|
|
16380
|
+
}).where(eq29(agentSessions.projectId, projectId)).run();
|
|
15945
16381
|
}
|
|
15946
16382
|
const agent2 = createAeroSession({
|
|
15947
16383
|
projectName,
|
|
@@ -16159,7 +16595,7 @@ ${lines.join("\n")}
|
|
|
16159
16595
|
modelProvider: nextProvider,
|
|
16160
16596
|
modelId: nextModelId,
|
|
16161
16597
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16162
|
-
}).where(
|
|
16598
|
+
}).where(eq29(agentSessions.projectId, projectId)).run();
|
|
16163
16599
|
}
|
|
16164
16600
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
16165
16601
|
save(projectName) {
|
|
@@ -16321,11 +16757,11 @@ ${lines.join("\n")}
|
|
|
16321
16757
|
return id;
|
|
16322
16758
|
}
|
|
16323
16759
|
tryResolveProjectId(projectName) {
|
|
16324
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
16760
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq29(projects.name, projectName)).get();
|
|
16325
16761
|
return row?.id;
|
|
16326
16762
|
}
|
|
16327
16763
|
loadRow(projectId) {
|
|
16328
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
16764
|
+
const row = this.opts.db.select().from(agentSessions).where(eq29(agentSessions.projectId, projectId)).get();
|
|
16329
16765
|
return row ?? null;
|
|
16330
16766
|
}
|
|
16331
16767
|
insertRow(params) {
|
|
@@ -16344,14 +16780,14 @@ ${lines.join("\n")}
|
|
|
16344
16780
|
}
|
|
16345
16781
|
updateRow(projectId, patch) {
|
|
16346
16782
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16347
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
16783
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq29(agentSessions.projectId, projectId)).run();
|
|
16348
16784
|
}
|
|
16349
16785
|
};
|
|
16350
16786
|
|
|
16351
16787
|
// src/agent/agent-routes.ts
|
|
16352
|
-
import { eq as
|
|
16788
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
16353
16789
|
function resolveProject2(db, name) {
|
|
16354
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
16790
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq30(projects.name, name)).get();
|
|
16355
16791
|
if (!row) throw notFound("project", name);
|
|
16356
16792
|
return row;
|
|
16357
16793
|
}
|
|
@@ -16360,7 +16796,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16360
16796
|
"/projects/:name/agent/transcript",
|
|
16361
16797
|
async (request) => {
|
|
16362
16798
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16363
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
16799
|
+
const row = opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, project.id)).get();
|
|
16364
16800
|
if (!row) {
|
|
16365
16801
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
16366
16802
|
}
|
|
@@ -16384,7 +16820,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
16384
16820
|
async (request) => {
|
|
16385
16821
|
const project = resolveProject2(opts.db, request.params.name);
|
|
16386
16822
|
opts.sessionRegistry.reset(project.name);
|
|
16387
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16823
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(agentSessions.projectId, project.id)).run();
|
|
16388
16824
|
return { status: "reset" };
|
|
16389
16825
|
}
|
|
16390
16826
|
);
|
|
@@ -16688,7 +17124,7 @@ var SnapshotService = class {
|
|
|
16688
17124
|
}
|
|
16689
17125
|
async createReport(input) {
|
|
16690
17126
|
const companyName = input.companyName.trim();
|
|
16691
|
-
const domain =
|
|
17127
|
+
const domain = normalizeDomain2(input.domain);
|
|
16692
17128
|
const manualPhrases = normalizeStringList(input.phrases ?? []);
|
|
16693
17129
|
const manualCompetitors = normalizeStringList(input.competitors ?? []);
|
|
16694
17130
|
const providers = this.registry.getAll();
|
|
@@ -17128,7 +17564,7 @@ function extractCompetitorsFromResponse(ctx) {
|
|
|
17128
17564
|
const targetDomain = extractHostname2(ctx.targetDomain);
|
|
17129
17565
|
for (const hint of ctx.manualCompetitors) {
|
|
17130
17566
|
if (isDomainLike(hint)) {
|
|
17131
|
-
const normalizedHint =
|
|
17567
|
+
const normalizedHint = normalizeDomain2(hint);
|
|
17132
17568
|
if (domainMatches2(normalizedHint, targetDomain)) continue;
|
|
17133
17569
|
if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
|
|
17134
17570
|
competitors2.add(normalizedHint);
|
|
@@ -17187,7 +17623,7 @@ function uniqueStrings2(values) {
|
|
|
17187
17623
|
values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
|
|
17188
17624
|
)];
|
|
17189
17625
|
}
|
|
17190
|
-
function
|
|
17626
|
+
function normalizeDomain2(value) {
|
|
17191
17627
|
const trimmed = value.trim();
|
|
17192
17628
|
if (!trimmed) return trimmed;
|
|
17193
17629
|
try {
|
|
@@ -17198,15 +17634,15 @@ function normalizeDomain(value) {
|
|
|
17198
17634
|
}
|
|
17199
17635
|
}
|
|
17200
17636
|
function extractHostname2(value) {
|
|
17201
|
-
return
|
|
17637
|
+
return normalizeDomain2(value);
|
|
17202
17638
|
}
|
|
17203
17639
|
function domainMatches2(candidate, target) {
|
|
17204
|
-
const normalizedCandidate =
|
|
17205
|
-
const normalizedTarget =
|
|
17640
|
+
const normalizedCandidate = normalizeDomain2(candidate);
|
|
17641
|
+
const normalizedTarget = normalizeDomain2(target);
|
|
17206
17642
|
return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
|
|
17207
17643
|
}
|
|
17208
17644
|
function isDomainLike(value) {
|
|
17209
|
-
const normalized =
|
|
17645
|
+
const normalized = normalizeDomain2(value);
|
|
17210
17646
|
return normalized.includes(".") && !normalized.includes(" ");
|
|
17211
17647
|
}
|
|
17212
17648
|
function clipText(value, length) {
|
|
@@ -17406,7 +17842,7 @@ async function createServer(opts) {
|
|
|
17406
17842
|
intelligenceService,
|
|
17407
17843
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
17408
17844
|
async ({ runId, projectId, insightCount, criticalOrHigh }) => {
|
|
17409
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
17845
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq31(projects.id, projectId)).get();
|
|
17410
17846
|
if (!project) return;
|
|
17411
17847
|
sessionRegistry.queueFollowUp(project.name, {
|
|
17412
17848
|
role: "user",
|
|
@@ -17546,7 +17982,7 @@ async function createServer(opts) {
|
|
|
17546
17982
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
17547
17983
|
if (opts.config.apiKey) {
|
|
17548
17984
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
17549
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
17985
|
+
const existing = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, keyHash)).get();
|
|
17550
17986
|
if (!existing) {
|
|
17551
17987
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
17552
17988
|
opts.db.insert(apiKeys).values({
|
|
@@ -17598,7 +18034,7 @@ async function createServer(opts) {
|
|
|
17598
18034
|
};
|
|
17599
18035
|
const getDefaultApiKey = () => {
|
|
17600
18036
|
if (!opts.config.apiKey) return void 0;
|
|
17601
|
-
return opts.db.select().from(apiKeys).where(
|
|
18037
|
+
return opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
17602
18038
|
};
|
|
17603
18039
|
const createPasswordSession = (reply) => {
|
|
17604
18040
|
const key = getDefaultApiKey();
|
|
@@ -17655,12 +18091,12 @@ async function createServer(opts) {
|
|
|
17655
18091
|
return reply.send({ authenticated: true });
|
|
17656
18092
|
}
|
|
17657
18093
|
if (apiKey) {
|
|
17658
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
18094
|
+
const key = opts.db.select().from(apiKeys).where(eq31(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
17659
18095
|
if (!key || key.revokedAt) {
|
|
17660
18096
|
const err2 = authInvalid();
|
|
17661
18097
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
17662
18098
|
}
|
|
17663
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
18099
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(apiKeys.id, key.id)).run();
|
|
17664
18100
|
const sessionId = createSession(key.id);
|
|
17665
18101
|
reply.header("set-cookie", serializeSessionCookie({
|
|
17666
18102
|
name: SESSION_COOKIE_NAME,
|