@ainyc/canonry 2.16.0 → 2.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/assets/index-CwZdqfeb.js +302 -0
- package/assets/assets/{index-DMx3Oy9W.css → index-Ec4au-rY.css} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-CKABU6PE.js → chunk-75EV36V6.js} +525 -214
- package/dist/{chunk-HNVRN5QL.js → chunk-7DVIJC6L.js} +144 -62
- package/dist/cli.js +90 -2
- package/dist/index.js +2 -2
- package/dist/mcp.js +1 -1
- package/package.json +7 -7
- package/assets/assets/index-B18l8ugs.js +0 -302
|
@@ -23,11 +23,13 @@ import {
|
|
|
23
23
|
canonryMcpTools,
|
|
24
24
|
categorizeSource,
|
|
25
25
|
categoryLabel,
|
|
26
|
+
citationStateToCited,
|
|
26
27
|
competitorBatchRequestSchema,
|
|
27
28
|
configExists,
|
|
28
29
|
deliveryFailed,
|
|
29
30
|
determineAnswerMentioned,
|
|
30
31
|
effectiveDomains,
|
|
32
|
+
emptyCitationVisibility,
|
|
31
33
|
extractAnswerMentions,
|
|
32
34
|
findDuplicateLocationLabels,
|
|
33
35
|
hasLocationLabel,
|
|
@@ -60,7 +62,7 @@ import {
|
|
|
60
62
|
visibilityStateFromAnswerMentioned,
|
|
61
63
|
windowCutoff,
|
|
62
64
|
wordpressEnvSchema
|
|
63
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-7DVIJC6L.js";
|
|
64
66
|
import {
|
|
65
67
|
IntelligenceService,
|
|
66
68
|
agentMemory,
|
|
@@ -178,7 +180,7 @@ import crypto28 from "crypto";
|
|
|
178
180
|
import fs12 from "fs";
|
|
179
181
|
import path14 from "path";
|
|
180
182
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
181
|
-
import { eq as
|
|
183
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
182
184
|
import Fastify from "fastify";
|
|
183
185
|
|
|
184
186
|
// ../api-routes/src/auth.ts
|
|
@@ -2358,22 +2360,168 @@ async function intelligenceRoutes(app) {
|
|
|
2358
2360
|
});
|
|
2359
2361
|
}
|
|
2360
2362
|
|
|
2363
|
+
// ../api-routes/src/citations.ts
|
|
2364
|
+
import { eq as eq12, inArray as inArray3 } from "drizzle-orm";
|
|
2365
|
+
async function citationRoutes(app) {
|
|
2366
|
+
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
2367
|
+
const project = resolveProject(app.db, request.params.name);
|
|
2368
|
+
const configuredProviders = parseJsonColumn(project.providers, []);
|
|
2369
|
+
const projectKeywords = app.db.select().from(keywords).where(eq12(keywords.projectId, project.id)).all();
|
|
2370
|
+
if (projectKeywords.length === 0) {
|
|
2371
|
+
return reply.send(emptyCitationVisibility("no-keywords"));
|
|
2372
|
+
}
|
|
2373
|
+
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq12(runs.projectId, project.id)).all();
|
|
2374
|
+
if (projectRuns.length === 0) {
|
|
2375
|
+
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
2376
|
+
}
|
|
2377
|
+
const runCreatedAt = new Map(projectRuns.map((r) => [r.id, r.createdAt]));
|
|
2378
|
+
const rawSnapshots = app.db.select({
|
|
2379
|
+
id: querySnapshots.id,
|
|
2380
|
+
runId: querySnapshots.runId,
|
|
2381
|
+
keywordId: querySnapshots.keywordId,
|
|
2382
|
+
provider: querySnapshots.provider,
|
|
2383
|
+
citationState: querySnapshots.citationState,
|
|
2384
|
+
citedDomains: querySnapshots.citedDomains,
|
|
2385
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
2386
|
+
createdAt: querySnapshots.createdAt
|
|
2387
|
+
}).from(querySnapshots).where(inArray3(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
|
|
2388
|
+
if (rawSnapshots.length === 0) {
|
|
2389
|
+
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
2390
|
+
}
|
|
2391
|
+
const snapshots = rawSnapshots.map((s) => ({
|
|
2392
|
+
...s,
|
|
2393
|
+
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
2394
|
+
}));
|
|
2395
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, project.id)).all().map((c) => normalizeDomain(c.domain)).filter((d) => d.length > 0);
|
|
2396
|
+
const response = computeCitationVisibility({
|
|
2397
|
+
keywords: projectKeywords.map((k) => ({ id: k.id, keyword: k.keyword })),
|
|
2398
|
+
snapshots,
|
|
2399
|
+
configuredProviders,
|
|
2400
|
+
competitorDomains: projectCompetitors
|
|
2401
|
+
});
|
|
2402
|
+
return reply.send(response);
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
function computeCitationVisibility(input) {
|
|
2406
|
+
const { keywords: kws, snapshots, configuredProviders, competitorDomains } = input;
|
|
2407
|
+
const latestByPair = /* @__PURE__ */ new Map();
|
|
2408
|
+
for (const snap of snapshots) {
|
|
2409
|
+
const key = `${snap.keywordId}::${snap.provider}`;
|
|
2410
|
+
const existing = latestByPair.get(key);
|
|
2411
|
+
if (!existing || snap.createdAt > existing.createdAt) {
|
|
2412
|
+
latestByPair.set(key, snap);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const observedProviders = /* @__PURE__ */ new Set();
|
|
2416
|
+
for (const pair of latestByPair.values()) observedProviders.add(pair.provider);
|
|
2417
|
+
const providerUniverse = configuredProviders.length > 0 ? Array.from(new Set(configuredProviders)) : Array.from(observedProviders).sort();
|
|
2418
|
+
const byKeyword = [];
|
|
2419
|
+
const providersCitingTracker = /* @__PURE__ */ new Set();
|
|
2420
|
+
let keywordsCited = 0;
|
|
2421
|
+
let keywordsFullyCovered = 0;
|
|
2422
|
+
let keywordsUncovered = 0;
|
|
2423
|
+
for (const kw of kws) {
|
|
2424
|
+
const providers = [];
|
|
2425
|
+
let citedCount = 0;
|
|
2426
|
+
for (const provider of providerUniverse) {
|
|
2427
|
+
const snap = latestByPair.get(`${kw.id}::${provider}`);
|
|
2428
|
+
if (!snap) continue;
|
|
2429
|
+
const state = snap.citationState;
|
|
2430
|
+
const cited = citationStateToCited(state);
|
|
2431
|
+
if (cited) {
|
|
2432
|
+
citedCount++;
|
|
2433
|
+
providersCitingTracker.add(provider);
|
|
2434
|
+
}
|
|
2435
|
+
providers.push({
|
|
2436
|
+
provider,
|
|
2437
|
+
citationState: state,
|
|
2438
|
+
cited,
|
|
2439
|
+
runId: snap.runId,
|
|
2440
|
+
runCreatedAt: snap.runCreatedAt
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
if (citedCount > 0) keywordsCited++;
|
|
2444
|
+
if (providerUniverse.length > 0 && citedCount === providerUniverse.length) keywordsFullyCovered++;
|
|
2445
|
+
if (providers.length > 0 && citedCount === 0) keywordsUncovered++;
|
|
2446
|
+
byKeyword.push({
|
|
2447
|
+
keywordId: kw.id,
|
|
2448
|
+
keyword: kw.keyword,
|
|
2449
|
+
providers,
|
|
2450
|
+
citedCount,
|
|
2451
|
+
totalProviders: providers.length
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
const competitorSet = new Set(competitorDomains);
|
|
2455
|
+
const competitorGaps = [];
|
|
2456
|
+
const keywordById = new Map(kws.map((k) => [k.id, k.keyword]));
|
|
2457
|
+
for (const snap of latestByPair.values()) {
|
|
2458
|
+
if (citationStateToCited(snap.citationState)) continue;
|
|
2459
|
+
if (competitorSet.size === 0) continue;
|
|
2460
|
+
const cited = parseJsonColumn(snap.citedDomains, []);
|
|
2461
|
+
const overlap = parseJsonColumn(snap.competitorOverlap, []);
|
|
2462
|
+
const candidates = new Set(
|
|
2463
|
+
[...cited, ...overlap].map((d) => normalizeDomain(d)).filter((d) => d.length > 0)
|
|
2464
|
+
);
|
|
2465
|
+
const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
|
|
2466
|
+
if (citingCompetitors.length === 0) continue;
|
|
2467
|
+
competitorGaps.push({
|
|
2468
|
+
keywordId: snap.keywordId,
|
|
2469
|
+
keyword: keywordById.get(snap.keywordId) ?? "",
|
|
2470
|
+
provider: snap.provider,
|
|
2471
|
+
citingCompetitors: citingCompetitors.sort(),
|
|
2472
|
+
runId: snap.runId,
|
|
2473
|
+
runCreatedAt: snap.runCreatedAt
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
competitorGaps.sort((a, b) => {
|
|
2477
|
+
if (a.keyword !== b.keyword) return a.keyword.localeCompare(b.keyword);
|
|
2478
|
+
return a.provider.localeCompare(b.provider);
|
|
2479
|
+
});
|
|
2480
|
+
let latestRunId = null;
|
|
2481
|
+
let latestRunAt = null;
|
|
2482
|
+
for (const snap of latestByPair.values()) {
|
|
2483
|
+
if (latestRunAt === null || snap.runCreatedAt > latestRunAt) {
|
|
2484
|
+
latestRunAt = snap.runCreatedAt;
|
|
2485
|
+
latestRunId = snap.runId;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
const summary = {
|
|
2489
|
+
providersConfigured: providerUniverse.length,
|
|
2490
|
+
providersCiting: providersCitingTracker.size,
|
|
2491
|
+
totalKeywords: kws.length,
|
|
2492
|
+
keywordsCited,
|
|
2493
|
+
keywordsFullyCovered,
|
|
2494
|
+
keywordsUncovered,
|
|
2495
|
+
latestRunId,
|
|
2496
|
+
latestRunAt
|
|
2497
|
+
};
|
|
2498
|
+
return {
|
|
2499
|
+
summary,
|
|
2500
|
+
byKeyword,
|
|
2501
|
+
competitorGaps,
|
|
2502
|
+
status: "ready"
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function normalizeDomain(domain) {
|
|
2506
|
+
return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2361
2509
|
// ../api-routes/src/composites.ts
|
|
2362
|
-
import { eq as
|
|
2510
|
+
import { eq as eq13, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
|
|
2363
2511
|
var TOP_INSIGHT_LIMIT = 5;
|
|
2364
2512
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
2365
2513
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
2366
2514
|
async function compositeRoutes(app) {
|
|
2367
2515
|
app.get("/projects/:name/overview", async (request, reply) => {
|
|
2368
2516
|
const project = resolveProject(app.db, request.params.name);
|
|
2369
|
-
const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(
|
|
2517
|
+
const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq13(runs.projectId, project.id)).get();
|
|
2370
2518
|
const totalRuns = totalRunsRow?.count ?? 0;
|
|
2371
|
-
const recentRuns = app.db.select().from(runs).where(
|
|
2519
|
+
const recentRuns = app.db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
|
|
2372
2520
|
const [latestRunRow, previousRunRow] = recentRuns;
|
|
2373
2521
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
2374
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
2522
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq13(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
|
|
2375
2523
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
2376
|
-
const insightRows = app.db.select().from(insights).where(
|
|
2524
|
+
const insightRows = app.db.select().from(insights).where(eq13(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
|
|
2377
2525
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
2378
2526
|
const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
|
|
2379
2527
|
const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
|
|
@@ -2409,9 +2557,9 @@ async function compositeRoutes(app) {
|
|
|
2409
2557
|
citedDomains: querySnapshots.citedDomains,
|
|
2410
2558
|
rawResponse: querySnapshots.rawResponse,
|
|
2411
2559
|
createdAt: querySnapshots.createdAt
|
|
2412
|
-
}).from(querySnapshots).innerJoin(keywords,
|
|
2560
|
+
}).from(querySnapshots).innerJoin(keywords, eq13(querySnapshots.keywordId, keywords.id)).where(
|
|
2413
2561
|
and3(
|
|
2414
|
-
|
|
2562
|
+
eq13(keywords.projectId, project.id),
|
|
2415
2563
|
or2(
|
|
2416
2564
|
sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
2417
2565
|
sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -2422,7 +2570,7 @@ async function compositeRoutes(app) {
|
|
|
2422
2570
|
).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
|
|
2423
2571
|
const insightMatches = app.db.select().from(insights).where(
|
|
2424
2572
|
and3(
|
|
2425
|
-
|
|
2573
|
+
eq13(insights.projectId, project.id),
|
|
2426
2574
|
or2(
|
|
2427
2575
|
like(insights.title, pattern),
|
|
2428
2576
|
like(insights.keyword, pattern),
|
|
@@ -2485,7 +2633,7 @@ function summarizeLatestRun(app, run) {
|
|
|
2485
2633
|
keywordId: querySnapshots.keywordId,
|
|
2486
2634
|
provider: querySnapshots.provider,
|
|
2487
2635
|
citationState: querySnapshots.citationState
|
|
2488
|
-
}).from(querySnapshots).where(
|
|
2636
|
+
}).from(querySnapshots).where(eq13(querySnapshots.runId, run.id)).all();
|
|
2489
2637
|
if (rows.length === 0) return empty;
|
|
2490
2638
|
const perKeyword = /* @__PURE__ */ new Map();
|
|
2491
2639
|
const perProvider = /* @__PURE__ */ new Map();
|
|
@@ -2524,7 +2672,7 @@ function summarizeTransitions(app, latest, previous) {
|
|
|
2524
2672
|
const rows = app.db.select({
|
|
2525
2673
|
keywordId: querySnapshots.keywordId,
|
|
2526
2674
|
citationState: querySnapshots.citationState
|
|
2527
|
-
}).from(querySnapshots).where(
|
|
2675
|
+
}).from(querySnapshots).where(eq13(querySnapshots.runId, runId)).all();
|
|
2528
2676
|
const map = /* @__PURE__ */ new Map();
|
|
2529
2677
|
for (const row of rows) {
|
|
2530
2678
|
const cited = row.citationState === "cited";
|
|
@@ -2681,16 +2829,16 @@ function makeSnippet(text, query) {
|
|
|
2681
2829
|
}
|
|
2682
2830
|
|
|
2683
2831
|
// ../api-routes/src/content-data.ts
|
|
2684
|
-
import { and as and4, eq as
|
|
2832
|
+
import { and as and4, eq as eq14, desc as desc6, inArray as inArray4 } from "drizzle-orm";
|
|
2685
2833
|
var RECENT_RUNS_WINDOW = 5;
|
|
2686
2834
|
function loadOrchestratorInput(db, project) {
|
|
2687
2835
|
const projectId = project.id;
|
|
2688
|
-
const ownDomain =
|
|
2836
|
+
const ownDomain = normalizeDomain2(project.canonicalDomain);
|
|
2689
2837
|
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
2690
|
-
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(
|
|
2838
|
+
const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain2)]);
|
|
2691
2839
|
const trackedKeywords = listKeywords(db, projectId);
|
|
2692
2840
|
const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
|
|
2693
|
-
const trackedCompetitors = listCompetitorDomains(db, projectId).map(
|
|
2841
|
+
const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain2);
|
|
2694
2842
|
const competitorSet = new Set(trackedCompetitors);
|
|
2695
2843
|
const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
|
|
2696
2844
|
const latestRunId = recentRunIds[0] ?? "";
|
|
@@ -2727,43 +2875,43 @@ function loadOrchestratorInput(db, project) {
|
|
|
2727
2875
|
};
|
|
2728
2876
|
}
|
|
2729
2877
|
function listKeywords(db, projectId) {
|
|
2730
|
-
const rows = db.select({ text: keywords.keyword }).from(keywords).where(
|
|
2878
|
+
const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, projectId)).all();
|
|
2731
2879
|
return rows.map((r) => r.text);
|
|
2732
2880
|
}
|
|
2733
2881
|
function listCompetitorDomains(db, projectId) {
|
|
2734
|
-
const rows = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
2882
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all();
|
|
2735
2883
|
return rows.map((r) => r.domain);
|
|
2736
2884
|
}
|
|
2737
2885
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
|
|
2738
2886
|
const rows = db.select({ id: runs.id }).from(runs).where(
|
|
2739
2887
|
and4(
|
|
2740
|
-
|
|
2741
|
-
|
|
2888
|
+
eq14(runs.projectId, projectId),
|
|
2889
|
+
eq14(runs.kind, RunKinds["answer-visibility"]),
|
|
2742
2890
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
2743
2891
|
// snapshots; including them risks pointing latestRunId at a run with
|
|
2744
2892
|
// no usable evidence.
|
|
2745
|
-
|
|
2893
|
+
inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2746
2894
|
)
|
|
2747
2895
|
).orderBy(desc6(runs.createdAt)).limit(limit).all();
|
|
2748
2896
|
return rows.map((r) => r.id);
|
|
2749
2897
|
}
|
|
2750
2898
|
function lookupRunTimestamp(db, runId) {
|
|
2751
|
-
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(
|
|
2899
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq14(runs.id, runId)).get();
|
|
2752
2900
|
return row?.createdAt ?? "";
|
|
2753
2901
|
}
|
|
2754
2902
|
function listGscPagesForProject(db, projectId) {
|
|
2755
|
-
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(
|
|
2903
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
|
|
2756
2904
|
return rows.map((r) => r.page);
|
|
2757
2905
|
}
|
|
2758
2906
|
function listGa4LandingPagesForProject(db, projectId) {
|
|
2759
|
-
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(
|
|
2907
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2760
2908
|
return rows.map((r) => r.landingPage);
|
|
2761
2909
|
}
|
|
2762
2910
|
function buildGaTrafficByPage(db, projectId) {
|
|
2763
2911
|
const rows = db.select({
|
|
2764
2912
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
2765
2913
|
sessions: gaTrafficSnapshots.sessions
|
|
2766
|
-
}).from(gaTrafficSnapshots).where(
|
|
2914
|
+
}).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
|
|
2767
2915
|
const map = /* @__PURE__ */ new Map();
|
|
2768
2916
|
for (const row of rows) {
|
|
2769
2917
|
const path15 = extractPath(row.landingPage);
|
|
@@ -2773,24 +2921,24 @@ function buildGaTrafficByPage(db, projectId) {
|
|
|
2773
2921
|
return map;
|
|
2774
2922
|
}
|
|
2775
2923
|
function sumAiReferralSessions(db, projectId) {
|
|
2776
|
-
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(
|
|
2924
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq14(gaAiReferrals.projectId, projectId)).all();
|
|
2777
2925
|
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
2778
2926
|
}
|
|
2779
2927
|
function buildCandidateQueries(opts) {
|
|
2780
2928
|
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
2781
2929
|
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
2782
2930
|
}
|
|
2783
|
-
const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(
|
|
2931
|
+
const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, opts.projectId)).all();
|
|
2784
2932
|
const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
|
|
2785
2933
|
const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
|
|
2786
|
-
const snapshotRows = opts.db.select().from(querySnapshots).where(
|
|
2934
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
|
|
2787
2935
|
const snapshotsByKeyword = /* @__PURE__ */ new Map();
|
|
2788
2936
|
for (const row of snapshotRows) {
|
|
2789
2937
|
const list = snapshotsByKeyword.get(row.keywordId) ?? [];
|
|
2790
2938
|
list.push(row);
|
|
2791
2939
|
snapshotsByKeyword.set(row.keywordId, list);
|
|
2792
2940
|
}
|
|
2793
|
-
const gscRows = opts.db.select().from(gscSearchData).where(
|
|
2941
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, opts.projectId)).all();
|
|
2794
2942
|
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
2795
2943
|
return opts.candidateQueryStrings.map((query) => {
|
|
2796
2944
|
const keywordId = keywordIdByText.get(query);
|
|
@@ -2854,13 +3002,13 @@ function aggregateCandidate(opts) {
|
|
|
2854
3002
|
const isLatestRun = snap.runId === opts.latestRunId;
|
|
2855
3003
|
const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
|
|
2856
3004
|
for (const domain of competitorOverlap) {
|
|
2857
|
-
const normalized =
|
|
3005
|
+
const normalized = normalizeDomain2(domain);
|
|
2858
3006
|
if (!opts.competitorSet.has(normalized)) continue;
|
|
2859
3007
|
competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
|
|
2860
3008
|
}
|
|
2861
3009
|
const grounding = extractGroundingSources(snap.rawResponse);
|
|
2862
3010
|
for (const g of grounding) {
|
|
2863
|
-
const domain =
|
|
3011
|
+
const domain = normalizeDomain2(extractHostFromUri(g.uri));
|
|
2864
3012
|
if (!domain) continue;
|
|
2865
3013
|
if (opts.ourDomains.has(domain)) {
|
|
2866
3014
|
if (isLatestRun) ourCitedInLatestRun = true;
|
|
@@ -2946,7 +3094,7 @@ function extractHostFromUri(uri) {
|
|
|
2946
3094
|
return "";
|
|
2947
3095
|
}
|
|
2948
3096
|
}
|
|
2949
|
-
function
|
|
3097
|
+
function normalizeDomain2(domain) {
|
|
2950
3098
|
return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
2951
3099
|
}
|
|
2952
3100
|
function extractPath(url) {
|
|
@@ -5237,6 +5385,18 @@ var routeCatalog = [
|
|
|
5237
5385
|
404: { description: "Project not found." }
|
|
5238
5386
|
}
|
|
5239
5387
|
},
|
|
5388
|
+
{
|
|
5389
|
+
method: "get",
|
|
5390
|
+
path: "/api/v1/projects/{name}/citations/visibility",
|
|
5391
|
+
summary: "Citation visibility headline (cited by N of M engines)",
|
|
5392
|
+
description: 'Single-call read for the AI citation surface. Returns project headline (`providersConfigured`/`providersCiting`/keyword coverage counts), per-keyword engine coverage rows from the latest snapshot per (keyword \xD7 provider), and a competitor-gap list (keywords where the project is not cited but a configured competitor is). Status `no-data` with `reason: "no-runs-yet"` or `"no-keywords"` when the project lacks the inputs.',
|
|
5393
|
+
tags: ["intelligence"],
|
|
5394
|
+
parameters: [nameParameter],
|
|
5395
|
+
responses: {
|
|
5396
|
+
200: { description: "Citation visibility report or no-data sentinel returned." },
|
|
5397
|
+
404: { description: "Project not found." }
|
|
5398
|
+
}
|
|
5399
|
+
},
|
|
5240
5400
|
// Content opportunity engine
|
|
5241
5401
|
{
|
|
5242
5402
|
method: "get",
|
|
@@ -5881,7 +6041,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
5881
6041
|
|
|
5882
6042
|
// ../api-routes/src/schedules.ts
|
|
5883
6043
|
import crypto11 from "crypto";
|
|
5884
|
-
import { eq as
|
|
6044
|
+
import { eq as eq15 } from "drizzle-orm";
|
|
5885
6045
|
async function scheduleRoutes(app, opts) {
|
|
5886
6046
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
5887
6047
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -5924,7 +6084,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5924
6084
|
}
|
|
5925
6085
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5926
6086
|
const enabledInt = enabled === false ? 0 : 1;
|
|
5927
|
-
const existing = app.db.select().from(schedules).where(
|
|
6087
|
+
const existing = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
|
|
5928
6088
|
if (existing) {
|
|
5929
6089
|
app.db.update(schedules).set({
|
|
5930
6090
|
cronExpr,
|
|
@@ -5933,7 +6093,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
5933
6093
|
providers: JSON.stringify(providers),
|
|
5934
6094
|
enabled: enabledInt,
|
|
5935
6095
|
updatedAt: now
|
|
5936
|
-
}).where(
|
|
6096
|
+
}).where(eq15(schedules.id, existing.id)).run();
|
|
5937
6097
|
} else {
|
|
5938
6098
|
app.db.insert(schedules).values({
|
|
5939
6099
|
id: crypto11.randomUUID(),
|
|
@@ -5955,12 +6115,12 @@ async function scheduleRoutes(app, opts) {
|
|
|
5955
6115
|
diff: { cronExpr, preset, timezone, providers }
|
|
5956
6116
|
});
|
|
5957
6117
|
opts.onScheduleUpdated?.("upsert", project.id);
|
|
5958
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6118
|
+
const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
|
|
5959
6119
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
5960
6120
|
});
|
|
5961
6121
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
5962
6122
|
const project = resolveProject(app.db, request.params.name);
|
|
5963
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6123
|
+
const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
|
|
5964
6124
|
if (!schedule) {
|
|
5965
6125
|
throw notFound("Schedule", request.params.name);
|
|
5966
6126
|
}
|
|
@@ -5968,11 +6128,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
5968
6128
|
});
|
|
5969
6129
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
5970
6130
|
const project = resolveProject(app.db, request.params.name);
|
|
5971
|
-
const schedule = app.db.select().from(schedules).where(
|
|
6131
|
+
const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
|
|
5972
6132
|
if (!schedule) {
|
|
5973
6133
|
throw notFound("Schedule", request.params.name);
|
|
5974
6134
|
}
|
|
5975
|
-
app.db.delete(schedules).where(
|
|
6135
|
+
app.db.delete(schedules).where(eq15(schedules.id, schedule.id)).run();
|
|
5976
6136
|
writeAuditLog(app.db, {
|
|
5977
6137
|
projectId: project.id,
|
|
5978
6138
|
actor: "api",
|
|
@@ -6002,7 +6162,7 @@ function formatSchedule(row) {
|
|
|
6002
6162
|
|
|
6003
6163
|
// ../api-routes/src/notifications.ts
|
|
6004
6164
|
import crypto12 from "crypto";
|
|
6005
|
-
import { eq as
|
|
6165
|
+
import { eq as eq16 } from "drizzle-orm";
|
|
6006
6166
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
6007
6167
|
async function notificationRoutes(app) {
|
|
6008
6168
|
app.get("/notifications/events", async (_request, reply) => {
|
|
@@ -6041,22 +6201,22 @@ async function notificationRoutes(app) {
|
|
|
6041
6201
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
6042
6202
|
});
|
|
6043
6203
|
return reply.status(201).send({
|
|
6044
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
6204
|
+
...formatNotification(app.db.select().from(notifications).where(eq16(notifications.id, id)).get()),
|
|
6045
6205
|
webhookSecret
|
|
6046
6206
|
});
|
|
6047
6207
|
});
|
|
6048
6208
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
6049
6209
|
const project = resolveProject(app.db, request.params.name);
|
|
6050
|
-
const rows = app.db.select().from(notifications).where(
|
|
6210
|
+
const rows = app.db.select().from(notifications).where(eq16(notifications.projectId, project.id)).all();
|
|
6051
6211
|
return reply.send(rows.map(formatNotification));
|
|
6052
6212
|
});
|
|
6053
6213
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
6054
6214
|
const project = resolveProject(app.db, request.params.name);
|
|
6055
|
-
const notification = app.db.select().from(notifications).where(
|
|
6215
|
+
const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
|
|
6056
6216
|
if (!notification || notification.projectId !== project.id) {
|
|
6057
6217
|
throw notFound("Notification", request.params.id);
|
|
6058
6218
|
}
|
|
6059
|
-
app.db.delete(notifications).where(
|
|
6219
|
+
app.db.delete(notifications).where(eq16(notifications.id, notification.id)).run();
|
|
6060
6220
|
writeAuditLog(app.db, {
|
|
6061
6221
|
projectId: project.id,
|
|
6062
6222
|
actor: "api",
|
|
@@ -6068,7 +6228,7 @@ async function notificationRoutes(app) {
|
|
|
6068
6228
|
});
|
|
6069
6229
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
6070
6230
|
const project = resolveProject(app.db, request.params.name);
|
|
6071
|
-
const notification = app.db.select().from(notifications).where(
|
|
6231
|
+
const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
|
|
6072
6232
|
if (!notification || notification.projectId !== project.id) {
|
|
6073
6233
|
throw notFound("Notification", request.params.id);
|
|
6074
6234
|
}
|
|
@@ -6121,7 +6281,7 @@ function formatNotification(row) {
|
|
|
6121
6281
|
|
|
6122
6282
|
// ../api-routes/src/google.ts
|
|
6123
6283
|
import crypto14 from "crypto";
|
|
6124
|
-
import { eq as
|
|
6284
|
+
import { eq as eq17, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
|
|
6125
6285
|
|
|
6126
6286
|
// ../integration-google/src/constants.ts
|
|
6127
6287
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -7256,14 +7416,14 @@ async function googleRoutes(app, opts) {
|
|
|
7256
7416
|
if (opts.onGscSyncRequested) {
|
|
7257
7417
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
7258
7418
|
}
|
|
7259
|
-
const run = app.db.select().from(runs).where(
|
|
7419
|
+
const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
|
|
7260
7420
|
return run;
|
|
7261
7421
|
});
|
|
7262
7422
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
7263
7423
|
const project = resolveProject(app.db, request.params.name);
|
|
7264
7424
|
const { startDate, endDate, query, page, limit } = request.query;
|
|
7265
7425
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
7266
|
-
const conditions = [
|
|
7426
|
+
const conditions = [eq17(gscSearchData.projectId, project.id)];
|
|
7267
7427
|
if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
|
|
7268
7428
|
else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
|
|
7269
7429
|
if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -7341,8 +7501,8 @@ async function googleRoutes(app, opts) {
|
|
|
7341
7501
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
7342
7502
|
const project = resolveProject(app.db, request.params.name);
|
|
7343
7503
|
const { url, limit } = request.query;
|
|
7344
|
-
const conditions = [
|
|
7345
|
-
if (url) conditions.push(
|
|
7504
|
+
const conditions = [eq17(gscUrlInspections.projectId, project.id)];
|
|
7505
|
+
if (url) conditions.push(eq17(gscUrlInspections.url, url));
|
|
7346
7506
|
const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
7347
7507
|
return rows.map((r) => ({
|
|
7348
7508
|
id: r.id,
|
|
@@ -7362,7 +7522,7 @@ async function googleRoutes(app, opts) {
|
|
|
7362
7522
|
});
|
|
7363
7523
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
7364
7524
|
const project = resolveProject(app.db, request.params.name);
|
|
7365
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7525
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
7366
7526
|
const byUrl = /* @__PURE__ */ new Map();
|
|
7367
7527
|
for (const row of allInspections) {
|
|
7368
7528
|
const existing = byUrl.get(row.url);
|
|
@@ -7390,7 +7550,7 @@ async function googleRoutes(app, opts) {
|
|
|
7390
7550
|
});
|
|
7391
7551
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
7392
7552
|
const project = resolveProject(app.db, request.params.name);
|
|
7393
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7553
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
7394
7554
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
7395
7555
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7396
7556
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -7487,7 +7647,7 @@ async function googleRoutes(app, opts) {
|
|
|
7487
7647
|
const project = resolveProject(app.db, request.params.name);
|
|
7488
7648
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
7489
7649
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
7490
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
7650
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
|
|
7491
7651
|
return rows.map((r) => ({
|
|
7492
7652
|
date: r.date,
|
|
7493
7653
|
indexed: r.indexed,
|
|
@@ -7547,7 +7707,7 @@ async function googleRoutes(app, opts) {
|
|
|
7547
7707
|
if (opts.onInspectSitemapRequested) {
|
|
7548
7708
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
7549
7709
|
}
|
|
7550
|
-
const run = app.db.select().from(runs).where(
|
|
7710
|
+
const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
|
|
7551
7711
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
7552
7712
|
});
|
|
7553
7713
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -7574,7 +7734,7 @@ async function googleRoutes(app, opts) {
|
|
|
7574
7734
|
if (opts.onInspectSitemapRequested) {
|
|
7575
7735
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
7576
7736
|
}
|
|
7577
|
-
const run = app.db.select().from(runs).where(
|
|
7737
|
+
const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
|
|
7578
7738
|
return run;
|
|
7579
7739
|
});
|
|
7580
7740
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -7621,7 +7781,7 @@ async function googleRoutes(app, opts) {
|
|
|
7621
7781
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
7622
7782
|
let urlsToNotify = request.body?.urls ?? [];
|
|
7623
7783
|
if (request.body?.allUnindexed) {
|
|
7624
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
7784
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
7625
7785
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7626
7786
|
for (const row of allInspections) {
|
|
7627
7787
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -7692,7 +7852,7 @@ async function googleRoutes(app, opts) {
|
|
|
7692
7852
|
|
|
7693
7853
|
// ../api-routes/src/bing.ts
|
|
7694
7854
|
import crypto15 from "crypto";
|
|
7695
|
-
import { eq as
|
|
7855
|
+
import { eq as eq18, and as and6, desc as desc8 } from "drizzle-orm";
|
|
7696
7856
|
|
|
7697
7857
|
// ../integration-bing/src/constants.ts
|
|
7698
7858
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -8005,7 +8165,7 @@ async function bingRoutes(app, opts) {
|
|
|
8005
8165
|
const store = requireConnectionStore();
|
|
8006
8166
|
const project = resolveProject(app.db, request.params.name);
|
|
8007
8167
|
requireConnection(store, project.canonicalDomain);
|
|
8008
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8168
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
|
|
8009
8169
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
8010
8170
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
8011
8171
|
for (const row of allInspections) {
|
|
@@ -8094,7 +8254,7 @@ async function bingRoutes(app, opts) {
|
|
|
8094
8254
|
const project = resolveProject(app.db, request.params.name);
|
|
8095
8255
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
8096
8256
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
8097
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
8257
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
|
|
8098
8258
|
return rows.map((r) => ({
|
|
8099
8259
|
date: r.date,
|
|
8100
8260
|
indexed: r.indexed,
|
|
@@ -8106,7 +8266,7 @@ async function bingRoutes(app, opts) {
|
|
|
8106
8266
|
requireConnectionStore();
|
|
8107
8267
|
const project = resolveProject(app.db, request.params.name);
|
|
8108
8268
|
const { url, limit } = request.query;
|
|
8109
|
-
const whereClause = url ? and6(
|
|
8269
|
+
const whereClause = url ? and6(eq18(bingUrlInspections.projectId, project.id), eq18(bingUrlInspections.url, url)) : eq18(bingUrlInspections.projectId, project.id);
|
|
8110
8270
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc8(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
8111
8271
|
return filtered.map((r) => ({
|
|
8112
8272
|
id: r.id,
|
|
@@ -8196,7 +8356,7 @@ async function bingRoutes(app, opts) {
|
|
|
8196
8356
|
anchorCount: result.AnchorCount ?? null,
|
|
8197
8357
|
discoveryDate
|
|
8198
8358
|
}).run();
|
|
8199
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8359
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
|
|
8200
8360
|
return {
|
|
8201
8361
|
id,
|
|
8202
8362
|
url,
|
|
@@ -8212,7 +8372,7 @@ async function bingRoutes(app, opts) {
|
|
|
8212
8372
|
} catch (e) {
|
|
8213
8373
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8214
8374
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
8215
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
8375
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
|
|
8216
8376
|
throw e;
|
|
8217
8377
|
}
|
|
8218
8378
|
});
|
|
@@ -8239,7 +8399,7 @@ async function bingRoutes(app, opts) {
|
|
|
8239
8399
|
} else {
|
|
8240
8400
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
8241
8401
|
}
|
|
8242
|
-
const run = app.db.select().from(runs).where(
|
|
8402
|
+
const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
|
|
8243
8403
|
return run;
|
|
8244
8404
|
});
|
|
8245
8405
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -8251,7 +8411,7 @@ async function bingRoutes(app, opts) {
|
|
|
8251
8411
|
}
|
|
8252
8412
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
8253
8413
|
if (request.body?.allUnindexed) {
|
|
8254
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
8414
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
|
|
8255
8415
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
8256
8416
|
for (const row of allInspections) {
|
|
8257
8417
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -8338,14 +8498,14 @@ async function bingRoutes(app, opts) {
|
|
|
8338
8498
|
import fs from "fs";
|
|
8339
8499
|
import path from "path";
|
|
8340
8500
|
import os from "os";
|
|
8341
|
-
import { eq as
|
|
8501
|
+
import { eq as eq19, and as and7 } from "drizzle-orm";
|
|
8342
8502
|
function getScreenshotDir() {
|
|
8343
8503
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
8344
8504
|
}
|
|
8345
8505
|
async function cdpRoutes(app, opts) {
|
|
8346
8506
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
8347
8507
|
const { snapshotId } = request.params;
|
|
8348
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
8508
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq19(querySnapshots.id, snapshotId)).get();
|
|
8349
8509
|
if (!snapshot?.screenshotPath) {
|
|
8350
8510
|
const err = notFound("Screenshot", snapshotId);
|
|
8351
8511
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8411,7 +8571,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8411
8571
|
async (request, reply) => {
|
|
8412
8572
|
const project = resolveProject(app.db, request.params.name);
|
|
8413
8573
|
const { runId } = request.params;
|
|
8414
|
-
const run = app.db.select().from(runs).where(and7(
|
|
8574
|
+
const run = app.db.select().from(runs).where(and7(eq19(runs.id, runId), eq19(runs.projectId, project.id))).get();
|
|
8415
8575
|
if (!run) {
|
|
8416
8576
|
const err = notFound("Run", runId);
|
|
8417
8577
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -8424,8 +8584,8 @@ async function cdpRoutes(app, opts) {
|
|
|
8424
8584
|
citedDomains: querySnapshots.citedDomains,
|
|
8425
8585
|
screenshotPath: querySnapshots.screenshotPath,
|
|
8426
8586
|
rawResponse: querySnapshots.rawResponse
|
|
8427
|
-
}).from(querySnapshots).where(
|
|
8428
|
-
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(
|
|
8587
|
+
}).from(querySnapshots).where(eq19(querySnapshots.runId, runId)).all();
|
|
8588
|
+
const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq19(keywords.projectId, project.id)).all();
|
|
8429
8589
|
const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
|
|
8430
8590
|
const byKeyword = /* @__PURE__ */ new Map();
|
|
8431
8591
|
for (const snap of snapshots) {
|
|
@@ -8508,7 +8668,7 @@ async function cdpRoutes(app, opts) {
|
|
|
8508
8668
|
|
|
8509
8669
|
// ../api-routes/src/ga.ts
|
|
8510
8670
|
import crypto16 from "crypto";
|
|
8511
|
-
import { eq as
|
|
8671
|
+
import { eq as eq20, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
|
|
8512
8672
|
function gaLog(level, action, ctx) {
|
|
8513
8673
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
8514
8674
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -8665,10 +8825,10 @@ async function ga4Routes(app, opts) {
|
|
|
8665
8825
|
if (!saConn && !oauthConn) {
|
|
8666
8826
|
throw notFound("GA4 connection", project.name);
|
|
8667
8827
|
}
|
|
8668
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
8669
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
8670
|
-
app.db.delete(gaAiReferrals).where(
|
|
8671
|
-
app.db.delete(gaSocialReferrals).where(
|
|
8828
|
+
app.db.delete(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).run();
|
|
8829
|
+
app.db.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
|
|
8830
|
+
app.db.delete(gaAiReferrals).where(eq20(gaAiReferrals.projectId, project.id)).run();
|
|
8831
|
+
app.db.delete(gaSocialReferrals).where(eq20(gaSocialReferrals.projectId, project.id)).run();
|
|
8672
8832
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
8673
8833
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
8674
8834
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -8689,7 +8849,7 @@ async function ga4Routes(app, opts) {
|
|
|
8689
8849
|
if (!connected) {
|
|
8690
8850
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
8691
8851
|
}
|
|
8692
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
8852
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8693
8853
|
return {
|
|
8694
8854
|
connected: true,
|
|
8695
8855
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -8749,7 +8909,7 @@ async function ga4Routes(app, opts) {
|
|
|
8749
8909
|
if (syncTraffic) {
|
|
8750
8910
|
tx.delete(gaTrafficSnapshots).where(
|
|
8751
8911
|
and8(
|
|
8752
|
-
|
|
8912
|
+
eq20(gaTrafficSnapshots.projectId, project.id),
|
|
8753
8913
|
sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
8754
8914
|
sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
8755
8915
|
)
|
|
@@ -8773,7 +8933,7 @@ async function ga4Routes(app, opts) {
|
|
|
8773
8933
|
if (syncAi) {
|
|
8774
8934
|
tx.delete(gaAiReferrals).where(
|
|
8775
8935
|
and8(
|
|
8776
|
-
|
|
8936
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
8777
8937
|
sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
8778
8938
|
sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
8779
8939
|
)
|
|
@@ -8796,7 +8956,7 @@ async function ga4Routes(app, opts) {
|
|
|
8796
8956
|
if (syncSocial) {
|
|
8797
8957
|
tx.delete(gaSocialReferrals).where(
|
|
8798
8958
|
and8(
|
|
8799
|
-
|
|
8959
|
+
eq20(gaSocialReferrals.projectId, project.id),
|
|
8800
8960
|
sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
8801
8961
|
sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
8802
8962
|
)
|
|
@@ -8817,7 +8977,7 @@ async function ga4Routes(app, opts) {
|
|
|
8817
8977
|
}
|
|
8818
8978
|
}
|
|
8819
8979
|
if (syncSummary) {
|
|
8820
|
-
tx.delete(gaTrafficSummaries).where(
|
|
8980
|
+
tx.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
|
|
8821
8981
|
tx.insert(gaTrafficSummaries).values({
|
|
8822
8982
|
id: crypto16.randomUUID(),
|
|
8823
8983
|
projectId: project.id,
|
|
@@ -8831,7 +8991,7 @@ async function ga4Routes(app, opts) {
|
|
|
8831
8991
|
}).run();
|
|
8832
8992
|
}
|
|
8833
8993
|
});
|
|
8834
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
8994
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq20(runs.id, runId)).run();
|
|
8835
8995
|
const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
|
|
8836
8996
|
gaLog("info", "sync.complete", {
|
|
8837
8997
|
projectId: project.id,
|
|
@@ -8855,7 +9015,7 @@ async function ga4Routes(app, opts) {
|
|
|
8855
9015
|
} catch (e) {
|
|
8856
9016
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8857
9017
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
8858
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
9018
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
|
|
8859
9019
|
throw e;
|
|
8860
9020
|
}
|
|
8861
9021
|
});
|
|
@@ -8866,11 +9026,11 @@ async function ga4Routes(app, opts) {
|
|
|
8866
9026
|
const window = parseWindow(request.query.window);
|
|
8867
9027
|
const cutoff = windowCutoff(window);
|
|
8868
9028
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
8869
|
-
const snapshotConditions = [
|
|
9029
|
+
const snapshotConditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
|
|
8870
9030
|
if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
8871
|
-
const aiConditions = [
|
|
9031
|
+
const aiConditions = [eq20(gaAiReferrals.projectId, project.id)];
|
|
8872
9032
|
if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8873
|
-
const socialConditions = [
|
|
9033
|
+
const socialConditions = [eq20(gaSocialReferrals.projectId, project.id)];
|
|
8874
9034
|
if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
8875
9035
|
const summaryRow = cutoffDate ? app.db.select({
|
|
8876
9036
|
totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
@@ -8880,14 +9040,14 @@ async function ga4Routes(app, opts) {
|
|
|
8880
9040
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
8881
9041
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
8882
9042
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
8883
|
-
}).from(gaTrafficSummaries).where(
|
|
9043
|
+
}).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
|
|
8884
9044
|
const directTotalRow = app.db.select({
|
|
8885
9045
|
totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
8886
9046
|
}).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get();
|
|
8887
9047
|
const summaryMeta = app.db.select({
|
|
8888
9048
|
periodStart: gaTrafficSummaries.periodStart,
|
|
8889
9049
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
8890
|
-
}).from(gaTrafficSummaries).where(
|
|
9050
|
+
}).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
|
|
8891
9051
|
const rows = app.db.select({
|
|
8892
9052
|
landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
8893
9053
|
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
@@ -8918,7 +9078,7 @@ async function ga4Routes(app, opts) {
|
|
|
8918
9078
|
const aiBySession = app.db.select({
|
|
8919
9079
|
sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
8920
9080
|
users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
8921
|
-
}).from(gaAiReferrals).where(and8(...aiConditions,
|
|
9081
|
+
}).from(gaAiReferrals).where(and8(...aiConditions, eq20(gaAiReferrals.sourceDimension, "session"))).get();
|
|
8922
9082
|
const socialReferrals = app.db.select({
|
|
8923
9083
|
source: gaSocialReferrals.source,
|
|
8924
9084
|
medium: gaSocialReferrals.medium,
|
|
@@ -8930,7 +9090,7 @@ async function ga4Routes(app, opts) {
|
|
|
8930
9090
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
|
|
8931
9091
|
users: sql5`SUM(${gaSocialReferrals.users})`
|
|
8932
9092
|
}).from(gaSocialReferrals).where(and8(...socialConditions)).get();
|
|
8933
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
9093
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
8934
9094
|
const total = summaryRow?.totalSessions ?? 0;
|
|
8935
9095
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
8936
9096
|
return {
|
|
@@ -8984,7 +9144,7 @@ async function ga4Routes(app, opts) {
|
|
|
8984
9144
|
const project = resolveProject(app.db, request.params.name);
|
|
8985
9145
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
8986
9146
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
8987
|
-
const conditions = [
|
|
9147
|
+
const conditions = [eq20(gaAiReferrals.projectId, project.id)];
|
|
8988
9148
|
if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
8989
9149
|
const rows = app.db.select({
|
|
8990
9150
|
date: gaAiReferrals.date,
|
|
@@ -9000,7 +9160,7 @@ async function ga4Routes(app, opts) {
|
|
|
9000
9160
|
const project = resolveProject(app.db, request.params.name);
|
|
9001
9161
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
9002
9162
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
9003
|
-
const conditions = [
|
|
9163
|
+
const conditions = [eq20(gaSocialReferrals.projectId, project.id)];
|
|
9004
9164
|
if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
9005
9165
|
const rows = app.db.select({
|
|
9006
9166
|
date: gaSocialReferrals.date,
|
|
@@ -9023,7 +9183,7 @@ async function ga4Routes(app, opts) {
|
|
|
9023
9183
|
return fmt(d);
|
|
9024
9184
|
};
|
|
9025
9185
|
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
|
|
9026
|
-
|
|
9186
|
+
eq20(gaSocialReferrals.projectId, project.id),
|
|
9027
9187
|
sql5`${gaSocialReferrals.date} >= ${from}`,
|
|
9028
9188
|
sql5`${gaSocialReferrals.date} < ${to}`
|
|
9029
9189
|
)).get();
|
|
@@ -9036,7 +9196,7 @@ async function ga4Routes(app, opts) {
|
|
|
9036
9196
|
source: gaSocialReferrals.source,
|
|
9037
9197
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
9038
9198
|
}).from(gaSocialReferrals).where(and8(
|
|
9039
|
-
|
|
9199
|
+
eq20(gaSocialReferrals.projectId, project.id),
|
|
9040
9200
|
sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
9041
9201
|
sql5`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
9042
9202
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -9044,7 +9204,7 @@ async function ga4Routes(app, opts) {
|
|
|
9044
9204
|
source: gaSocialReferrals.source,
|
|
9045
9205
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
9046
9206
|
}).from(gaSocialReferrals).where(and8(
|
|
9047
|
-
|
|
9207
|
+
eq20(gaSocialReferrals.projectId, project.id),
|
|
9048
9208
|
sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
9049
9209
|
sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
9050
9210
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -9085,16 +9245,16 @@ async function ga4Routes(app, opts) {
|
|
|
9085
9245
|
return fmt(d);
|
|
9086
9246
|
};
|
|
9087
9247
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
9088
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(
|
|
9089
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(
|
|
9090
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(
|
|
9248
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
9249
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
9250
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
9091
9251
|
const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
|
|
9092
|
-
|
|
9252
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9093
9253
|
sql5`${gaAiReferrals.date} >= ${from}`,
|
|
9094
9254
|
sql5`${gaAiReferrals.date} < ${to}`,
|
|
9095
|
-
|
|
9255
|
+
eq20(gaAiReferrals.sourceDimension, "session")
|
|
9096
9256
|
)).get();
|
|
9097
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
|
|
9257
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
|
|
9098
9258
|
const todayStr = fmt(today);
|
|
9099
9259
|
const buildTrend = (sum) => {
|
|
9100
9260
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -9104,16 +9264,16 @@ async function ga4Routes(app, opts) {
|
|
|
9104
9264
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
9105
9265
|
};
|
|
9106
9266
|
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
|
|
9107
|
-
|
|
9267
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9108
9268
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
|
|
9109
9269
|
sql5`${gaAiReferrals.date} < ${todayStr}`,
|
|
9110
|
-
|
|
9270
|
+
eq20(gaAiReferrals.sourceDimension, "session")
|
|
9111
9271
|
)).groupBy(gaAiReferrals.source).all();
|
|
9112
9272
|
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
|
|
9113
|
-
|
|
9273
|
+
eq20(gaAiReferrals.projectId, project.id),
|
|
9114
9274
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
|
|
9115
9275
|
sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
|
|
9116
|
-
|
|
9276
|
+
eq20(gaAiReferrals.sourceDimension, "session")
|
|
9117
9277
|
)).groupBy(gaAiReferrals.source).all();
|
|
9118
9278
|
const findBiggestMover = (current, prev) => {
|
|
9119
9279
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -9129,8 +9289,8 @@ async function ga4Routes(app, opts) {
|
|
|
9129
9289
|
}
|
|
9130
9290
|
return mover;
|
|
9131
9291
|
};
|
|
9132
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(
|
|
9133
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(
|
|
9292
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
9293
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
9134
9294
|
return {
|
|
9135
9295
|
total: buildTrend(sumTotal),
|
|
9136
9296
|
organic: buildTrend(sumOrganic),
|
|
@@ -9145,7 +9305,7 @@ async function ga4Routes(app, opts) {
|
|
|
9145
9305
|
const project = resolveProject(app.db, request.params.name);
|
|
9146
9306
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
9147
9307
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
9148
|
-
const conditions = [
|
|
9308
|
+
const conditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
|
|
9149
9309
|
if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
9150
9310
|
const rows = app.db.select({
|
|
9151
9311
|
date: gaTrafficSnapshots.date,
|
|
@@ -9168,7 +9328,7 @@ async function ga4Routes(app, opts) {
|
|
|
9168
9328
|
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
9169
9329
|
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
9170
9330
|
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
9171
|
-
}).from(gaTrafficSnapshots).where(
|
|
9331
|
+
}).from(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
9172
9332
|
return {
|
|
9173
9333
|
pages: trafficPages.map((r) => ({
|
|
9174
9334
|
landingPage: r.landingPage,
|
|
@@ -10805,7 +10965,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
10805
10965
|
|
|
10806
10966
|
// ../api-routes/src/backlinks.ts
|
|
10807
10967
|
import crypto18 from "crypto";
|
|
10808
|
-
import { and as and9, asc as asc2, desc as desc10, eq as
|
|
10968
|
+
import { and as and9, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
|
|
10809
10969
|
|
|
10810
10970
|
// ../integration-commoncrawl/src/constants.ts
|
|
10811
10971
|
import os2 from "os";
|
|
@@ -11257,7 +11417,7 @@ function mapRunRow(row) {
|
|
|
11257
11417
|
};
|
|
11258
11418
|
}
|
|
11259
11419
|
function latestSummaryForProject(db, projectId, release) {
|
|
11260
|
-
const condition = release ? and9(
|
|
11420
|
+
const condition = release ? and9(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
|
|
11261
11421
|
return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
|
|
11262
11422
|
}
|
|
11263
11423
|
async function backlinksRoutes(app, opts) {
|
|
@@ -11301,7 +11461,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
11301
11461
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
11302
11462
|
);
|
|
11303
11463
|
}
|
|
11304
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
11464
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.release, release)).get();
|
|
11305
11465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11306
11466
|
if (existing) {
|
|
11307
11467
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -11312,9 +11472,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
11312
11472
|
phaseDetail: null,
|
|
11313
11473
|
error: null,
|
|
11314
11474
|
updatedAt: now
|
|
11315
|
-
}).where(
|
|
11475
|
+
}).where(eq21(ccReleaseSyncs.id, existing.id)).run();
|
|
11316
11476
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
11317
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
11477
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, existing.id)).get();
|
|
11318
11478
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
11319
11479
|
}
|
|
11320
11480
|
const id = crypto18.randomUUID();
|
|
@@ -11326,7 +11486,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
11326
11486
|
updatedAt: now
|
|
11327
11487
|
}).run();
|
|
11328
11488
|
opts.onReleaseSyncRequested(id, release);
|
|
11329
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
11489
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, id)).get();
|
|
11330
11490
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
11331
11491
|
});
|
|
11332
11492
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -11384,7 +11544,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
11384
11544
|
createdAt: now
|
|
11385
11545
|
}).run();
|
|
11386
11546
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
11387
|
-
const run = app.db.select().from(runs).where(
|
|
11547
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
11388
11548
|
return reply.status(201).send(mapRunRow(run));
|
|
11389
11549
|
});
|
|
11390
11550
|
app.get(
|
|
@@ -11406,8 +11566,8 @@ async function backlinksRoutes(app, opts) {
|
|
|
11406
11566
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
11407
11567
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
11408
11568
|
const domainCondition = and9(
|
|
11409
|
-
|
|
11410
|
-
|
|
11569
|
+
eq21(backlinkDomains.projectId, project.id),
|
|
11570
|
+
eq21(backlinkDomains.release, targetRelease)
|
|
11411
11571
|
);
|
|
11412
11572
|
const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
11413
11573
|
const rows = app.db.select({
|
|
@@ -11425,7 +11585,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
11425
11585
|
"/projects/:name/backlinks/history",
|
|
11426
11586
|
async (request, reply) => {
|
|
11427
11587
|
const project = resolveProject(app.db, request.params.name);
|
|
11428
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
11588
|
+
const rows = app.db.select().from(backlinkSummaries).where(eq21(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
|
|
11429
11589
|
const response = rows.map((r) => ({
|
|
11430
11590
|
release: r.release,
|
|
11431
11591
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -11438,6 +11598,152 @@ async function backlinksRoutes(app, opts) {
|
|
|
11438
11598
|
);
|
|
11439
11599
|
}
|
|
11440
11600
|
|
|
11601
|
+
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
11602
|
+
var BING_AUTH_CHECKS = [
|
|
11603
|
+
{
|
|
11604
|
+
id: "bing.auth.connection",
|
|
11605
|
+
category: CheckCategories.auth,
|
|
11606
|
+
scope: CheckScopes.project,
|
|
11607
|
+
title: "Bing WMT connection",
|
|
11608
|
+
run: async (ctx) => {
|
|
11609
|
+
if (!ctx.project) {
|
|
11610
|
+
return {
|
|
11611
|
+
status: CheckStatuses.skipped,
|
|
11612
|
+
code: "bing.auth.no-project",
|
|
11613
|
+
summary: "Project context required.",
|
|
11614
|
+
remediation: null
|
|
11615
|
+
};
|
|
11616
|
+
}
|
|
11617
|
+
const store = ctx.bingConnectionStore;
|
|
11618
|
+
if (!store) {
|
|
11619
|
+
return {
|
|
11620
|
+
status: CheckStatuses.skipped,
|
|
11621
|
+
code: "bing.auth.store-unavailable",
|
|
11622
|
+
summary: "Bing connection store is not configured for this deployment.",
|
|
11623
|
+
remediation: null
|
|
11624
|
+
};
|
|
11625
|
+
}
|
|
11626
|
+
const conn = store.getConnection(ctx.project.canonicalDomain);
|
|
11627
|
+
if (!conn) {
|
|
11628
|
+
return {
|
|
11629
|
+
status: CheckStatuses.fail,
|
|
11630
|
+
code: "bing.auth.no-connection",
|
|
11631
|
+
summary: `No Bing connection for ${ctx.project.canonicalDomain}.`,
|
|
11632
|
+
remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to authorize.`
|
|
11633
|
+
};
|
|
11634
|
+
}
|
|
11635
|
+
if (!conn.apiKey) {
|
|
11636
|
+
return {
|
|
11637
|
+
status: CheckStatuses.fail,
|
|
11638
|
+
code: "bing.auth.no-api-key",
|
|
11639
|
+
summary: "Bing connection exists but has no API key stored.",
|
|
11640
|
+
remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to re-authorize.`
|
|
11641
|
+
};
|
|
11642
|
+
}
|
|
11643
|
+
try {
|
|
11644
|
+
await getSites(conn.apiKey);
|
|
11645
|
+
return {
|
|
11646
|
+
status: CheckStatuses.ok,
|
|
11647
|
+
code: "bing.auth.connected",
|
|
11648
|
+
summary: "Bing API key is valid and can list sites.",
|
|
11649
|
+
remediation: null
|
|
11650
|
+
};
|
|
11651
|
+
} catch (err) {
|
|
11652
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11653
|
+
return {
|
|
11654
|
+
status: CheckStatuses.fail,
|
|
11655
|
+
code: "bing.auth.verification-failed",
|
|
11656
|
+
summary: "Bing API key verification failed.",
|
|
11657
|
+
remediation: "Verify your Bing API key is correct and active in Bing Webmaster Tools.",
|
|
11658
|
+
details: { error: message }
|
|
11659
|
+
};
|
|
11660
|
+
}
|
|
11661
|
+
}
|
|
11662
|
+
},
|
|
11663
|
+
{
|
|
11664
|
+
id: "bing.auth.site-access",
|
|
11665
|
+
category: CheckCategories.auth,
|
|
11666
|
+
scope: CheckScopes.project,
|
|
11667
|
+
title: "Bing site access",
|
|
11668
|
+
run: async (ctx) => {
|
|
11669
|
+
if (!ctx.project) {
|
|
11670
|
+
return {
|
|
11671
|
+
status: CheckStatuses.skipped,
|
|
11672
|
+
code: "bing.auth.no-project",
|
|
11673
|
+
summary: "Project context required.",
|
|
11674
|
+
remediation: null
|
|
11675
|
+
};
|
|
11676
|
+
}
|
|
11677
|
+
const store = ctx.bingConnectionStore;
|
|
11678
|
+
if (!store) {
|
|
11679
|
+
return {
|
|
11680
|
+
status: CheckStatuses.skipped,
|
|
11681
|
+
code: "bing.auth.store-unavailable",
|
|
11682
|
+
summary: "Bing connection store is not configured.",
|
|
11683
|
+
remediation: null
|
|
11684
|
+
};
|
|
11685
|
+
}
|
|
11686
|
+
const conn = store.getConnection(ctx.project.canonicalDomain);
|
|
11687
|
+
if (!conn || !conn.apiKey) {
|
|
11688
|
+
return {
|
|
11689
|
+
status: CheckStatuses.skipped,
|
|
11690
|
+
code: "bing.auth.no-connection",
|
|
11691
|
+
summary: "Skipped \u2014 no Bing connection (see bing.auth.connection).",
|
|
11692
|
+
remediation: null
|
|
11693
|
+
};
|
|
11694
|
+
}
|
|
11695
|
+
if (!conn.siteUrl) {
|
|
11696
|
+
return {
|
|
11697
|
+
status: CheckStatuses.fail,
|
|
11698
|
+
code: "bing.auth.no-site-selected",
|
|
11699
|
+
summary: "Bing connection has no site URL selected.",
|
|
11700
|
+
remediation: `Run \`canonry bing sites ${ctx.project.name}\` to see available sites, then \`canonry bing set-site ${ctx.project.name} <url>\`.`
|
|
11701
|
+
};
|
|
11702
|
+
}
|
|
11703
|
+
try {
|
|
11704
|
+
const sites = await getSites(conn.apiKey);
|
|
11705
|
+
const match = sites.find((s) => s.Url === conn.siteUrl);
|
|
11706
|
+
if (!match) {
|
|
11707
|
+
return {
|
|
11708
|
+
status: CheckStatuses.fail,
|
|
11709
|
+
code: "bing.auth.site-not-found",
|
|
11710
|
+
summary: `Configured site "${conn.siteUrl}" is not in the authorized account's site list.`,
|
|
11711
|
+
remediation: `Add and verify "${conn.siteUrl}" in Bing Webmaster Tools, or pick an existing site using \`canonry bing set-site ${ctx.project.name}\`.`,
|
|
11712
|
+
details: {
|
|
11713
|
+
configuredSite: conn.siteUrl,
|
|
11714
|
+
availableSites: sites.map((s) => s.Url)
|
|
11715
|
+
}
|
|
11716
|
+
};
|
|
11717
|
+
}
|
|
11718
|
+
if (!match.Verified) {
|
|
11719
|
+
return {
|
|
11720
|
+
status: CheckStatuses.fail,
|
|
11721
|
+
code: "bing.auth.site-not-verified",
|
|
11722
|
+
summary: `Site "${conn.siteUrl}" is registered but not verified in Bing.`,
|
|
11723
|
+
remediation: "Complete site verification in Bing Webmaster Tools (DNS, HTML file, or Meta tag).",
|
|
11724
|
+
details: { siteUrl: conn.siteUrl }
|
|
11725
|
+
};
|
|
11726
|
+
}
|
|
11727
|
+
return {
|
|
11728
|
+
status: CheckStatuses.ok,
|
|
11729
|
+
code: "bing.auth.site-verified",
|
|
11730
|
+
summary: `Site "${conn.siteUrl}" is verified and accessible.`,
|
|
11731
|
+
remediation: null
|
|
11732
|
+
};
|
|
11733
|
+
} catch (err) {
|
|
11734
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11735
|
+
return {
|
|
11736
|
+
status: CheckStatuses.fail,
|
|
11737
|
+
code: "bing.auth.site-check-failed",
|
|
11738
|
+
summary: "Failed to verify Bing site access.",
|
|
11739
|
+
remediation: "Check Bing Webmaster Tools availability.",
|
|
11740
|
+
details: { error: message }
|
|
11741
|
+
};
|
|
11742
|
+
}
|
|
11743
|
+
}
|
|
11744
|
+
}
|
|
11745
|
+
];
|
|
11746
|
+
|
|
11441
11747
|
// ../api-routes/src/doctor/checks/ga-auth.ts
|
|
11442
11748
|
async function checkServiceAccount(conn) {
|
|
11443
11749
|
if (!conn.propertyId) {
|
|
@@ -11915,6 +12221,7 @@ var PROVIDERS_CHECKS = [providersConfiguredCheck];
|
|
|
11915
12221
|
// ../api-routes/src/doctor/registry.ts
|
|
11916
12222
|
var ALL_CHECKS = [
|
|
11917
12223
|
...GOOGLE_AUTH_CHECKS,
|
|
12224
|
+
...BING_AUTH_CHECKS,
|
|
11918
12225
|
...GA_AUTH_CHECKS,
|
|
11919
12226
|
...PROVIDERS_CHECKS
|
|
11920
12227
|
];
|
|
@@ -11999,6 +12306,7 @@ async function doctorRoutes(app, opts) {
|
|
|
11999
12306
|
db: app.db,
|
|
12000
12307
|
project: null,
|
|
12001
12308
|
googleConnectionStore: opts.googleConnectionStore,
|
|
12309
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
12002
12310
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
12003
12311
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
12004
12312
|
redirectUri,
|
|
@@ -12018,6 +12326,7 @@ async function doctorRoutes(app, opts) {
|
|
|
12018
12326
|
displayName: project.displayName
|
|
12019
12327
|
},
|
|
12020
12328
|
googleConnectionStore: opts.googleConnectionStore,
|
|
12329
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
12021
12330
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
12022
12331
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
12023
12332
|
redirectUri,
|
|
@@ -12087,6 +12396,7 @@ async function apiRoutes(app, opts) {
|
|
|
12087
12396
|
await api.register(historyRoutes);
|
|
12088
12397
|
await api.register(analyticsRoutes);
|
|
12089
12398
|
await api.register(intelligenceRoutes);
|
|
12399
|
+
await api.register(citationRoutes);
|
|
12090
12400
|
await api.register(compositeRoutes);
|
|
12091
12401
|
await api.register(contentRoutes);
|
|
12092
12402
|
await api.register(settingsRoutes, {
|
|
@@ -12149,6 +12459,7 @@ async function apiRoutes(app, opts) {
|
|
|
12149
12459
|
});
|
|
12150
12460
|
await api.register(doctorRoutes, {
|
|
12151
12461
|
googleConnectionStore: opts.googleConnectionStore,
|
|
12462
|
+
bingConnectionStore: opts.bingConnectionStore,
|
|
12152
12463
|
ga4CredentialStore: opts.ga4CredentialStore,
|
|
12153
12464
|
getGoogleAuthConfig: opts.getGoogleAuthConfig,
|
|
12154
12465
|
publicUrl: opts.publicUrl,
|
|
@@ -14618,7 +14929,7 @@ import crypto19 from "crypto";
|
|
|
14618
14929
|
import fs7 from "fs";
|
|
14619
14930
|
import path9 from "path";
|
|
14620
14931
|
import os4 from "os";
|
|
14621
|
-
import { and as and10, eq as
|
|
14932
|
+
import { and as and10, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
|
|
14622
14933
|
|
|
14623
14934
|
// src/citation-utils.ts
|
|
14624
14935
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -14870,11 +15181,11 @@ var JobRunner = class {
|
|
|
14870
15181
|
this.registry = registry;
|
|
14871
15182
|
}
|
|
14872
15183
|
recoverStaleRuns() {
|
|
14873
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
15184
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray5(runs.status, ["running", "queued"])).all();
|
|
14874
15185
|
if (stale.length === 0) return;
|
|
14875
15186
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14876
15187
|
for (const run of stale) {
|
|
14877
|
-
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(
|
|
15188
|
+
this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq22(runs.id, run.id)).run();
|
|
14878
15189
|
log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
|
|
14879
15190
|
}
|
|
14880
15191
|
}
|
|
@@ -14902,10 +15213,10 @@ var JobRunner = class {
|
|
|
14902
15213
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
14903
15214
|
}
|
|
14904
15215
|
if (existingRun.status === "queued") {
|
|
14905
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(
|
|
15216
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
|
|
14906
15217
|
}
|
|
14907
15218
|
this.throwIfRunCancelled(runId);
|
|
14908
|
-
const project = this.db.select().from(projects).where(
|
|
15219
|
+
const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
14909
15220
|
if (!project) {
|
|
14910
15221
|
throw new Error(`Project ${projectId} not found`);
|
|
14911
15222
|
}
|
|
@@ -14925,8 +15236,8 @@ var JobRunner = class {
|
|
|
14925
15236
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
14926
15237
|
}
|
|
14927
15238
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
14928
|
-
projectKeywords = this.db.select().from(keywords).where(
|
|
14929
|
-
const projectCompetitors = this.db.select().from(competitors).where(
|
|
15239
|
+
projectKeywords = this.db.select().from(keywords).where(eq22(keywords.projectId, projectId)).all();
|
|
15240
|
+
const projectCompetitors = this.db.select().from(competitors).where(eq22(competitors.projectId, projectId)).all();
|
|
14930
15241
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
14931
15242
|
const allDomains = effectiveDomains({
|
|
14932
15243
|
canonicalDomain: project.canonicalDomain,
|
|
@@ -14942,7 +15253,7 @@ var JobRunner = class {
|
|
|
14942
15253
|
const todayPeriod = getCurrentUsageDay();
|
|
14943
15254
|
for (const p of activeProviders) {
|
|
14944
15255
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
14945
|
-
const providerUsage = this.db.select().from(usageCounters).where(
|
|
15256
|
+
const providerUsage = this.db.select().from(usageCounters).where(eq22(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
14946
15257
|
const limit = p.config.quotaPolicy.maxRequestsPerDay;
|
|
14947
15258
|
if (providerUsage + queriesPerProvider > limit) {
|
|
14948
15259
|
throw new Error(
|
|
@@ -15083,12 +15394,12 @@ var JobRunner = class {
|
|
|
15083
15394
|
const someFailed = providerErrors.size > 0;
|
|
15084
15395
|
if (allFailed) {
|
|
15085
15396
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
15086
|
-
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
15397
|
+
this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
|
|
15087
15398
|
} else if (someFailed) {
|
|
15088
15399
|
const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
|
|
15089
|
-
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(
|
|
15400
|
+
this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
|
|
15090
15401
|
} else {
|
|
15091
|
-
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15402
|
+
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
|
|
15092
15403
|
}
|
|
15093
15404
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
15094
15405
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
@@ -15123,7 +15434,7 @@ var JobRunner = class {
|
|
|
15123
15434
|
status: "failed",
|
|
15124
15435
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15125
15436
|
error: errorMessage
|
|
15126
|
-
}).where(
|
|
15437
|
+
}).where(eq22(runs.id, runId)).run();
|
|
15127
15438
|
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
15128
15439
|
trackEvent("run.completed", {
|
|
15129
15440
|
status: "failed",
|
|
@@ -15166,7 +15477,7 @@ var JobRunner = class {
|
|
|
15166
15477
|
status: runs.status,
|
|
15167
15478
|
finishedAt: runs.finishedAt,
|
|
15168
15479
|
error: runs.error
|
|
15169
|
-
}).from(runs).where(
|
|
15480
|
+
}).from(runs).where(eq22(runs.id, runId)).get();
|
|
15170
15481
|
}
|
|
15171
15482
|
isRunCancelled(runId) {
|
|
15172
15483
|
return this.getRunState(runId)?.status === "cancelled";
|
|
@@ -15182,7 +15493,7 @@ var JobRunner = class {
|
|
|
15182
15493
|
this.db.update(runs).set({
|
|
15183
15494
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15184
15495
|
error: currentRun.error ?? "Cancelled by user"
|
|
15185
|
-
}).where(
|
|
15496
|
+
}).where(eq22(runs.id, runId)).run();
|
|
15186
15497
|
}
|
|
15187
15498
|
trackEvent("run.completed", {
|
|
15188
15499
|
status: "cancelled",
|
|
@@ -15205,7 +15516,7 @@ function getCurrentUsageDay() {
|
|
|
15205
15516
|
|
|
15206
15517
|
// src/gsc-sync.ts
|
|
15207
15518
|
import crypto20 from "crypto";
|
|
15208
|
-
import { eq as
|
|
15519
|
+
import { eq as eq23, and as and11, sql as sql8 } from "drizzle-orm";
|
|
15209
15520
|
var log2 = createLogger("GscSync");
|
|
15210
15521
|
function formatDate2(d) {
|
|
15211
15522
|
return d.toISOString().split("T")[0];
|
|
@@ -15217,13 +15528,13 @@ function daysAgo(n) {
|
|
|
15217
15528
|
}
|
|
15218
15529
|
async function executeGscSync(db, runId, projectId, opts) {
|
|
15219
15530
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15220
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
15531
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
|
|
15221
15532
|
try {
|
|
15222
15533
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
15223
15534
|
if (!googleClientId || !googleClientSecret) {
|
|
15224
15535
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
15225
15536
|
}
|
|
15226
|
-
const project = db.select().from(projects).where(
|
|
15537
|
+
const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
15227
15538
|
if (!project) {
|
|
15228
15539
|
throw new Error(`Project not found: ${projectId}`);
|
|
15229
15540
|
}
|
|
@@ -15258,7 +15569,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
15258
15569
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
15259
15570
|
db.delete(gscSearchData).where(
|
|
15260
15571
|
and11(
|
|
15261
|
-
|
|
15572
|
+
eq23(gscSearchData.projectId, projectId),
|
|
15262
15573
|
sql8`${gscSearchData.date} >= ${startDate}`,
|
|
15263
15574
|
sql8`${gscSearchData.date} <= ${endDate}`
|
|
15264
15575
|
)
|
|
@@ -15325,7 +15636,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
15325
15636
|
log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
|
|
15326
15637
|
}
|
|
15327
15638
|
}
|
|
15328
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
15639
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
|
|
15329
15640
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
15330
15641
|
for (const row of allInspections) {
|
|
15331
15642
|
const existing = latestByUrl.get(row.url);
|
|
@@ -15346,7 +15657,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
15346
15657
|
}
|
|
15347
15658
|
}
|
|
15348
15659
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
15349
|
-
db.delete(gscCoverageSnapshots).where(and11(
|
|
15660
|
+
db.delete(gscCoverageSnapshots).where(and11(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
15350
15661
|
db.insert(gscCoverageSnapshots).values({
|
|
15351
15662
|
id: crypto20.randomUUID(),
|
|
15352
15663
|
projectId,
|
|
@@ -15357,11 +15668,11 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
15357
15668
|
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
15358
15669
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15359
15670
|
}).run();
|
|
15360
|
-
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15671
|
+
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
15361
15672
|
log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
15362
15673
|
} catch (err) {
|
|
15363
15674
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
15364
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15675
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
15365
15676
|
log2.error("sync.failed", { runId, projectId, error: errorMsg });
|
|
15366
15677
|
throw err;
|
|
15367
15678
|
}
|
|
@@ -15369,7 +15680,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
15369
15680
|
|
|
15370
15681
|
// src/gsc-inspect-sitemap.ts
|
|
15371
15682
|
import crypto21 from "crypto";
|
|
15372
|
-
import { eq as
|
|
15683
|
+
import { eq as eq24, and as and12 } from "drizzle-orm";
|
|
15373
15684
|
|
|
15374
15685
|
// src/sitemap-parser.ts
|
|
15375
15686
|
var log3 = createLogger("SitemapParser");
|
|
@@ -15490,13 +15801,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
|
|
|
15490
15801
|
var log4 = createLogger("InspectSitemap");
|
|
15491
15802
|
async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
15492
15803
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15493
|
-
db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
15804
|
+
db.update(runs).set({ status: "running", startedAt: now }).where(eq24(runs.id, runId)).run();
|
|
15494
15805
|
try {
|
|
15495
15806
|
const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
|
|
15496
15807
|
if (!googleClientId || !googleClientSecret) {
|
|
15497
15808
|
throw new Error("Google OAuth is not configured in the local Canonry config");
|
|
15498
15809
|
}
|
|
15499
|
-
const project = db.select().from(projects).where(
|
|
15810
|
+
const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
15500
15811
|
if (!project) {
|
|
15501
15812
|
throw new Error(`Project not found: ${projectId}`);
|
|
15502
15813
|
}
|
|
@@ -15564,7 +15875,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
15564
15875
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
15565
15876
|
}
|
|
15566
15877
|
}
|
|
15567
|
-
const allInspections = db.select().from(gscUrlInspections).where(
|
|
15878
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, projectId)).all();
|
|
15568
15879
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
15569
15880
|
for (const row of allInspections) {
|
|
15570
15881
|
const existing = latestByUrl.get(row.url);
|
|
@@ -15585,7 +15896,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
15585
15896
|
}
|
|
15586
15897
|
}
|
|
15587
15898
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
15588
|
-
db.delete(gscCoverageSnapshots).where(and12(
|
|
15899
|
+
db.delete(gscCoverageSnapshots).where(and12(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
15589
15900
|
db.insert(gscCoverageSnapshots).values({
|
|
15590
15901
|
id: crypto21.randomUUID(),
|
|
15591
15902
|
projectId,
|
|
@@ -15597,11 +15908,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
15597
15908
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
15598
15909
|
}).run();
|
|
15599
15910
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
15600
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15911
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
15601
15912
|
log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
|
|
15602
15913
|
} catch (err) {
|
|
15603
15914
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
15604
|
-
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
15915
|
+
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
15605
15916
|
log4.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
15606
15917
|
throw err;
|
|
15607
15918
|
}
|
|
@@ -15609,7 +15920,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
15609
15920
|
|
|
15610
15921
|
// src/bing-inspect-sitemap.ts
|
|
15611
15922
|
import crypto22 from "crypto";
|
|
15612
|
-
import { eq as
|
|
15923
|
+
import { eq as eq25, desc as desc11 } from "drizzle-orm";
|
|
15613
15924
|
var log5 = createLogger("BingInspectSitemap");
|
|
15614
15925
|
function parseBingDate2(value) {
|
|
15615
15926
|
if (!value) return null;
|
|
@@ -15627,9 +15938,9 @@ function isBlockingIssueType2(issueType) {
|
|
|
15627
15938
|
}
|
|
15628
15939
|
async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
15629
15940
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15630
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
15941
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
|
|
15631
15942
|
try {
|
|
15632
|
-
const project = db.select().from(projects).where(
|
|
15943
|
+
const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
|
|
15633
15944
|
if (!project) {
|
|
15634
15945
|
throw new Error(`Project not found: ${projectId}`);
|
|
15635
15946
|
}
|
|
@@ -15647,7 +15958,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
15647
15958
|
if (sitemapUrls.length === 0) {
|
|
15648
15959
|
throw new Error("No URLs found in sitemap");
|
|
15649
15960
|
}
|
|
15650
|
-
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(
|
|
15961
|
+
const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).all();
|
|
15651
15962
|
const trackedUrls = new Set(trackedRows.map((r) => r.url));
|
|
15652
15963
|
const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
|
|
15653
15964
|
log5.info("sitemap.diff", {
|
|
@@ -15730,7 +16041,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
15730
16041
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
15731
16042
|
}
|
|
15732
16043
|
}
|
|
15733
|
-
const allInspections = db.select().from(bingUrlInspections).where(
|
|
16044
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
|
|
15734
16045
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
15735
16046
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
15736
16047
|
for (const row of allInspections) {
|
|
@@ -15773,7 +16084,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
15773
16084
|
}
|
|
15774
16085
|
}).run();
|
|
15775
16086
|
const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
|
|
15776
|
-
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16087
|
+
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
15777
16088
|
log5.info("inspect.completed", {
|
|
15778
16089
|
runId,
|
|
15779
16090
|
projectId,
|
|
@@ -15787,7 +16098,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
15787
16098
|
});
|
|
15788
16099
|
} catch (err) {
|
|
15789
16100
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
15790
|
-
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
16101
|
+
db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
15791
16102
|
log5.error("inspect.failed", { runId, projectId, error: errorMsg });
|
|
15792
16103
|
throw err;
|
|
15793
16104
|
}
|
|
@@ -15796,7 +16107,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
15796
16107
|
// src/commoncrawl-sync.ts
|
|
15797
16108
|
import crypto23 from "crypto";
|
|
15798
16109
|
import path10 from "path";
|
|
15799
|
-
import { and as and13, eq as
|
|
16110
|
+
import { and as and13, eq as eq26, sql as sql9 } from "drizzle-orm";
|
|
15800
16111
|
var log6 = createLogger("CommonCrawlSync");
|
|
15801
16112
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
15802
16113
|
function defaultDeps() {
|
|
@@ -15822,7 +16133,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
15822
16133
|
phaseDetail: "downloading vertices + edges",
|
|
15823
16134
|
updatedAt: downloadStartedAt,
|
|
15824
16135
|
error: null
|
|
15825
|
-
}).where(
|
|
16136
|
+
}).where(eq26(ccReleaseSyncs.id, syncId)).run();
|
|
15826
16137
|
const paths = ccReleasePaths(release);
|
|
15827
16138
|
const releaseCacheDir = path10.join(deps.cacheDir, release);
|
|
15828
16139
|
const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
|
|
@@ -15845,7 +16156,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
15845
16156
|
vertexSha256: vertex.sha256,
|
|
15846
16157
|
edgesSha256: edges.sha256,
|
|
15847
16158
|
updatedAt: downloadFinishedAt
|
|
15848
|
-
}).where(
|
|
16159
|
+
}).where(eq26(ccReleaseSyncs.id, syncId)).run();
|
|
15849
16160
|
const allProjects = db.select().from(projects).all();
|
|
15850
16161
|
const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
|
|
15851
16162
|
let rows = [];
|
|
@@ -15861,8 +16172,8 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
15861
16172
|
}
|
|
15862
16173
|
const queriedAt = deps.now().toISOString();
|
|
15863
16174
|
db.transaction((tx) => {
|
|
15864
|
-
tx.delete(backlinkDomains).where(
|
|
15865
|
-
tx.delete(backlinkSummaries).where(
|
|
16175
|
+
tx.delete(backlinkDomains).where(eq26(backlinkDomains.releaseSyncId, syncId)).run();
|
|
16176
|
+
tx.delete(backlinkSummaries).where(eq26(backlinkSummaries.releaseSyncId, syncId)).run();
|
|
15866
16177
|
const expanded = [];
|
|
15867
16178
|
for (const r of rows) {
|
|
15868
16179
|
const projectIds = projectsByDomain.get(r.targetDomain);
|
|
@@ -15921,7 +16232,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
15921
16232
|
domainsDiscovered: rows.length,
|
|
15922
16233
|
updatedAt: finishedAt,
|
|
15923
16234
|
error: null
|
|
15924
|
-
}).where(
|
|
16235
|
+
}).where(eq26(ccReleaseSyncs.id, syncId)).run();
|
|
15925
16236
|
log6.info("sync.completed", {
|
|
15926
16237
|
syncId,
|
|
15927
16238
|
release,
|
|
@@ -15951,7 +16262,7 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
15951
16262
|
error: errorMsg,
|
|
15952
16263
|
phaseDetail: null,
|
|
15953
16264
|
updatedAt: finishedAt
|
|
15954
|
-
}).where(
|
|
16265
|
+
}).where(eq26(ccReleaseSyncs.id, syncId)).run();
|
|
15955
16266
|
log6.error("sync.failed", { syncId, release, error: errorMsg });
|
|
15956
16267
|
throw err;
|
|
15957
16268
|
}
|
|
@@ -15987,7 +16298,7 @@ function computeSummary(rows) {
|
|
|
15987
16298
|
// src/backlink-extract.ts
|
|
15988
16299
|
import crypto24 from "crypto";
|
|
15989
16300
|
import fs8 from "fs";
|
|
15990
|
-
import { and as and14, desc as desc12, eq as
|
|
16301
|
+
import { and as and14, desc as desc12, eq as eq27 } from "drizzle-orm";
|
|
15991
16302
|
var log7 = createLogger("BacklinkExtract");
|
|
15992
16303
|
function defaultDeps2() {
|
|
15993
16304
|
return {
|
|
@@ -15999,13 +16310,13 @@ function defaultDeps2() {
|
|
|
15999
16310
|
async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
16000
16311
|
const deps = { ...defaultDeps2(), ...opts.deps };
|
|
16001
16312
|
const startedAt = deps.now().toISOString();
|
|
16002
|
-
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(
|
|
16313
|
+
db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq27(runs.id, runId)).run();
|
|
16003
16314
|
try {
|
|
16004
|
-
const project = db.select().from(projects).where(
|
|
16315
|
+
const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
16005
16316
|
if (!project) {
|
|
16006
16317
|
throw new Error(`Project not found: ${projectId}`);
|
|
16007
16318
|
}
|
|
16008
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(
|
|
16319
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
16009
16320
|
if (!sync) {
|
|
16010
16321
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
16011
16322
|
}
|
|
@@ -16033,7 +16344,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
16033
16344
|
const targetDomain = project.canonicalDomain;
|
|
16034
16345
|
db.transaction((tx) => {
|
|
16035
16346
|
tx.delete(backlinkDomains).where(
|
|
16036
|
-
and14(
|
|
16347
|
+
and14(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
|
|
16037
16348
|
).run();
|
|
16038
16349
|
if (rows.length > 0) {
|
|
16039
16350
|
const values = rows.map((r) => ({
|
|
@@ -16073,7 +16384,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
16073
16384
|
}).run();
|
|
16074
16385
|
});
|
|
16075
16386
|
const finishedAt = deps.now().toISOString();
|
|
16076
|
-
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
16387
|
+
db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
|
|
16077
16388
|
log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
|
|
16078
16389
|
} catch (err) {
|
|
16079
16390
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -16082,7 +16393,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
16082
16393
|
status: RunStatuses.failed,
|
|
16083
16394
|
error: errorMsg,
|
|
16084
16395
|
finishedAt
|
|
16085
|
-
}).where(
|
|
16396
|
+
}).where(eq27(runs.id, runId)).run();
|
|
16086
16397
|
log7.error("extract.failed", { runId, projectId, error: errorMsg });
|
|
16087
16398
|
throw err;
|
|
16088
16399
|
}
|
|
@@ -16155,7 +16466,7 @@ var ProviderRegistry = class {
|
|
|
16155
16466
|
|
|
16156
16467
|
// src/scheduler.ts
|
|
16157
16468
|
import cron from "node-cron";
|
|
16158
|
-
import { eq as
|
|
16469
|
+
import { eq as eq28 } from "drizzle-orm";
|
|
16159
16470
|
var log8 = createLogger("Scheduler");
|
|
16160
16471
|
var Scheduler = class {
|
|
16161
16472
|
db;
|
|
@@ -16167,7 +16478,7 @@ var Scheduler = class {
|
|
|
16167
16478
|
}
|
|
16168
16479
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
16169
16480
|
start() {
|
|
16170
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
16481
|
+
const allSchedules = this.db.select().from(schedules).where(eq28(schedules.enabled, 1)).all();
|
|
16171
16482
|
for (const schedule of allSchedules) {
|
|
16172
16483
|
const missedRunAt = schedule.nextRunAt;
|
|
16173
16484
|
this.registerCronTask(schedule);
|
|
@@ -16192,7 +16503,7 @@ var Scheduler = class {
|
|
|
16192
16503
|
this.stopTask(projectId, existing, "Stopped");
|
|
16193
16504
|
this.tasks.delete(projectId);
|
|
16194
16505
|
}
|
|
16195
|
-
const schedule = this.db.select().from(schedules).where(
|
|
16506
|
+
const schedule = this.db.select().from(schedules).where(eq28(schedules.projectId, projectId)).get();
|
|
16196
16507
|
if (schedule && schedule.enabled === 1) {
|
|
16197
16508
|
this.registerCronTask(schedule);
|
|
16198
16509
|
}
|
|
@@ -16225,14 +16536,14 @@ var Scheduler = class {
|
|
|
16225
16536
|
this.db.update(schedules).set({
|
|
16226
16537
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
16227
16538
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16228
|
-
}).where(
|
|
16539
|
+
}).where(eq28(schedules.id, scheduleId)).run();
|
|
16229
16540
|
const label = schedule.preset ?? cronExpr;
|
|
16230
16541
|
log8.info("cron.registered", { projectId, schedule: label, timezone });
|
|
16231
16542
|
}
|
|
16232
16543
|
triggerRun(scheduleId, projectId) {
|
|
16233
16544
|
try {
|
|
16234
16545
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16235
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
16546
|
+
const currentSchedule = this.db.select().from(schedules).where(eq28(schedules.id, scheduleId)).get();
|
|
16236
16547
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
16237
16548
|
log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
16238
16549
|
this.remove(projectId);
|
|
@@ -16240,7 +16551,7 @@ var Scheduler = class {
|
|
|
16240
16551
|
}
|
|
16241
16552
|
const task = this.tasks.get(projectId);
|
|
16242
16553
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
16243
|
-
const project = this.db.select().from(projects).where(
|
|
16554
|
+
const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
|
|
16244
16555
|
if (!project) {
|
|
16245
16556
|
log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
16246
16557
|
this.remove(projectId);
|
|
@@ -16269,7 +16580,7 @@ var Scheduler = class {
|
|
|
16269
16580
|
this.db.update(schedules).set({
|
|
16270
16581
|
nextRunAt,
|
|
16271
16582
|
updatedAt: now
|
|
16272
|
-
}).where(
|
|
16583
|
+
}).where(eq28(schedules.id, currentSchedule.id)).run();
|
|
16273
16584
|
return;
|
|
16274
16585
|
}
|
|
16275
16586
|
const runId = queueResult.runId;
|
|
@@ -16277,7 +16588,7 @@ var Scheduler = class {
|
|
|
16277
16588
|
lastRunAt: now,
|
|
16278
16589
|
nextRunAt,
|
|
16279
16590
|
updatedAt: now
|
|
16280
|
-
}).where(
|
|
16591
|
+
}).where(eq28(schedules.id, currentSchedule.id)).run();
|
|
16281
16592
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
16282
16593
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
16283
16594
|
log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -16289,7 +16600,7 @@ var Scheduler = class {
|
|
|
16289
16600
|
};
|
|
16290
16601
|
|
|
16291
16602
|
// src/notifier.ts
|
|
16292
|
-
import { eq as
|
|
16603
|
+
import { eq as eq29, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
|
|
16293
16604
|
import crypto25 from "crypto";
|
|
16294
16605
|
var log9 = createLogger("Notifier");
|
|
16295
16606
|
var Notifier = class {
|
|
@@ -16302,18 +16613,18 @@ var Notifier = class {
|
|
|
16302
16613
|
/** Called after a run completes (success, partial, or failed). */
|
|
16303
16614
|
async onRunCompleted(runId, projectId) {
|
|
16304
16615
|
log9.info("run.completed", { runId, projectId });
|
|
16305
|
-
const notifs = this.db.select().from(notifications).where(
|
|
16616
|
+
const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
16306
16617
|
if (notifs.length === 0) {
|
|
16307
16618
|
log9.info("notifications.none-enabled", { projectId });
|
|
16308
16619
|
return;
|
|
16309
16620
|
}
|
|
16310
16621
|
log9.info("notifications.found", { projectId, count: notifs.length });
|
|
16311
|
-
const run = this.db.select().from(runs).where(
|
|
16622
|
+
const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
|
|
16312
16623
|
if (!run) {
|
|
16313
16624
|
log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
16314
16625
|
return;
|
|
16315
16626
|
}
|
|
16316
|
-
const project = this.db.select().from(projects).where(
|
|
16627
|
+
const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
|
|
16317
16628
|
if (!project) {
|
|
16318
16629
|
log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
16319
16630
|
return;
|
|
@@ -16360,11 +16671,11 @@ var Notifier = class {
|
|
|
16360
16671
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
16361
16672
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
16362
16673
|
if (insightEvents.length === 0) return;
|
|
16363
|
-
const notifs = this.db.select().from(notifications).where(
|
|
16674
|
+
const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
16364
16675
|
if (notifs.length === 0) return;
|
|
16365
|
-
const run = this.db.select().from(runs).where(
|
|
16676
|
+
const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
|
|
16366
16677
|
if (!run) return;
|
|
16367
|
-
const project = this.db.select().from(projects).where(
|
|
16678
|
+
const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
|
|
16368
16679
|
if (!project) return;
|
|
16369
16680
|
for (const notif of notifs) {
|
|
16370
16681
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -16396,8 +16707,8 @@ var Notifier = class {
|
|
|
16396
16707
|
computeTransitions(runId, projectId) {
|
|
16397
16708
|
const recentRuns = this.db.select().from(runs).where(
|
|
16398
16709
|
and15(
|
|
16399
|
-
|
|
16400
|
-
or3(
|
|
16710
|
+
eq29(runs.projectId, projectId),
|
|
16711
|
+
or3(eq29(runs.status, "completed"), eq29(runs.status, "partial"))
|
|
16401
16712
|
)
|
|
16402
16713
|
).orderBy(desc13(runs.createdAt)).limit(2).all();
|
|
16403
16714
|
if (recentRuns.length < 2) return [];
|
|
@@ -16409,12 +16720,12 @@ var Notifier = class {
|
|
|
16409
16720
|
keyword: keywords.keyword,
|
|
16410
16721
|
provider: querySnapshots.provider,
|
|
16411
16722
|
citationState: querySnapshots.citationState
|
|
16412
|
-
}).from(querySnapshots).leftJoin(keywords,
|
|
16723
|
+
}).from(querySnapshots).leftJoin(keywords, eq29(querySnapshots.keywordId, keywords.id)).where(eq29(querySnapshots.runId, currentRunId)).all();
|
|
16413
16724
|
const previousSnapshots = this.db.select({
|
|
16414
16725
|
keywordId: querySnapshots.keywordId,
|
|
16415
16726
|
provider: querySnapshots.provider,
|
|
16416
16727
|
citationState: querySnapshots.citationState
|
|
16417
|
-
}).from(querySnapshots).where(
|
|
16728
|
+
}).from(querySnapshots).where(eq29(querySnapshots.runId, previousRunId)).all();
|
|
16418
16729
|
const prevMap = /* @__PURE__ */ new Map();
|
|
16419
16730
|
for (const s of previousSnapshots) {
|
|
16420
16731
|
prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
|
|
@@ -16531,7 +16842,7 @@ var RunCoordinator = class {
|
|
|
16531
16842
|
|
|
16532
16843
|
// src/agent/session-registry.ts
|
|
16533
16844
|
import crypto27 from "crypto";
|
|
16534
|
-
import { eq as
|
|
16845
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
16535
16846
|
|
|
16536
16847
|
// src/agent/session.ts
|
|
16537
16848
|
import fs11 from "fs";
|
|
@@ -16881,7 +17192,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
16881
17192
|
|
|
16882
17193
|
// src/agent/memory-store.ts
|
|
16883
17194
|
import crypto26 from "crypto";
|
|
16884
|
-
import { and as and16, desc as desc14, eq as
|
|
17195
|
+
import { and as and16, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
|
|
16885
17196
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
16886
17197
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
16887
17198
|
function rowToDto(row) {
|
|
@@ -16895,7 +17206,7 @@ function rowToDto(row) {
|
|
|
16895
17206
|
};
|
|
16896
17207
|
}
|
|
16897
17208
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
16898
|
-
const query = db.select().from(agentMemory).where(
|
|
17209
|
+
const query = db.select().from(agentMemory).where(eq30(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
|
|
16899
17210
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
16900
17211
|
return rows.map(rowToDto);
|
|
16901
17212
|
}
|
|
@@ -16926,12 +17237,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
16926
17237
|
updatedAt: now
|
|
16927
17238
|
}
|
|
16928
17239
|
}).run();
|
|
16929
|
-
const row = db.select().from(agentMemory).where(and16(
|
|
17240
|
+
const row = db.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
|
|
16930
17241
|
if (!row) throw new Error("memory upsert produced no row");
|
|
16931
17242
|
return rowToDto(row);
|
|
16932
17243
|
}
|
|
16933
17244
|
function deleteMemoryEntry(db, projectId, key) {
|
|
16934
|
-
const result = db.delete(agentMemory).where(and16(
|
|
17245
|
+
const result = db.delete(agentMemory).where(and16(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
|
|
16935
17246
|
const changes = result.changes ?? 0;
|
|
16936
17247
|
return changes > 0;
|
|
16937
17248
|
}
|
|
@@ -16961,7 +17272,7 @@ function writeCompactionNote(db, args) {
|
|
|
16961
17272
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
16962
17273
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
16963
17274
|
and16(
|
|
16964
|
-
|
|
17275
|
+
eq30(agentMemory.projectId, args.projectId),
|
|
16965
17276
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
16966
17277
|
)
|
|
16967
17278
|
).orderBy(desc14(agentMemory.updatedAt)).all();
|
|
@@ -16969,7 +17280,7 @@ function writeCompactionNote(db, args) {
|
|
|
16969
17280
|
if (stale.length > 0) {
|
|
16970
17281
|
tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
|
|
16971
17282
|
}
|
|
16972
|
-
const row = tx.select().from(agentMemory).where(and16(
|
|
17283
|
+
const row = tx.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
|
|
16973
17284
|
if (row) inserted = rowToDto(row);
|
|
16974
17285
|
});
|
|
16975
17286
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -17151,7 +17462,7 @@ var SessionRegistry = class {
|
|
|
17151
17462
|
modelProvider: effectiveProvider,
|
|
17152
17463
|
modelId: effectiveModelId,
|
|
17153
17464
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17154
|
-
}).where(
|
|
17465
|
+
}).where(eq31(agentSessions.projectId, projectId)).run();
|
|
17155
17466
|
}
|
|
17156
17467
|
const agent2 = createAeroSession({
|
|
17157
17468
|
projectName,
|
|
@@ -17365,7 +17676,7 @@ ${lines.join("\n")}
|
|
|
17365
17676
|
modelProvider: nextProvider,
|
|
17366
17677
|
modelId: nextModelId,
|
|
17367
17678
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
17368
|
-
}).where(
|
|
17679
|
+
}).where(eq31(agentSessions.projectId, projectId)).run();
|
|
17369
17680
|
}
|
|
17370
17681
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
17371
17682
|
save(projectName) {
|
|
@@ -17527,11 +17838,11 @@ ${lines.join("\n")}
|
|
|
17527
17838
|
return id;
|
|
17528
17839
|
}
|
|
17529
17840
|
tryResolveProjectId(projectName) {
|
|
17530
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
17841
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq31(projects.name, projectName)).get();
|
|
17531
17842
|
return row?.id;
|
|
17532
17843
|
}
|
|
17533
17844
|
loadRow(projectId) {
|
|
17534
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
17845
|
+
const row = this.opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, projectId)).get();
|
|
17535
17846
|
return row ?? null;
|
|
17536
17847
|
}
|
|
17537
17848
|
insertRow(params) {
|
|
@@ -17550,14 +17861,14 @@ ${lines.join("\n")}
|
|
|
17550
17861
|
}
|
|
17551
17862
|
updateRow(projectId, patch) {
|
|
17552
17863
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17553
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
17864
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq31(agentSessions.projectId, projectId)).run();
|
|
17554
17865
|
}
|
|
17555
17866
|
};
|
|
17556
17867
|
|
|
17557
17868
|
// src/agent/agent-routes.ts
|
|
17558
|
-
import { eq as
|
|
17869
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
17559
17870
|
function resolveProject2(db, name) {
|
|
17560
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
17871
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq32(projects.name, name)).get();
|
|
17561
17872
|
if (!row) throw notFound("project", name);
|
|
17562
17873
|
return row;
|
|
17563
17874
|
}
|
|
@@ -17566,7 +17877,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
17566
17877
|
"/projects/:name/agent/transcript",
|
|
17567
17878
|
async (request) => {
|
|
17568
17879
|
const project = resolveProject2(opts.db, request.params.name);
|
|
17569
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
17880
|
+
const row = opts.db.select().from(agentSessions).where(eq32(agentSessions.projectId, project.id)).get();
|
|
17570
17881
|
if (!row) {
|
|
17571
17882
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
17572
17883
|
}
|
|
@@ -17590,7 +17901,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
17590
17901
|
async (request) => {
|
|
17591
17902
|
const project = resolveProject2(opts.db, request.params.name);
|
|
17592
17903
|
opts.sessionRegistry.reset(project.name);
|
|
17593
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
17904
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(agentSessions.projectId, project.id)).run();
|
|
17594
17905
|
return { status: "reset" };
|
|
17595
17906
|
}
|
|
17596
17907
|
);
|
|
@@ -17894,7 +18205,7 @@ var SnapshotService = class {
|
|
|
17894
18205
|
}
|
|
17895
18206
|
async createReport(input) {
|
|
17896
18207
|
const companyName = input.companyName.trim();
|
|
17897
|
-
const domain =
|
|
18208
|
+
const domain = normalizeDomain3(input.domain);
|
|
17898
18209
|
const manualPhrases = normalizeStringList(input.phrases ?? []);
|
|
17899
18210
|
const manualCompetitors = normalizeStringList(input.competitors ?? []);
|
|
17900
18211
|
const providers = this.registry.getAll();
|
|
@@ -18334,7 +18645,7 @@ function extractCompetitorsFromResponse(ctx) {
|
|
|
18334
18645
|
const targetDomain = extractHostname2(ctx.targetDomain);
|
|
18335
18646
|
for (const hint of ctx.manualCompetitors) {
|
|
18336
18647
|
if (isDomainLike(hint)) {
|
|
18337
|
-
const normalizedHint =
|
|
18648
|
+
const normalizedHint = normalizeDomain3(hint);
|
|
18338
18649
|
if (domainMatches2(normalizedHint, targetDomain)) continue;
|
|
18339
18650
|
if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
|
|
18340
18651
|
competitors2.add(normalizedHint);
|
|
@@ -18393,7 +18704,7 @@ function uniqueStrings2(values) {
|
|
|
18393
18704
|
values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
|
|
18394
18705
|
)];
|
|
18395
18706
|
}
|
|
18396
|
-
function
|
|
18707
|
+
function normalizeDomain3(value) {
|
|
18397
18708
|
const trimmed = value.trim();
|
|
18398
18709
|
if (!trimmed) return trimmed;
|
|
18399
18710
|
try {
|
|
@@ -18404,15 +18715,15 @@ function normalizeDomain2(value) {
|
|
|
18404
18715
|
}
|
|
18405
18716
|
}
|
|
18406
18717
|
function extractHostname2(value) {
|
|
18407
|
-
return
|
|
18718
|
+
return normalizeDomain3(value);
|
|
18408
18719
|
}
|
|
18409
18720
|
function domainMatches2(candidate, target) {
|
|
18410
|
-
const normalizedCandidate =
|
|
18411
|
-
const normalizedTarget =
|
|
18721
|
+
const normalizedCandidate = normalizeDomain3(candidate);
|
|
18722
|
+
const normalizedTarget = normalizeDomain3(target);
|
|
18412
18723
|
return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
|
|
18413
18724
|
}
|
|
18414
18725
|
function isDomainLike(value) {
|
|
18415
|
-
const normalized =
|
|
18726
|
+
const normalized = normalizeDomain3(value);
|
|
18416
18727
|
return normalized.includes(".") && !normalized.includes(" ");
|
|
18417
18728
|
}
|
|
18418
18729
|
function clipText(value, length) {
|
|
@@ -18612,7 +18923,7 @@ async function createServer(opts) {
|
|
|
18612
18923
|
intelligenceService,
|
|
18613
18924
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
18614
18925
|
async ({ runId, projectId, insightCount, criticalOrHigh }) => {
|
|
18615
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
18926
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
18616
18927
|
if (!project) return;
|
|
18617
18928
|
sessionRegistry.queueFollowUp(project.name, {
|
|
18618
18929
|
role: "user",
|
|
@@ -18752,7 +19063,7 @@ async function createServer(opts) {
|
|
|
18752
19063
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
18753
19064
|
if (opts.config.apiKey) {
|
|
18754
19065
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
18755
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
19066
|
+
const existing = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, keyHash)).get();
|
|
18756
19067
|
if (!existing) {
|
|
18757
19068
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
18758
19069
|
opts.db.insert(apiKeys).values({
|
|
@@ -18804,7 +19115,7 @@ async function createServer(opts) {
|
|
|
18804
19115
|
};
|
|
18805
19116
|
const getDefaultApiKey = () => {
|
|
18806
19117
|
if (!opts.config.apiKey) return void 0;
|
|
18807
|
-
return opts.db.select().from(apiKeys).where(
|
|
19118
|
+
return opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
18808
19119
|
};
|
|
18809
19120
|
const createPasswordSession = (reply) => {
|
|
18810
19121
|
const key = getDefaultApiKey();
|
|
@@ -18861,12 +19172,12 @@ async function createServer(opts) {
|
|
|
18861
19172
|
return reply.send({ authenticated: true });
|
|
18862
19173
|
}
|
|
18863
19174
|
if (apiKey) {
|
|
18864
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
19175
|
+
const key = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
18865
19176
|
if (!key || key.revokedAt) {
|
|
18866
19177
|
const err2 = authInvalid();
|
|
18867
19178
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
18868
19179
|
}
|
|
18869
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
19180
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq33(apiKeys.id, key.id)).run();
|
|
18870
19181
|
const sessionId = createSession(key.id);
|
|
18871
19182
|
reply.header("set-cookie", serializeSessionCookie({
|
|
18872
19183
|
name: SESSION_COOKIE_NAME,
|