@ainyc/canonry 4.34.0 → 4.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/assets/assets/index-CAmKaZIt.js +302 -0
- package/assets/assets/index-CTrHzgs-.css +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-7AF6B3L6.js → chunk-F2G67CIU.js} +1412 -484
- package/dist/{chunk-7256SFYT.js → chunk-JQQXMCQ7.js} +286 -53
- package/dist/{chunk-5EBN7736.js → chunk-O7S623DL.js} +15 -1
- package/dist/{chunk-XW3F5EEW.js → chunk-XJVYVURK.js} +76 -21
- package/dist/cli.js +157 -586
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-3P2DMYRR.js → intelligence-service-7AWRUNI2.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
- package/assets/assets/index-47V0U52s.js +0 -302
- package/assets/assets/index-CNKAwZMB.css +0 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-O7S623DL.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
buildContentTargetRows,
|
|
30
30
|
buildGapQueryScore,
|
|
31
31
|
buildInventory,
|
|
32
|
+
buildMentionCoverage,
|
|
32
33
|
buildMentionLandscape,
|
|
33
34
|
buildMovementSummary,
|
|
34
35
|
buildOverviewCompetitors,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
ccReleaseSyncs,
|
|
40
41
|
competitors,
|
|
41
42
|
crawlerEventsHourly,
|
|
43
|
+
createClient,
|
|
42
44
|
createLogger,
|
|
43
45
|
discoveryProbes,
|
|
44
46
|
discoverySessions,
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
isBlogShapedQuery,
|
|
61
63
|
isTrendBaseline,
|
|
62
64
|
mapOpportunitiesToNextSteps,
|
|
65
|
+
migrate,
|
|
63
66
|
notifications,
|
|
64
67
|
parseJsonColumn,
|
|
65
68
|
pickGroupRepresentative,
|
|
@@ -71,7 +74,7 @@ import {
|
|
|
71
74
|
schedules,
|
|
72
75
|
trafficSources,
|
|
73
76
|
usageCounters
|
|
74
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-JQQXMCQ7.js";
|
|
75
78
|
import {
|
|
76
79
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
77
80
|
AGENT_PROVIDER_IDS,
|
|
@@ -90,6 +93,7 @@ import {
|
|
|
90
93
|
DiscoveryCompetitorTypes,
|
|
91
94
|
DiscoverySessionStatuses,
|
|
92
95
|
MemorySources,
|
|
96
|
+
ProviderNames,
|
|
93
97
|
RunKinds,
|
|
94
98
|
RunStatuses,
|
|
95
99
|
RunTriggers,
|
|
@@ -127,6 +131,7 @@ import {
|
|
|
127
131
|
discoveryBucketSchema,
|
|
128
132
|
discoveryPromoteRequestSchema,
|
|
129
133
|
discoveryRunRequestSchema,
|
|
134
|
+
effectiveBrandNames,
|
|
130
135
|
effectiveDomains,
|
|
131
136
|
emptyCitationVisibility,
|
|
132
137
|
extractAnswerMentions,
|
|
@@ -144,7 +149,9 @@ import {
|
|
|
144
149
|
isBrowserProvider,
|
|
145
150
|
keywordGenerateRequestSchema,
|
|
146
151
|
locationContextSchema,
|
|
152
|
+
mentionStateFromAnswerMentioned,
|
|
147
153
|
missingDependency,
|
|
154
|
+
normalizeProjectAliases,
|
|
148
155
|
normalizeProjectDomain,
|
|
149
156
|
normalizeUrlPath,
|
|
150
157
|
notFound,
|
|
@@ -180,7 +187,7 @@ import {
|
|
|
180
187
|
visibilityStateFromAnswerMentioned,
|
|
181
188
|
windowCutoff,
|
|
182
189
|
wordpressEnvSchema
|
|
183
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-XJVYVURK.js";
|
|
184
191
|
|
|
185
192
|
// src/telemetry.ts
|
|
186
193
|
import crypto from "crypto";
|
|
@@ -333,13 +340,146 @@ function trackEvent(event, properties, options) {
|
|
|
333
340
|
}).finally(() => clearTimeout(timeout));
|
|
334
341
|
}
|
|
335
342
|
|
|
343
|
+
// src/update-check.ts
|
|
344
|
+
import { createRequire as createRequire2 } from "module";
|
|
345
|
+
var _require2 = createRequire2(import.meta.url);
|
|
346
|
+
var { version: PKG_VERSION } = _require2("../package.json");
|
|
347
|
+
var PKG_NAME = "@ainyc/canonry";
|
|
348
|
+
var NPM_DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
|
|
349
|
+
var NPM_PACKAGE_URL = `https://www.npmjs.com/package/${PKG_NAME}`;
|
|
350
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
351
|
+
function isUpdateCheckEnabled() {
|
|
352
|
+
if (process.env.CANONRY_DISABLE_UPDATE_CHECK === "1") return false;
|
|
353
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
354
|
+
if (process.env.CI) return false;
|
|
355
|
+
if (!configExists()) return true;
|
|
356
|
+
try {
|
|
357
|
+
const raw = loadConfigRaw();
|
|
358
|
+
return raw?.updateCheck !== false;
|
|
359
|
+
} catch {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function compareSemver(a, b) {
|
|
364
|
+
const parse = (v) => {
|
|
365
|
+
const core = v.split(/[-+]/)[0];
|
|
366
|
+
if (!core) return null;
|
|
367
|
+
const parts = core.split(".");
|
|
368
|
+
if (parts.length < 3) return null;
|
|
369
|
+
const nums = [];
|
|
370
|
+
for (let i = 0; i < 3; i++) {
|
|
371
|
+
const n = Number(parts[i]);
|
|
372
|
+
if (!Number.isInteger(n) || n < 0) return null;
|
|
373
|
+
nums.push(n);
|
|
374
|
+
}
|
|
375
|
+
return [nums[0], nums[1], nums[2]];
|
|
376
|
+
};
|
|
377
|
+
const pa = parse(a);
|
|
378
|
+
const pb = parse(b);
|
|
379
|
+
if (!pa || !pb) return 0;
|
|
380
|
+
for (let i = 0; i < 3; i++) {
|
|
381
|
+
if (pa[i] > pb[i]) return 1;
|
|
382
|
+
if (pa[i] < pb[i]) return -1;
|
|
383
|
+
}
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
async function fetchLatestVersion(opts) {
|
|
387
|
+
const controller = new AbortController();
|
|
388
|
+
const timeout = setTimeout(() => controller.abort(), opts?.timeoutMs ?? FETCH_TIMEOUT_MS);
|
|
389
|
+
timeout.unref();
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(NPM_DIST_TAGS_URL, {
|
|
392
|
+
signal: controller.signal,
|
|
393
|
+
headers: { accept: "application/json" }
|
|
394
|
+
});
|
|
395
|
+
if (!res.ok) return null;
|
|
396
|
+
const data = await res.json();
|
|
397
|
+
if (typeof data.latest !== "string") return null;
|
|
398
|
+
return data.latest;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
} finally {
|
|
402
|
+
clearTimeout(timeout);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function buildUpdateAvailable(current, latest) {
|
|
406
|
+
if (compareSemver(latest, current) <= 0) return null;
|
|
407
|
+
return {
|
|
408
|
+
current,
|
|
409
|
+
latest,
|
|
410
|
+
url: NPM_PACKAGE_URL,
|
|
411
|
+
upgradeCommand: `npm install -g ${PKG_NAME}`
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function checkLatestVersionForCli(opts) {
|
|
415
|
+
if (!isUpdateCheckEnabled()) return null;
|
|
416
|
+
if (!configExists()) return null;
|
|
417
|
+
const now = opts?.now ? opts.now() : /* @__PURE__ */ new Date();
|
|
418
|
+
const ttlMs = (opts?.ttlHours ?? 24) * 60 * 60 * 1e3;
|
|
419
|
+
let raw;
|
|
420
|
+
try {
|
|
421
|
+
raw = loadConfigRaw();
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
if (!raw) return null;
|
|
426
|
+
const lastCheckedAt = raw.lastUpdateCheckAt ? Date.parse(raw.lastUpdateCheckAt) : NaN;
|
|
427
|
+
const cachedLatest = typeof raw.lastKnownLatestVersion === "string" ? raw.lastKnownLatestVersion : void 0;
|
|
428
|
+
if (Number.isFinite(lastCheckedAt) && now.getTime() - lastCheckedAt < ttlMs) {
|
|
429
|
+
if (!cachedLatest) return null;
|
|
430
|
+
return buildUpdateAvailable(PKG_VERSION, cachedLatest);
|
|
431
|
+
}
|
|
432
|
+
const latest = await fetchLatestVersion();
|
|
433
|
+
if (!latest) {
|
|
434
|
+
try {
|
|
435
|
+
saveConfigPatch({ lastUpdateCheckAt: now.toISOString() });
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
saveConfigPatch({
|
|
442
|
+
lastUpdateCheckAt: now.toISOString(),
|
|
443
|
+
lastKnownLatestVersion: latest
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
return buildUpdateAvailable(PKG_VERSION, latest);
|
|
448
|
+
}
|
|
449
|
+
var memoryCache = null;
|
|
450
|
+
var inFlight = null;
|
|
451
|
+
function startBackgroundRefresh(getNow) {
|
|
452
|
+
if (inFlight) return;
|
|
453
|
+
inFlight = (async () => {
|
|
454
|
+
try {
|
|
455
|
+
const latest = await fetchLatestVersion();
|
|
456
|
+
memoryCache = { fetchedAt: getNow(), latest };
|
|
457
|
+
} catch {
|
|
458
|
+
memoryCache = { fetchedAt: getNow(), latest: null };
|
|
459
|
+
} finally {
|
|
460
|
+
inFlight = null;
|
|
461
|
+
}
|
|
462
|
+
})();
|
|
463
|
+
}
|
|
464
|
+
function checkLatestVersionForServer(opts) {
|
|
465
|
+
if (!isUpdateCheckEnabled()) return null;
|
|
466
|
+
const getNow = opts?.now ?? Date.now;
|
|
467
|
+
const ttl = opts?.ttlMs ?? 60 * 60 * 1e3;
|
|
468
|
+
const now = getNow();
|
|
469
|
+
if (!memoryCache || now - memoryCache.fetchedAt >= ttl) {
|
|
470
|
+
startBackgroundRefresh(getNow);
|
|
471
|
+
}
|
|
472
|
+
if (!memoryCache || !memoryCache.latest) return null;
|
|
473
|
+
return buildUpdateAvailable(PKG_VERSION, memoryCache.latest);
|
|
474
|
+
}
|
|
475
|
+
|
|
336
476
|
// src/server.ts
|
|
337
|
-
import { createRequire as
|
|
477
|
+
import { createRequire as createRequire4 } from "module";
|
|
338
478
|
import crypto34 from "crypto";
|
|
339
|
-
import
|
|
340
|
-
import
|
|
479
|
+
import fs13 from "fs";
|
|
480
|
+
import path15 from "path";
|
|
341
481
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
342
|
-
import { eq as
|
|
482
|
+
import { eq as eq41 } from "drizzle-orm";
|
|
343
483
|
import Fastify from "fastify";
|
|
344
484
|
|
|
345
485
|
// ../api-routes/src/auth.ts
|
|
@@ -409,7 +549,7 @@ async function authPlugin(app, opts = {}) {
|
|
|
409
549
|
|
|
410
550
|
// ../api-routes/src/projects.ts
|
|
411
551
|
import crypto4 from "crypto";
|
|
412
|
-
import { eq as eq3 } from "drizzle-orm";
|
|
552
|
+
import { eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
413
553
|
|
|
414
554
|
// ../api-routes/src/helpers.ts
|
|
415
555
|
import crypto3 from "crypto";
|
|
@@ -440,7 +580,11 @@ function resolveSnapshotMentionResult(snapshot, project) {
|
|
|
440
580
|
canonicalDomain: project.canonicalDomain,
|
|
441
581
|
ownedDomains: normalizeOwnedDomains(project.ownedDomains)
|
|
442
582
|
});
|
|
443
|
-
|
|
583
|
+
const brandNames = effectiveBrandNames({
|
|
584
|
+
displayName: project.displayName,
|
|
585
|
+
aliases: normalizeOwnedDomains(project.aliases)
|
|
586
|
+
});
|
|
587
|
+
return extractAnswerMentions(snapshot.answerText, brandNames, domains);
|
|
444
588
|
}
|
|
445
589
|
if (typeof snapshot.answerMentioned === "boolean") {
|
|
446
590
|
return { mentioned: snapshot.answerMentioned, matchedTerms: [] };
|
|
@@ -453,6 +597,9 @@ function resolveSnapshotAnswerMentioned(snapshot, project) {
|
|
|
453
597
|
function resolveSnapshotVisibilityState(snapshot, project) {
|
|
454
598
|
return visibilityStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
|
|
455
599
|
}
|
|
600
|
+
function resolveSnapshotMentionState(snapshot, project) {
|
|
601
|
+
return mentionStateFromAnswerMentioned(resolveSnapshotMentionResult(snapshot, project).mentioned);
|
|
602
|
+
}
|
|
456
603
|
function resolveSnapshotMatchedTerms(snapshot, project) {
|
|
457
604
|
return resolveSnapshotMentionResult(snapshot, project).matchedTerms;
|
|
458
605
|
}
|
|
@@ -503,12 +650,16 @@ async function projectRoutes(app, opts) {
|
|
|
503
650
|
});
|
|
504
651
|
}
|
|
505
652
|
const nextAutoExtractBacklinks = body.autoExtractBacklinks !== void 0 ? body.autoExtractBacklinks ? 1 : 0 : existing?.autoExtractBacklinks ?? 0;
|
|
653
|
+
const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
|
|
506
654
|
if (existing) {
|
|
655
|
+
const prevAliases = parseJsonColumn(existing.aliases, []);
|
|
656
|
+
const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
|
|
507
657
|
app.db.transaction((tx) => {
|
|
508
658
|
tx.update(projects).set({
|
|
509
659
|
displayName: body.displayName,
|
|
510
660
|
canonicalDomain: body.canonicalDomain,
|
|
511
661
|
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
662
|
+
aliases: JSON.stringify(nextAliases),
|
|
512
663
|
country: body.country,
|
|
513
664
|
language: body.language,
|
|
514
665
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -530,6 +681,7 @@ async function projectRoutes(app, opts) {
|
|
|
530
681
|
});
|
|
531
682
|
});
|
|
532
683
|
opts.onProjectUpserted?.(existing.id, name);
|
|
684
|
+
if (aliasesChanged) opts.onAliasesChanged?.(existing.id, name);
|
|
533
685
|
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
534
686
|
return reply.status(200).send(formatProject(updated));
|
|
535
687
|
}
|
|
@@ -541,6 +693,7 @@ async function projectRoutes(app, opts) {
|
|
|
541
693
|
displayName: body.displayName,
|
|
542
694
|
canonicalDomain: body.canonicalDomain,
|
|
543
695
|
ownedDomains: JSON.stringify(body.ownedDomains ?? []),
|
|
696
|
+
aliases: JSON.stringify(nextAliases),
|
|
544
697
|
country: body.country,
|
|
545
698
|
language: body.language,
|
|
546
699
|
tags: JSON.stringify(body.tags ?? []),
|
|
@@ -574,6 +727,30 @@ async function projectRoutes(app, opts) {
|
|
|
574
727
|
const project = resolveProject(app.db, request.params.name);
|
|
575
728
|
return reply.send(formatProject(project));
|
|
576
729
|
});
|
|
730
|
+
app.get("/projects/:name/delete-preview", async (request, reply) => {
|
|
731
|
+
const project = resolveProject(app.db, request.params.name);
|
|
732
|
+
const pid = project.id;
|
|
733
|
+
const count = (n) => n ?? 0;
|
|
734
|
+
const queryCount = app.db.select({ n: sql2`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
|
|
735
|
+
const competitorCount = app.db.select({ n: sql2`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
|
|
736
|
+
const runCount = app.db.select({ n: sql2`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
|
|
737
|
+
const snapshotCount = app.db.select({ n: sql2`count(*)` }).from(querySnapshots).innerJoin(runs, eq3(querySnapshots.runId, runs.id)).where(eq3(runs.projectId, pid)).get();
|
|
738
|
+
const insightCount = app.db.select({ n: sql2`count(*)` }).from(insights).where(eq3(insights.projectId, pid)).get();
|
|
739
|
+
const auditLogCount = app.db.select({ n: sql2`count(*)` }).from(auditLog).where(eq3(auditLog.projectId, pid)).get();
|
|
740
|
+
return reply.send({
|
|
741
|
+
project: { id: project.id, name: project.name },
|
|
742
|
+
cascadeRows: {
|
|
743
|
+
queries: count(queryCount?.n),
|
|
744
|
+
competitors: count(competitorCount?.n),
|
|
745
|
+
runs: count(runCount?.n),
|
|
746
|
+
snapshots: count(snapshotCount?.n),
|
|
747
|
+
insights: count(insightCount?.n)
|
|
748
|
+
},
|
|
749
|
+
detachedRows: {
|
|
750
|
+
auditLog: count(auditLogCount?.n)
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
});
|
|
577
754
|
app.delete("/projects/:name", async (request, reply) => {
|
|
578
755
|
const project = resolveProject(app.db, request.params.name);
|
|
579
756
|
writeAuditLog(app.db, {
|
|
@@ -688,6 +865,7 @@ async function projectRoutes(app, opts) {
|
|
|
688
865
|
displayName: project.displayName,
|
|
689
866
|
canonicalDomain: project.canonicalDomain,
|
|
690
867
|
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
868
|
+
aliases: parseJsonColumn(project.aliases, []),
|
|
691
869
|
country: project.country,
|
|
692
870
|
language: project.language,
|
|
693
871
|
queries: qs.map((q) => q.query),
|
|
@@ -723,6 +901,7 @@ function formatProject(row) {
|
|
|
723
901
|
displayName: row.displayName,
|
|
724
902
|
canonicalDomain: row.canonicalDomain,
|
|
725
903
|
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
904
|
+
aliases: parseJsonColumn(row.aliases, []),
|
|
726
905
|
country: row.country,
|
|
727
906
|
language: row.language,
|
|
728
907
|
tags: parseJsonColumn(row.tags, []),
|
|
@@ -737,6 +916,13 @@ function formatProject(row) {
|
|
|
737
916
|
updatedAt: row.updatedAt
|
|
738
917
|
};
|
|
739
918
|
}
|
|
919
|
+
function aliasArraysEqual(a, b) {
|
|
920
|
+
if (a.length !== b.length) return false;
|
|
921
|
+
for (let i = 0; i < a.length; i++) {
|
|
922
|
+
if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
|
|
923
|
+
}
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
740
926
|
|
|
741
927
|
// ../api-routes/src/queries.ts
|
|
742
928
|
import crypto5 from "crypto";
|
|
@@ -1139,7 +1325,7 @@ function parseCompetitorBatch(value) {
|
|
|
1139
1325
|
|
|
1140
1326
|
// ../api-routes/src/runs.ts
|
|
1141
1327
|
import crypto8 from "crypto";
|
|
1142
|
-
import { eq as eq7, asc, desc, sql as
|
|
1328
|
+
import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql3 } from "drizzle-orm";
|
|
1143
1329
|
|
|
1144
1330
|
// ../api-routes/src/run-queue.ts
|
|
1145
1331
|
import crypto7 from "crypto";
|
|
@@ -1233,23 +1419,36 @@ async function runRoutes(app, opts) {
|
|
|
1233
1419
|
if (projectLocations.length === 0) {
|
|
1234
1420
|
throw validationError("No locations configured for this project");
|
|
1235
1421
|
}
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1422
|
+
const result = app.db.transaction((tx) => {
|
|
1423
|
+
const activeRun = tx.select({ id: runs.id }).from(runs).where(and2(
|
|
1424
|
+
eq7(runs.projectId, project.id),
|
|
1425
|
+
or2(eq7(runs.status, "queued"), eq7(runs.status, "running"))
|
|
1426
|
+
)).get();
|
|
1427
|
+
if (activeRun) {
|
|
1428
|
+
return { conflict: true };
|
|
1429
|
+
}
|
|
1430
|
+
const inserted = [];
|
|
1431
|
+
for (const loc of projectLocations) {
|
|
1432
|
+
const runId2 = crypto8.randomUUID();
|
|
1433
|
+
tx.insert(runs).values({
|
|
1434
|
+
id: runId2,
|
|
1435
|
+
projectId: project.id,
|
|
1436
|
+
kind,
|
|
1437
|
+
status: "queued",
|
|
1438
|
+
trigger,
|
|
1439
|
+
location: loc.label,
|
|
1440
|
+
queries: queriesColumn,
|
|
1441
|
+
createdAt: now
|
|
1442
|
+
}).run();
|
|
1443
|
+
inserted.push({ runId: runId2, loc });
|
|
1444
|
+
}
|
|
1445
|
+
return { conflict: false, inserted };
|
|
1446
|
+
});
|
|
1447
|
+
if (result.conflict) {
|
|
1448
|
+
throw runInProgress(project.name);
|
|
1250
1449
|
}
|
|
1251
1450
|
const results = [];
|
|
1252
|
-
for (const { runId: runId2, loc } of
|
|
1451
|
+
for (const { runId: runId2, loc } of result.inserted) {
|
|
1253
1452
|
writeAuditLog(app.db, {
|
|
1254
1453
|
projectId: project.id,
|
|
1255
1454
|
actor: "api",
|
|
@@ -1298,7 +1497,7 @@ async function runRoutes(app, opts) {
|
|
|
1298
1497
|
});
|
|
1299
1498
|
app.get("/projects/:name/runs/latest", async (request, reply) => {
|
|
1300
1499
|
const project = resolveProject(app.db, request.params.name);
|
|
1301
|
-
const countRow = app.db.select({ count:
|
|
1500
|
+
const countRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
|
|
1302
1501
|
const totalRuns = countRow?.count ?? 0;
|
|
1303
1502
|
const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
|
|
1304
1503
|
if (!latestRun) {
|
|
@@ -1437,7 +1636,8 @@ function loadRunDetail(app, run) {
|
|
|
1437
1636
|
const project = app.db.select({
|
|
1438
1637
|
displayName: projects.displayName,
|
|
1439
1638
|
canonicalDomain: projects.canonicalDomain,
|
|
1440
|
-
ownedDomains: projects.ownedDomains
|
|
1639
|
+
ownedDomains: projects.ownedDomains,
|
|
1640
|
+
aliases: projects.aliases
|
|
1441
1641
|
}).from(projects).where(eq7(projects.id, run.projectId)).get();
|
|
1442
1642
|
const snapshots = app.db.select({
|
|
1443
1643
|
id: querySnapshots.id,
|
|
@@ -1469,7 +1669,10 @@ function loadRunDetail(app, run) {
|
|
|
1469
1669
|
provider: s.provider,
|
|
1470
1670
|
citationState: s.citationState,
|
|
1471
1671
|
answerMentioned,
|
|
1672
|
+
// Legacy alias of `mentionState`, retained for backwards compatibility.
|
|
1472
1673
|
visibilityState: project ? resolveSnapshotVisibilityState(s, project) : answerMentioned ? "visible" : "not-visible",
|
|
1674
|
+
// Canonical vocabulary for answer-text presence; new consumers prefer this.
|
|
1675
|
+
mentionState: project ? resolveSnapshotMentionState(s, project) : answerMentioned ? "mentioned" : "not-mentioned",
|
|
1473
1676
|
answerText: s.answerText,
|
|
1474
1677
|
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
1475
1678
|
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
@@ -1487,7 +1690,7 @@ function loadRunDetail(app, run) {
|
|
|
1487
1690
|
|
|
1488
1691
|
// ../api-routes/src/apply.ts
|
|
1489
1692
|
import crypto10 from "crypto";
|
|
1490
|
-
import { and as
|
|
1693
|
+
import { and as and3, eq as eq8 } from "drizzle-orm";
|
|
1491
1694
|
|
|
1492
1695
|
// ../api-routes/src/schedule-utils.ts
|
|
1493
1696
|
var DAY_MAP = {
|
|
@@ -1584,7 +1787,7 @@ import http from "http";
|
|
|
1584
1787
|
import https from "https";
|
|
1585
1788
|
import net from "net";
|
|
1586
1789
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
1587
|
-
async function resolveWebhookTarget(raw) {
|
|
1790
|
+
async function resolveWebhookTarget(raw, options = {}) {
|
|
1588
1791
|
let parsed;
|
|
1589
1792
|
try {
|
|
1590
1793
|
parsed = new URL(raw);
|
|
@@ -1605,7 +1808,7 @@ async function resolveWebhookTarget(raw) {
|
|
|
1605
1808
|
if (addresses.length === 0) {
|
|
1606
1809
|
return { ok: false, message: '"url" hostname could not be resolved' };
|
|
1607
1810
|
}
|
|
1608
|
-
const blocked = addresses.find((entry) => isBlockedAddress(entry.address));
|
|
1811
|
+
const blocked = addresses.find((entry) => isBlockedAddress(entry.address, options));
|
|
1609
1812
|
if (blocked) {
|
|
1610
1813
|
return { ok: false, message: '"url" must not resolve to a private or loopback address' };
|
|
1611
1814
|
}
|
|
@@ -1622,7 +1825,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
1622
1825
|
const body = JSON.stringify(payload);
|
|
1623
1826
|
const isHttps = target.url.protocol === "https:";
|
|
1624
1827
|
const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
|
|
1625
|
-
const
|
|
1828
|
+
const path16 = `${target.url.pathname}${target.url.search}`;
|
|
1626
1829
|
const headers = {
|
|
1627
1830
|
"Content-Length": String(Buffer.byteLength(body)),
|
|
1628
1831
|
"Content-Type": "application/json",
|
|
@@ -1638,7 +1841,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
1638
1841
|
headers,
|
|
1639
1842
|
hostname: target.address,
|
|
1640
1843
|
method: "POST",
|
|
1641
|
-
path:
|
|
1844
|
+
path: path16,
|
|
1642
1845
|
port,
|
|
1643
1846
|
timeout: REQUEST_TIMEOUT_MS
|
|
1644
1847
|
};
|
|
@@ -1683,34 +1886,40 @@ async function resolveHostAddresses(hostname) {
|
|
|
1683
1886
|
return [];
|
|
1684
1887
|
}
|
|
1685
1888
|
}
|
|
1686
|
-
function isBlockedAddress(address) {
|
|
1889
|
+
function isBlockedAddress(address, options) {
|
|
1687
1890
|
const normalized = stripIpv6Brackets(address).toLowerCase();
|
|
1688
1891
|
const family = net.isIP(normalized);
|
|
1689
1892
|
if (family === 4) {
|
|
1690
|
-
return isBlockedIpv4(normalized);
|
|
1893
|
+
return isBlockedIpv4(normalized, options);
|
|
1691
1894
|
}
|
|
1692
1895
|
if (family === 6) {
|
|
1693
1896
|
const mappedIpv4 = extractMappedIpv4(normalized);
|
|
1694
1897
|
if (mappedIpv4) {
|
|
1695
|
-
return isBlockedIpv4(mappedIpv4);
|
|
1898
|
+
return isBlockedIpv4(mappedIpv4, options);
|
|
1696
1899
|
}
|
|
1697
|
-
return isBlockedIpv6(normalized);
|
|
1900
|
+
return isBlockedIpv6(normalized, options);
|
|
1698
1901
|
}
|
|
1699
1902
|
return true;
|
|
1700
1903
|
}
|
|
1701
|
-
function isBlockedIpv4(address) {
|
|
1904
|
+
function isBlockedIpv4(address, options) {
|
|
1702
1905
|
const octets = address.split(".").map((part) => Number.parseInt(part, 10));
|
|
1703
1906
|
if (octets.length !== 4 || octets.some(Number.isNaN)) {
|
|
1704
1907
|
return true;
|
|
1705
1908
|
}
|
|
1706
1909
|
const [first, second] = octets;
|
|
1910
|
+
if (first === 127 && !options.allowLoopback) {
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1707
1913
|
return first === 0 || first === 10 || first === 100 && second >= 64 && second <= 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 198 && (second === 18 || second === 19);
|
|
1708
1914
|
}
|
|
1709
|
-
function isBlockedIpv6(address) {
|
|
1915
|
+
function isBlockedIpv6(address, options) {
|
|
1710
1916
|
const normalized = address.split("%")[0].toLowerCase();
|
|
1711
1917
|
if (normalized === "::") {
|
|
1712
1918
|
return true;
|
|
1713
1919
|
}
|
|
1920
|
+
if (normalized === "::1") {
|
|
1921
|
+
return !options.allowLoopback;
|
|
1922
|
+
}
|
|
1714
1923
|
const firstHextetText = normalized.split(":")[0] ?? "";
|
|
1715
1924
|
const firstHextet = firstHextetText === "" ? 0 : Number.parseInt(firstHextetText, 16);
|
|
1716
1925
|
if (Number.isNaN(firstHextet)) {
|
|
@@ -1741,6 +1950,7 @@ function stripIpv6Brackets(value) {
|
|
|
1741
1950
|
|
|
1742
1951
|
// ../api-routes/src/apply.ts
|
|
1743
1952
|
async function applyRoutes(app, opts) {
|
|
1953
|
+
const allowLoopback = opts?.allowLoopbackWebhooks === true;
|
|
1744
1954
|
app.post("/apply", async (request, reply) => {
|
|
1745
1955
|
const parsed = projectConfigSchema.safeParse(request.body);
|
|
1746
1956
|
if (!parsed.success) {
|
|
@@ -1795,7 +2005,7 @@ async function applyRoutes(app, opts) {
|
|
|
1795
2005
|
const hasNotifications = "notifications" in rawSpec;
|
|
1796
2006
|
if (hasNotifications) {
|
|
1797
2007
|
for (const notif of config.spec.notifications) {
|
|
1798
|
-
const urlCheck = await resolveWebhookTarget(notif.url ?? "");
|
|
2008
|
+
const urlCheck = await resolveWebhookTarget(notif.url ?? "", { allowLoopback });
|
|
1799
2009
|
if (!urlCheck.ok) throw validationError(`Notification URL invalid: ${urlCheck.message}`);
|
|
1800
2010
|
}
|
|
1801
2011
|
}
|
|
@@ -1804,14 +2014,21 @@ async function applyRoutes(app, opts) {
|
|
|
1804
2014
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
1805
2015
|
let projectId;
|
|
1806
2016
|
let scheduleAction = null;
|
|
2017
|
+
let aliasesChanged = false;
|
|
1807
2018
|
app.db.transaction((tx) => {
|
|
1808
2019
|
const existing = tx.select().from(projects).where(eq8(projects.name, name)).get();
|
|
2020
|
+
const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
|
|
2021
|
+
if (existing) {
|
|
2022
|
+
const prevAliases = parseJsonColumn(existing.aliases, []);
|
|
2023
|
+
aliasesChanged = !aliasArraysEqual2(prevAliases, nextAliases);
|
|
2024
|
+
}
|
|
1809
2025
|
if (existing) {
|
|
1810
2026
|
projectId = existing.id;
|
|
1811
2027
|
tx.update(projects).set({
|
|
1812
2028
|
displayName: config.spec.displayName,
|
|
1813
2029
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1814
2030
|
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2031
|
+
aliases: JSON.stringify(nextAliases),
|
|
1815
2032
|
country: config.spec.country,
|
|
1816
2033
|
language: config.spec.language,
|
|
1817
2034
|
labels: JSON.stringify(config.metadata.labels),
|
|
@@ -1838,6 +2055,7 @@ async function applyRoutes(app, opts) {
|
|
|
1838
2055
|
displayName: config.spec.displayName,
|
|
1839
2056
|
canonicalDomain: config.spec.canonicalDomain,
|
|
1840
2057
|
ownedDomains: JSON.stringify(config.spec.ownedDomains ?? []),
|
|
2058
|
+
aliases: JSON.stringify(nextAliases),
|
|
1841
2059
|
country: config.spec.country,
|
|
1842
2060
|
language: config.spec.language,
|
|
1843
2061
|
tags: "[]",
|
|
@@ -1896,7 +2114,7 @@ async function applyRoutes(app, opts) {
|
|
|
1896
2114
|
});
|
|
1897
2115
|
const AV_KIND = SchedulableRunKinds["answer-visibility"];
|
|
1898
2116
|
if (resolvedSchedule) {
|
|
1899
|
-
const existingSched = tx.select().from(schedules).where(
|
|
2117
|
+
const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1900
2118
|
if (existingSched) {
|
|
1901
2119
|
tx.update(schedules).set({
|
|
1902
2120
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -1922,9 +2140,9 @@ async function applyRoutes(app, opts) {
|
|
|
1922
2140
|
}
|
|
1923
2141
|
scheduleAction = "upsert";
|
|
1924
2142
|
} else if (deleteSchedule) {
|
|
1925
|
-
const existingSched = tx.select().from(schedules).where(
|
|
2143
|
+
const existingSched = tx.select().from(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1926
2144
|
if (existingSched) {
|
|
1927
|
-
tx.delete(schedules).where(
|
|
2145
|
+
tx.delete(schedules).where(and3(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
|
|
1928
2146
|
scheduleAction = "delete";
|
|
1929
2147
|
}
|
|
1930
2148
|
}
|
|
@@ -1957,6 +2175,9 @@ async function applyRoutes(app, opts) {
|
|
|
1957
2175
|
if (!hasNotifications) {
|
|
1958
2176
|
opts?.onProjectUpserted?.(projectId, config.metadata.name);
|
|
1959
2177
|
}
|
|
2178
|
+
if (aliasesChanged) {
|
|
2179
|
+
opts?.onAliasesChanged?.(projectId, config.metadata.name);
|
|
2180
|
+
}
|
|
1960
2181
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
1961
2182
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
1962
2183
|
}
|
|
@@ -1967,6 +2188,7 @@ async function applyRoutes(app, opts) {
|
|
|
1967
2188
|
displayName: project.displayName,
|
|
1968
2189
|
canonicalDomain: project.canonicalDomain,
|
|
1969
2190
|
ownedDomains: parseJsonColumn(project.ownedDomains, []),
|
|
2191
|
+
aliases: parseJsonColumn(project.aliases, []),
|
|
1970
2192
|
country: project.country,
|
|
1971
2193
|
language: project.language,
|
|
1972
2194
|
tags: parseJsonColumn(project.tags, []),
|
|
@@ -1982,6 +2204,13 @@ async function applyRoutes(app, opts) {
|
|
|
1982
2204
|
});
|
|
1983
2205
|
});
|
|
1984
2206
|
}
|
|
2207
|
+
function aliasArraysEqual2(a, b) {
|
|
2208
|
+
if (a.length !== b.length) return false;
|
|
2209
|
+
for (let i = 0; i < a.length; i++) {
|
|
2210
|
+
if (a[i].toLowerCase() !== b[i].toLowerCase()) return false;
|
|
2211
|
+
}
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
1985
2214
|
function normalizeCompetitorList2(domains) {
|
|
1986
2215
|
const seen = /* @__PURE__ */ new Set();
|
|
1987
2216
|
const result = [];
|
|
@@ -2089,6 +2318,7 @@ async function historyRoutes(app) {
|
|
|
2089
2318
|
citationState: s.citationState,
|
|
2090
2319
|
answerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2091
2320
|
visibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2321
|
+
mentionState: resolveSnapshotMentionState(s, project),
|
|
2092
2322
|
answerText: s.answerText,
|
|
2093
2323
|
citedDomains: parseJsonColumn(s.citedDomains, []),
|
|
2094
2324
|
competitorOverlap: parseJsonColumn(s.competitorOverlap, []),
|
|
@@ -2142,9 +2372,11 @@ async function historyRoutes(app) {
|
|
|
2142
2372
|
const run = projectRuns.find((r) => r.id === snap.runId);
|
|
2143
2373
|
let transition = snap.citationState === CitationStates.cited ? "cited" : "not-cited";
|
|
2144
2374
|
let visibilityTransition = snap.answerMentioned ? "visible" : "not-visible";
|
|
2375
|
+
let mentionTransition = snap.answerMentioned ? "mentioned" : "not-mentioned";
|
|
2145
2376
|
if (idx === 0) {
|
|
2146
2377
|
transition = "new";
|
|
2147
2378
|
visibilityTransition = "new";
|
|
2379
|
+
mentionTransition = "new";
|
|
2148
2380
|
} else {
|
|
2149
2381
|
const prev = snaps[idx - 1];
|
|
2150
2382
|
if (prev.citationState === CitationStates["not-cited"] && snap.citationState === CitationStates.cited) {
|
|
@@ -2154,8 +2386,10 @@ async function historyRoutes(app) {
|
|
|
2154
2386
|
}
|
|
2155
2387
|
if (!prev.answerMentioned && snap.answerMentioned) {
|
|
2156
2388
|
visibilityTransition = "emerging";
|
|
2389
|
+
mentionTransition = "emerging";
|
|
2157
2390
|
} else if (prev.answerMentioned && !snap.answerMentioned) {
|
|
2158
2391
|
visibilityTransition = "lost";
|
|
2392
|
+
mentionTransition = "lost";
|
|
2159
2393
|
}
|
|
2160
2394
|
}
|
|
2161
2395
|
return {
|
|
@@ -2164,8 +2398,12 @@ async function historyRoutes(app) {
|
|
|
2164
2398
|
citationState: snap.citationState,
|
|
2165
2399
|
transition,
|
|
2166
2400
|
answerMentioned: snap.answerMentioned,
|
|
2401
|
+
// Legacy aliases of `mentionState` / `mentionTransition`.
|
|
2167
2402
|
visibilityState: snap.answerMentioned ? "visible" : "not-visible",
|
|
2168
2403
|
visibilityTransition,
|
|
2404
|
+
// Canonical-vocabulary fields — new consumers prefer these.
|
|
2405
|
+
mentionState: snap.answerMentioned ? "mentioned" : "not-mentioned",
|
|
2406
|
+
mentionTransition,
|
|
2169
2407
|
location: snap.location
|
|
2170
2408
|
};
|
|
2171
2409
|
});
|
|
@@ -2221,7 +2459,8 @@ async function historyRoutes(app) {
|
|
|
2221
2459
|
const resolved = {
|
|
2222
2460
|
...s,
|
|
2223
2461
|
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2224
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
|
|
2462
|
+
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2463
|
+
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
2225
2464
|
};
|
|
2226
2465
|
const existing = map1.get(s.queryId);
|
|
2227
2466
|
if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
|
|
@@ -2233,7 +2472,8 @@ async function historyRoutes(app) {
|
|
|
2233
2472
|
const resolved = {
|
|
2234
2473
|
...s,
|
|
2235
2474
|
resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
|
|
2236
|
-
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
|
|
2475
|
+
resolvedVisibilityState: resolveSnapshotVisibilityState(s, project),
|
|
2476
|
+
resolvedMentionState: resolveSnapshotMentionState(s, project)
|
|
2237
2477
|
};
|
|
2238
2478
|
const existing = map2.get(s.queryId);
|
|
2239
2479
|
if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === CitationStates.cited) {
|
|
@@ -2251,8 +2491,11 @@ async function historyRoutes(app) {
|
|
|
2251
2491
|
run2State: s2?.citationState ?? null,
|
|
2252
2492
|
run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
|
|
2253
2493
|
run2AnswerMentioned: s2?.resolvedAnswerMentioned ?? null,
|
|
2494
|
+
// Legacy aliases — same data as run{1,2}MentionState below.
|
|
2254
2495
|
run1VisibilityState: s1?.resolvedVisibilityState ?? null,
|
|
2255
2496
|
run2VisibilityState: s2?.resolvedVisibilityState ?? null,
|
|
2497
|
+
run1MentionState: s1?.resolvedMentionState ?? null,
|
|
2498
|
+
run2MentionState: s2?.resolvedMentionState ?? null,
|
|
2256
2499
|
changed: (s1?.citationState ?? null) !== (s2?.citationState ?? null),
|
|
2257
2500
|
visibilityChanged: (s1?.resolvedAnswerMentioned ?? null) !== (s2?.resolvedAnswerMentioned ?? null)
|
|
2258
2501
|
};
|
|
@@ -2621,7 +2864,7 @@ function buildCategoryCounts(counts) {
|
|
|
2621
2864
|
}
|
|
2622
2865
|
|
|
2623
2866
|
// ../api-routes/src/intelligence.ts
|
|
2624
|
-
import { eq as eq11, desc as desc4, and as
|
|
2867
|
+
import { eq as eq11, desc as desc4, and as and4, inArray as inArray3 } from "drizzle-orm";
|
|
2625
2868
|
function emptyHealthSnapshot(projectId) {
|
|
2626
2869
|
return {
|
|
2627
2870
|
id: `no-data:${projectId}`,
|
|
@@ -2710,7 +2953,7 @@ async function intelligenceRoutes(app) {
|
|
|
2710
2953
|
if (request.query.runId) {
|
|
2711
2954
|
conditions.push(eq11(insights.runId, request.query.runId));
|
|
2712
2955
|
}
|
|
2713
|
-
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] :
|
|
2956
|
+
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and4(...conditions)).orderBy(desc4(insights.createdAt)).all();
|
|
2714
2957
|
const showDismissed = request.query.dismissed === "true";
|
|
2715
2958
|
const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
|
|
2716
2959
|
return reply.send(result);
|
|
@@ -2734,7 +2977,7 @@ async function intelligenceRoutes(app) {
|
|
|
2734
2977
|
});
|
|
2735
2978
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
2736
2979
|
const project = resolveProject(app.db, request.params.name);
|
|
2737
|
-
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
2980
|
+
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and4(
|
|
2738
2981
|
eq11(runs.projectId, project.id),
|
|
2739
2982
|
eq11(runs.kind, RunKinds["answer-visibility"]),
|
|
2740
2983
|
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
@@ -2742,7 +2985,7 @@ async function intelligenceRoutes(app) {
|
|
|
2742
2985
|
const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
|
|
2743
2986
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
2744
2987
|
if (latestGroupRunIds.length > 0) {
|
|
2745
|
-
const groupRows = app.db.select().from(healthSnapshots).where(
|
|
2988
|
+
const groupRows = app.db.select().from(healthSnapshots).where(and4(
|
|
2746
2989
|
eq11(healthSnapshots.projectId, project.id),
|
|
2747
2990
|
inArray3(healthSnapshots.runId, latestGroupRunIds)
|
|
2748
2991
|
)).all();
|
|
@@ -2766,7 +3009,7 @@ async function intelligenceRoutes(app) {
|
|
|
2766
3009
|
}
|
|
2767
3010
|
|
|
2768
3011
|
// ../api-routes/src/report.ts
|
|
2769
|
-
import { and as
|
|
3012
|
+
import { and as and6, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or3, sql as sql4 } from "drizzle-orm";
|
|
2770
3013
|
|
|
2771
3014
|
// ../api-routes/src/report-renderer.ts
|
|
2772
3015
|
var COLORS = {
|
|
@@ -2815,9 +3058,9 @@ function inferAdSource(params) {
|
|
|
2815
3058
|
function formatLandingPageHtml(raw) {
|
|
2816
3059
|
const value = raw ?? "";
|
|
2817
3060
|
const queryIdx = value.indexOf("?");
|
|
2818
|
-
const
|
|
3061
|
+
const path16 = queryIdx === -1 ? value : value.slice(0, queryIdx);
|
|
2819
3062
|
const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
|
|
2820
|
-
const pathHtml = `<span class="page-path">${escapeHtml(
|
|
3063
|
+
const pathHtml = `<span class="page-path">${escapeHtml(path16 || "/")}</span>`;
|
|
2821
3064
|
if (!query) return pathHtml;
|
|
2822
3065
|
let summary = "";
|
|
2823
3066
|
try {
|
|
@@ -4171,7 +4414,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
4171
4414
|
y: padY + usableH - p.y / max * usableH,
|
|
4172
4415
|
raw: p
|
|
4173
4416
|
}));
|
|
4174
|
-
const
|
|
4417
|
+
const path16 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
4175
4418
|
const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
|
|
4176
4419
|
const xLabels = xy.map((p, i) => {
|
|
4177
4420
|
if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
|
|
@@ -4183,7 +4426,7 @@ function renderLineChart(points, color, title, height = 200) {
|
|
|
4183
4426
|
<line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
|
|
4184
4427
|
<text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
|
|
4185
4428
|
<text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
|
|
4186
|
-
<path d="${
|
|
4429
|
+
<path d="${path16}" stroke="${color}" stroke-width="2" fill="none" />
|
|
4187
4430
|
${dots}
|
|
4188
4431
|
${xLabels}
|
|
4189
4432
|
</svg>
|
|
@@ -5002,7 +5245,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
5002
5245
|
}
|
|
5003
5246
|
|
|
5004
5247
|
// ../api-routes/src/content-data.ts
|
|
5005
|
-
import { and as
|
|
5248
|
+
import { and as and5, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
|
|
5006
5249
|
var RECENT_RUNS_WINDOW = 5;
|
|
5007
5250
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
5008
5251
|
const projectId = project.id;
|
|
@@ -5126,7 +5369,7 @@ function listCompetitorDomains(db, projectId) {
|
|
|
5126
5369
|
}
|
|
5127
5370
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
|
|
5128
5371
|
const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
5129
|
-
|
|
5372
|
+
and5(
|
|
5130
5373
|
eq12(runs.projectId, projectId),
|
|
5131
5374
|
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
5132
5375
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
@@ -5157,9 +5400,9 @@ function buildGaTrafficByPage(db, projectId) {
|
|
|
5157
5400
|
}).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
|
|
5158
5401
|
const map = /* @__PURE__ */ new Map();
|
|
5159
5402
|
for (const row of rows) {
|
|
5160
|
-
const
|
|
5161
|
-
if (!
|
|
5162
|
-
map.set(
|
|
5403
|
+
const path16 = extractPath(row.landingPage);
|
|
5404
|
+
if (!path16) continue;
|
|
5405
|
+
map.set(path16, (map.get(path16) ?? 0) + (row.sessions ?? 0));
|
|
5163
5406
|
}
|
|
5164
5407
|
return map;
|
|
5165
5408
|
}
|
|
@@ -5354,8 +5597,8 @@ function normalizeDomain(domain) {
|
|
|
5354
5597
|
function extractPath(url) {
|
|
5355
5598
|
if (!url) return "";
|
|
5356
5599
|
const match = /^https?:\/\/[^/]+(.*)$/.exec(url.trim());
|
|
5357
|
-
const
|
|
5358
|
-
const stripped =
|
|
5600
|
+
const path16 = match ? match[1] : url.trim();
|
|
5601
|
+
const stripped = path16.replace(/\/+$/, "");
|
|
5359
5602
|
return stripped || "/";
|
|
5360
5603
|
}
|
|
5361
5604
|
|
|
@@ -5384,8 +5627,8 @@ function safeNum(value) {
|
|
|
5384
5627
|
}
|
|
5385
5628
|
return 0;
|
|
5386
5629
|
}
|
|
5387
|
-
function categorizeQuery(query,
|
|
5388
|
-
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain,
|
|
5630
|
+
function categorizeQuery(query, projectBrandNames, canonicalDomain) {
|
|
5631
|
+
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectBrandNames));
|
|
5389
5632
|
}
|
|
5390
5633
|
function loadSnapshotsForRun(db, runId) {
|
|
5391
5634
|
return loadSnapshotsForRunIds(db, [runId]);
|
|
@@ -5414,7 +5657,7 @@ function loadQueryLookup(db, projectId) {
|
|
|
5414
5657
|
for (const row of rows) byId.set(row.id, row.query);
|
|
5415
5658
|
return { byId };
|
|
5416
5659
|
}
|
|
5417
|
-
function buildGscSection(db, projectId,
|
|
5660
|
+
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries) {
|
|
5418
5661
|
const allRows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
5419
5662
|
if (allRows.length === 0) return null;
|
|
5420
5663
|
let maxDate = "";
|
|
@@ -5449,11 +5692,11 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5449
5692
|
impressions: agg.impressions,
|
|
5450
5693
|
ctr: agg.impressions > 0 ? agg.clicks / agg.impressions : 0,
|
|
5451
5694
|
avgPosition: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0,
|
|
5452
|
-
category: categorizeQuery(query,
|
|
5695
|
+
category: categorizeQuery(query, projectBrandNames, canonicalDomain)
|
|
5453
5696
|
})).sort((a, b) => b.clicks - a.clicks).slice(0, TOP_QUERIES_LIMIT);
|
|
5454
5697
|
const categoryAgg = /* @__PURE__ */ new Map();
|
|
5455
5698
|
for (const [query, agg] of queryAgg) {
|
|
5456
|
-
const cat = categorizeQuery(query,
|
|
5699
|
+
const cat = categorizeQuery(query, projectBrandNames, canonicalDomain);
|
|
5457
5700
|
const bucket = categoryAgg.get(cat) ?? { clicks: 0, impressions: 0 };
|
|
5458
5701
|
bucket.clicks += agg.clicks;
|
|
5459
5702
|
bucket.impressions += agg.impressions;
|
|
@@ -5471,7 +5714,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5471
5714
|
const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
|
|
5472
5715
|
const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
|
|
5473
5716
|
const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
|
|
5474
|
-
const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q,
|
|
5717
|
+
const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectBrandNames, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
|
|
5475
5718
|
return {
|
|
5476
5719
|
periodStart,
|
|
5477
5720
|
periodEnd,
|
|
@@ -5488,7 +5731,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5488
5731
|
}
|
|
5489
5732
|
function buildGaSection(db, projectId) {
|
|
5490
5733
|
const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
|
|
5491
|
-
|
|
5734
|
+
and6(
|
|
5492
5735
|
eq13(gaTrafficWindowSummaries.projectId, projectId),
|
|
5493
5736
|
eq13(gaTrafficWindowSummaries.windowKey, "30d")
|
|
5494
5737
|
)
|
|
@@ -5642,7 +5885,7 @@ function buildAiReferrals(db, projectId) {
|
|
|
5642
5885
|
return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
|
|
5643
5886
|
}
|
|
5644
5887
|
function nonSubresourceReferralPathCondition() {
|
|
5645
|
-
return
|
|
5888
|
+
return sql4`
|
|
5646
5889
|
LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
|
|
5647
5890
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
|
|
5648
5891
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
|
|
@@ -5666,7 +5909,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
5666
5909
|
}
|
|
5667
5910
|
function buildServerActivity(db, projectId) {
|
|
5668
5911
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
5669
|
-
|
|
5912
|
+
and6(
|
|
5670
5913
|
eq13(trafficSources.projectId, projectId),
|
|
5671
5914
|
ne(trafficSources.status, TrafficSourceStatuses.archived)
|
|
5672
5915
|
)
|
|
@@ -5681,8 +5924,8 @@ function buildServerActivity(db, projectId) {
|
|
|
5681
5924
|
const priorStart = new Date(priorStartMs).toISOString();
|
|
5682
5925
|
const trendStart = new Date(trendStartMs).toISOString();
|
|
5683
5926
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5684
|
-
db.select({ total:
|
|
5685
|
-
|
|
5927
|
+
db.select({ total: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
5928
|
+
and6(
|
|
5686
5929
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5687
5930
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5688
5931
|
gte(crawlerEventsHourly.tsHour, windowStartIso),
|
|
@@ -5691,8 +5934,8 @@ function buildServerActivity(db, projectId) {
|
|
|
5691
5934
|
).get()?.total ?? 0
|
|
5692
5935
|
);
|
|
5693
5936
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5694
|
-
db.select({ total:
|
|
5695
|
-
|
|
5937
|
+
db.select({ total: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
5938
|
+
and6(
|
|
5696
5939
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5697
5940
|
ne(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5698
5941
|
gte(crawlerEventsHourly.tsHour, windowStartIso),
|
|
@@ -5701,8 +5944,8 @@ function buildServerActivity(db, projectId) {
|
|
|
5701
5944
|
).get()?.total ?? 0
|
|
5702
5945
|
);
|
|
5703
5946
|
const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
5704
|
-
db.select({ total:
|
|
5705
|
-
|
|
5947
|
+
db.select({ total: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
5948
|
+
and6(
|
|
5706
5949
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5707
5950
|
nonSubresourceReferralPathCondition(),
|
|
5708
5951
|
gte(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
@@ -5719,9 +5962,9 @@ function buildServerActivity(db, projectId) {
|
|
|
5719
5962
|
const crawlerByOperatorRows = db.select({
|
|
5720
5963
|
operator: crawlerEventsHourly.operator,
|
|
5721
5964
|
verificationStatus: crawlerEventsHourly.verificationStatus,
|
|
5722
|
-
hits:
|
|
5965
|
+
hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5723
5966
|
}).from(crawlerEventsHourly).where(
|
|
5724
|
-
|
|
5967
|
+
and6(
|
|
5725
5968
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5726
5969
|
gte(crawlerEventsHourly.tsHour, headlineStart),
|
|
5727
5970
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
@@ -5729,9 +5972,9 @@ function buildServerActivity(db, projectId) {
|
|
|
5729
5972
|
).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
|
|
5730
5973
|
const crawlerByOperatorPriorRows = db.select({
|
|
5731
5974
|
operator: crawlerEventsHourly.operator,
|
|
5732
|
-
hits:
|
|
5975
|
+
hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5733
5976
|
}).from(crawlerEventsHourly).where(
|
|
5734
|
-
|
|
5977
|
+
and6(
|
|
5735
5978
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5736
5979
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5737
5980
|
gte(crawlerEventsHourly.tsHour, priorStart),
|
|
@@ -5740,9 +5983,9 @@ function buildServerActivity(db, projectId) {
|
|
|
5740
5983
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
5741
5984
|
const referralByOperatorRows = db.select({
|
|
5742
5985
|
operator: aiReferralEventsHourly.operator,
|
|
5743
|
-
hits:
|
|
5986
|
+
hits: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
5744
5987
|
}).from(aiReferralEventsHourly).where(
|
|
5745
|
-
|
|
5988
|
+
and6(
|
|
5746
5989
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5747
5990
|
nonSubresourceReferralPathCondition(),
|
|
5748
5991
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
@@ -5780,16 +6023,16 @@ function buildServerActivity(db, projectId) {
|
|
|
5780
6023
|
);
|
|
5781
6024
|
const topPathsRows = db.select({
|
|
5782
6025
|
path: crawlerEventsHourly.pathNormalized,
|
|
5783
|
-
hits:
|
|
5784
|
-
operators:
|
|
6026
|
+
hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
6027
|
+
operators: sql4`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
5785
6028
|
}).from(crawlerEventsHourly).where(
|
|
5786
|
-
|
|
6029
|
+
and6(
|
|
5787
6030
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5788
6031
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5789
6032
|
gte(crawlerEventsHourly.tsHour, headlineStart),
|
|
5790
6033
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
5791
6034
|
)
|
|
5792
|
-
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(
|
|
6035
|
+
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc6(sql4`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
5793
6036
|
const topCrawledPaths = topPathsRows.map((r) => ({
|
|
5794
6037
|
path: r.path,
|
|
5795
6038
|
verifiedHits: Number(r.hits),
|
|
@@ -5797,16 +6040,16 @@ function buildServerActivity(db, projectId) {
|
|
|
5797
6040
|
}));
|
|
5798
6041
|
const referralProductsRows = db.select({
|
|
5799
6042
|
product: aiReferralEventsHourly.product,
|
|
5800
|
-
arrivals:
|
|
5801
|
-
landingPaths:
|
|
6043
|
+
arrivals: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
6044
|
+
landingPaths: sql4`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
5802
6045
|
}).from(aiReferralEventsHourly).where(
|
|
5803
|
-
|
|
6046
|
+
and6(
|
|
5804
6047
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5805
6048
|
nonSubresourceReferralPathCondition(),
|
|
5806
6049
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
5807
6050
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5808
6051
|
)
|
|
5809
|
-
).groupBy(aiReferralEventsHourly.product).orderBy(desc6(
|
|
6052
|
+
).groupBy(aiReferralEventsHourly.product).orderBy(desc6(sql4`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
|
|
5810
6053
|
const referralProducts = referralProductsRows.map((r) => ({
|
|
5811
6054
|
product: r.product,
|
|
5812
6055
|
arrivals: Number(r.arrivals),
|
|
@@ -5814,43 +6057,43 @@ function buildServerActivity(db, projectId) {
|
|
|
5814
6057
|
}));
|
|
5815
6058
|
const topReferralRows = db.select({
|
|
5816
6059
|
path: aiReferralEventsHourly.landingPathNormalized,
|
|
5817
|
-
arrivals:
|
|
5818
|
-
products:
|
|
6060
|
+
arrivals: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
6061
|
+
products: sql4`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
5819
6062
|
}).from(aiReferralEventsHourly).where(
|
|
5820
|
-
|
|
6063
|
+
and6(
|
|
5821
6064
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5822
6065
|
nonSubresourceReferralPathCondition(),
|
|
5823
6066
|
gte(aiReferralEventsHourly.tsHour, headlineStart),
|
|
5824
6067
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5825
6068
|
)
|
|
5826
|
-
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(
|
|
6069
|
+
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc6(sql4`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
5827
6070
|
const topReferralLandingPaths = topReferralRows.map((r) => ({
|
|
5828
6071
|
path: r.path,
|
|
5829
6072
|
arrivals: Number(r.arrivals),
|
|
5830
6073
|
distinctProducts: Number(r.products)
|
|
5831
6074
|
}));
|
|
5832
6075
|
const crawlerTrendRows = db.select({
|
|
5833
|
-
date:
|
|
5834
|
-
hits:
|
|
6076
|
+
date: sql4`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
|
|
6077
|
+
hits: sql4`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
5835
6078
|
}).from(crawlerEventsHourly).where(
|
|
5836
|
-
|
|
6079
|
+
and6(
|
|
5837
6080
|
eq13(crawlerEventsHourly.projectId, projectId),
|
|
5838
6081
|
eq13(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
5839
6082
|
gte(crawlerEventsHourly.tsHour, trendStart),
|
|
5840
6083
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
5841
6084
|
)
|
|
5842
|
-
).groupBy(
|
|
6085
|
+
).groupBy(sql4`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
5843
6086
|
const referralTrendRows = db.select({
|
|
5844
|
-
date:
|
|
5845
|
-
hits:
|
|
6087
|
+
date: sql4`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
|
|
6088
|
+
hits: sql4`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
5846
6089
|
}).from(aiReferralEventsHourly).where(
|
|
5847
|
-
|
|
6090
|
+
and6(
|
|
5848
6091
|
eq13(aiReferralEventsHourly.projectId, projectId),
|
|
5849
6092
|
nonSubresourceReferralPathCondition(),
|
|
5850
6093
|
gte(aiReferralEventsHourly.tsHour, trendStart),
|
|
5851
6094
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
5852
6095
|
)
|
|
5853
|
-
).groupBy(
|
|
6096
|
+
).groupBy(sql4`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
5854
6097
|
const dailyTrendMap = /* @__PURE__ */ new Map();
|
|
5855
6098
|
for (const r of crawlerTrendRows) {
|
|
5856
6099
|
const e = dailyTrendMap.get(r.date) ?? { verifiedCrawlerHits: 0, referralArrivals: 0 };
|
|
@@ -5919,7 +6162,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
5919
6162
|
return null;
|
|
5920
6163
|
}
|
|
5921
6164
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
5922
|
-
const visibilityRuns = db.select().from(runs).where(
|
|
6165
|
+
const visibilityRuns = db.select().from(runs).where(and6(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
5923
6166
|
const totalQueries = queryLookup.byId.size;
|
|
5924
6167
|
const points = [];
|
|
5925
6168
|
for (const run of visibilityRuns) {
|
|
@@ -5965,14 +6208,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
5965
6208
|
}
|
|
5966
6209
|
function buildInsightList(db, projectId, locationFilter) {
|
|
5967
6210
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
5968
|
-
|
|
6211
|
+
and6(
|
|
5969
6212
|
eq13(runs.projectId, projectId),
|
|
5970
6213
|
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
5971
|
-
|
|
6214
|
+
or3(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
|
|
5972
6215
|
)
|
|
5973
6216
|
).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
5974
6217
|
if (recentRunIds.length === 0) return [];
|
|
5975
|
-
const rows = db.select().from(insights).where(
|
|
6218
|
+
const rows = db.select().from(insights).where(and6(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
|
|
5976
6219
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
5977
6220
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
5978
6221
|
const recommendation = parseJsonColumn(r.recommendation, null);
|
|
@@ -6160,7 +6403,8 @@ function buildReportActionPlan(input) {
|
|
|
6160
6403
|
}
|
|
6161
6404
|
for (const [index, opportunity] of input.contentOpportunities.slice(0, 2).entries()) {
|
|
6162
6405
|
const verb = contentActionVerb(opportunity.action);
|
|
6163
|
-
const
|
|
6406
|
+
const bestPageUrl = opportunity.ourBestPage?.url;
|
|
6407
|
+
const hasUsablePage = !!bestPageUrl && bestPageUrl !== "/" && bestPageUrl.trim() !== "";
|
|
6164
6408
|
const evidence = [
|
|
6165
6409
|
`Opportunity score ${Math.round(opportunity.score)} with ${opportunity.actionConfidence} confidence`,
|
|
6166
6410
|
`Demand source: ${opportunity.demandSource}`
|
|
@@ -6169,17 +6413,22 @@ function buildReportActionPlan(input) {
|
|
|
6169
6413
|
evidence.push(`${opportunity.winningCompetitor.domain} is the current winning cited source`);
|
|
6170
6414
|
}
|
|
6171
6415
|
if (opportunity.ourBestPage) {
|
|
6172
|
-
|
|
6416
|
+
if (bestPageUrl === "/") {
|
|
6417
|
+
evidence.push("No topical page yet \u2014 homepage is the closest slug match");
|
|
6418
|
+
} else {
|
|
6419
|
+
evidence.push(`Best matching owned page: ${bestPageUrl}`);
|
|
6420
|
+
}
|
|
6173
6421
|
} else {
|
|
6174
6422
|
evidence.push("No matching owned page was found");
|
|
6175
6423
|
}
|
|
6424
|
+
const targetIsExistingPage = opportunity.action !== "create" && hasUsablePage;
|
|
6176
6425
|
actions.push({
|
|
6177
6426
|
audience: "both",
|
|
6178
6427
|
priority: 20 + index,
|
|
6179
6428
|
horizon: opportunity.actionConfidence === "high" ? "short-term" : "medium-term",
|
|
6180
6429
|
category: "content",
|
|
6181
6430
|
title: `${verb} content for "${opportunity.query}"`,
|
|
6182
|
-
action:
|
|
6431
|
+
action: targetIsExistingPage ? `${verb} the existing page so it directly answers the tracked query and cites the strongest supporting evidence.` : `${verb} a new page for "${opportunity.query}" that directly answers the query and earns citations from AI answer engines.`,
|
|
6183
6432
|
why: opportunity.drivers.length > 0 ? opportunity.drivers : ["Canonry ranked this as a content opportunity from search-demand and citation evidence."],
|
|
6184
6433
|
evidence,
|
|
6185
6434
|
successMetric: `A future check cites ${input.canonicalDomain} for "${opportunity.query}" and the matching GSC query/page improves.`,
|
|
@@ -6524,6 +6773,11 @@ function buildProjectReport(db, projectName) {
|
|
|
6524
6773
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
6525
6774
|
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
6526
6775
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
6776
|
+
const projectAliases = parseJsonColumn(project.aliases, []);
|
|
6777
|
+
const projectBrandNames = effectiveBrandNames({
|
|
6778
|
+
displayName: project.displayName,
|
|
6779
|
+
aliases: projectAliases
|
|
6780
|
+
});
|
|
6527
6781
|
const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
|
|
6528
6782
|
const competitorLandscape = buildCompetitorLandscape(
|
|
6529
6783
|
latestSnapshots,
|
|
@@ -6534,7 +6788,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6534
6788
|
const mentionLandscape = buildMentionLandscape(
|
|
6535
6789
|
latestSnapshots,
|
|
6536
6790
|
competitorDomains,
|
|
6537
|
-
|
|
6791
|
+
projectBrandNames,
|
|
6538
6792
|
projectDomains,
|
|
6539
6793
|
queryLookup
|
|
6540
6794
|
);
|
|
@@ -6543,7 +6797,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6543
6797
|
const gscSection = buildGscSection(
|
|
6544
6798
|
db,
|
|
6545
6799
|
project.id,
|
|
6546
|
-
|
|
6800
|
+
projectBrandNames,
|
|
6547
6801
|
project.canonicalDomain,
|
|
6548
6802
|
trackedQueries
|
|
6549
6803
|
);
|
|
@@ -6899,7 +7153,7 @@ function normalizeDomain2(domain) {
|
|
|
6899
7153
|
}
|
|
6900
7154
|
|
|
6901
7155
|
// ../api-routes/src/composites.ts
|
|
6902
|
-
import { eq as eq15, and as
|
|
7156
|
+
import { eq as eq15, and as and7, desc as desc7, sql as sql5, like, or as or4, inArray as inArray7 } from "drizzle-orm";
|
|
6903
7157
|
var TOP_INSIGHT_LIMIT = 5;
|
|
6904
7158
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
6905
7159
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -6952,6 +7206,7 @@ async function compositeRoutes(app) {
|
|
|
6952
7206
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
6953
7207
|
const configuredApiProviders = parseJsonColumn(project.providers, []).filter((p) => !p.startsWith("cdp:"));
|
|
6954
7208
|
const scores = {
|
|
7209
|
+
mention: buildMentionCoverage(latestSnapshots, { configuredApiProviders }),
|
|
6955
7210
|
visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
|
|
6956
7211
|
gapQueries: buildGapQueryScore(latestSnapshots),
|
|
6957
7212
|
indexCoverage: buildIndexCoverageScore(app, project.id),
|
|
@@ -7013,24 +7268,24 @@ async function compositeRoutes(app) {
|
|
|
7013
7268
|
rawResponse: querySnapshots.rawResponse,
|
|
7014
7269
|
createdAt: querySnapshots.createdAt
|
|
7015
7270
|
}).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
|
|
7016
|
-
|
|
7271
|
+
and7(
|
|
7017
7272
|
eq15(queries.projectId, project.id),
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7273
|
+
or4(
|
|
7274
|
+
sql5`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
7275
|
+
sql5`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
7276
|
+
sql5`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
|
|
7022
7277
|
like(queries.query, pattern)
|
|
7023
7278
|
)
|
|
7024
7279
|
)
|
|
7025
7280
|
).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
7026
7281
|
const insightMatches = app.db.select().from(insights).where(
|
|
7027
|
-
|
|
7282
|
+
and7(
|
|
7028
7283
|
eq15(insights.projectId, project.id),
|
|
7029
|
-
|
|
7284
|
+
or4(
|
|
7030
7285
|
like(insights.title, pattern),
|
|
7031
7286
|
like(insights.query, pattern),
|
|
7032
|
-
|
|
7033
|
-
|
|
7287
|
+
sql5`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
7288
|
+
sql5`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
7034
7289
|
)
|
|
7035
7290
|
)
|
|
7036
7291
|
).orderBy(desc7(insights.createdAt)).limit(limit + 1).all();
|
|
@@ -7102,6 +7357,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
7102
7357
|
provider: querySnapshots.provider,
|
|
7103
7358
|
model: querySnapshots.model,
|
|
7104
7359
|
citationState: querySnapshots.citationState,
|
|
7360
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
7105
7361
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
7106
7362
|
citedDomains: querySnapshots.citedDomains
|
|
7107
7363
|
}).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
|
|
@@ -7112,6 +7368,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
7112
7368
|
provider: row.provider,
|
|
7113
7369
|
model: row.model,
|
|
7114
7370
|
citationState: row.citationState,
|
|
7371
|
+
answerMentioned: row.answerMentioned,
|
|
7115
7372
|
competitorOverlap: parseJsonColumn(row.competitorOverlap, []),
|
|
7116
7373
|
citedDomains: parseJsonColumn(row.citedDomains, [])
|
|
7117
7374
|
});
|
|
@@ -7354,6 +7611,7 @@ function formatProject2(row) {
|
|
|
7354
7611
|
displayName: row.displayName,
|
|
7355
7612
|
canonicalDomain: row.canonicalDomain,
|
|
7356
7613
|
ownedDomains: parseJsonColumn(row.ownedDomains, []),
|
|
7614
|
+
aliases: parseJsonColumn(row.aliases, []),
|
|
7357
7615
|
country: row.country,
|
|
7358
7616
|
language: row.language,
|
|
7359
7617
|
tags: parseJsonColumn(row.tags, []),
|
|
@@ -7656,6 +7914,7 @@ var routeCatalog = [
|
|
|
7656
7914
|
displayName: stringSchema,
|
|
7657
7915
|
canonicalDomain: stringSchema,
|
|
7658
7916
|
ownedDomains: stringArraySchema,
|
|
7917
|
+
aliases: stringArraySchema,
|
|
7659
7918
|
country: stringSchema,
|
|
7660
7919
|
language: stringSchema,
|
|
7661
7920
|
tags: stringArraySchema,
|
|
@@ -7705,6 +7964,18 @@ var routeCatalog = [
|
|
|
7705
7964
|
404: { description: "Project not found." }
|
|
7706
7965
|
}
|
|
7707
7966
|
},
|
|
7967
|
+
{
|
|
7968
|
+
method: "get",
|
|
7969
|
+
path: "/api/v1/projects/{name}/delete-preview",
|
|
7970
|
+
summary: "Preview the cascade impact of deleting a project",
|
|
7971
|
+
description: "Read-only impact summary backing `canonry project delete --dry-run`. Returns counts of rows that would cascade-delete (queries, competitors, runs, snapshots, insights) and rows that would be detached (audit_log \u2014 `project_id` set to NULL).",
|
|
7972
|
+
tags: ["projects"],
|
|
7973
|
+
parameters: [nameParameter],
|
|
7974
|
+
responses: {
|
|
7975
|
+
200: { description: "Preview of cascade impact." },
|
|
7976
|
+
404: { description: "Project not found." }
|
|
7977
|
+
}
|
|
7978
|
+
},
|
|
7708
7979
|
{
|
|
7709
7980
|
method: "post",
|
|
7710
7981
|
path: "/api/v1/projects/{name}/locations",
|
|
@@ -10710,8 +10981,8 @@ async function openApiRoutes(app, opts = {}) {
|
|
|
10710
10981
|
return reply.type("application/json").send(buildOpenApiDocument(opts));
|
|
10711
10982
|
});
|
|
10712
10983
|
}
|
|
10713
|
-
function buildOperationId(method,
|
|
10714
|
-
const parts =
|
|
10984
|
+
function buildOperationId(method, path16) {
|
|
10985
|
+
const parts = path16.split("/").filter(Boolean).map((part) => {
|
|
10715
10986
|
if (part.startsWith("{") && part.endsWith("}")) {
|
|
10716
10987
|
return `by-${part.slice(1, -1)}`;
|
|
10717
10988
|
}
|
|
@@ -10875,7 +11146,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
10875
11146
|
|
|
10876
11147
|
// ../api-routes/src/schedules.ts
|
|
10877
11148
|
import crypto11 from "crypto";
|
|
10878
|
-
import { and as
|
|
11149
|
+
import { and as and8, eq as eq16 } from "drizzle-orm";
|
|
10879
11150
|
function parseKindParam(raw) {
|
|
10880
11151
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
10881
11152
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -10941,7 +11212,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
10941
11212
|
}
|
|
10942
11213
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10943
11214
|
const enabledInt = enabled === false ? 0 : 1;
|
|
10944
|
-
const existing = app.db.select().from(schedules).where(
|
|
11215
|
+
const existing = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10945
11216
|
if (existing) {
|
|
10946
11217
|
app.db.update(schedules).set({
|
|
10947
11218
|
cronExpr,
|
|
@@ -10975,13 +11246,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
10975
11246
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
10976
11247
|
});
|
|
10977
11248
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
10978
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11249
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10979
11250
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
10980
11251
|
});
|
|
10981
11252
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
10982
11253
|
const project = resolveProject(app.db, request.params.name);
|
|
10983
11254
|
const kind = parseKindParam(request.query?.kind);
|
|
10984
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11255
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10985
11256
|
if (!schedule) {
|
|
10986
11257
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10987
11258
|
}
|
|
@@ -10990,7 +11261,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
10990
11261
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
10991
11262
|
const project = resolveProject(app.db, request.params.name);
|
|
10992
11263
|
const kind = parseKindParam(request.query?.kind);
|
|
10993
|
-
const schedule = app.db.select().from(schedules).where(
|
|
11264
|
+
const schedule = app.db.select().from(schedules).where(and8(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10994
11265
|
if (!schedule) {
|
|
10995
11266
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10996
11267
|
}
|
|
@@ -11029,7 +11300,8 @@ function formatSchedule(row) {
|
|
|
11029
11300
|
import crypto12 from "crypto";
|
|
11030
11301
|
import { eq as eq17 } from "drizzle-orm";
|
|
11031
11302
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
11032
|
-
async function notificationRoutes(app) {
|
|
11303
|
+
async function notificationRoutes(app, opts = {}) {
|
|
11304
|
+
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
11033
11305
|
app.get("/notifications/events", async (_request, reply) => {
|
|
11034
11306
|
return reply.send(VALID_EVENTS);
|
|
11035
11307
|
});
|
|
@@ -11037,7 +11309,7 @@ async function notificationRoutes(app) {
|
|
|
11037
11309
|
const project = resolveProject(app.db, request.params.name);
|
|
11038
11310
|
const { channel, url, events, source } = request.body ?? {};
|
|
11039
11311
|
if (channel !== "webhook") throw validationError('Only "webhook" channel is supported');
|
|
11040
|
-
const urlCheck = await resolveWebhookTarget(url ?? "");
|
|
11312
|
+
const urlCheck = await resolveWebhookTarget(url ?? "", { allowLoopback });
|
|
11041
11313
|
if (!urlCheck.ok) throw validationError(urlCheck.message);
|
|
11042
11314
|
if (!events?.length) throw validationError('"events" must be a non-empty array');
|
|
11043
11315
|
const invalid = events.filter((e) => !VALID_EVENTS.includes(e));
|
|
@@ -11098,7 +11370,7 @@ async function notificationRoutes(app) {
|
|
|
11098
11370
|
throw notFound("Notification", request.params.id);
|
|
11099
11371
|
}
|
|
11100
11372
|
const config = parseJsonColumn(notification.config, { url: "", events: [] });
|
|
11101
|
-
const urlCheck = await resolveWebhookTarget(config.url);
|
|
11373
|
+
const urlCheck = await resolveWebhookTarget(config.url, { allowLoopback });
|
|
11102
11374
|
if (!urlCheck.ok) throw validationError(`Stored webhook URL is invalid: ${urlCheck.message}`);
|
|
11103
11375
|
const payload = {
|
|
11104
11376
|
source: "canonry",
|
|
@@ -11146,7 +11418,7 @@ function formatNotification(row) {
|
|
|
11146
11418
|
|
|
11147
11419
|
// ../api-routes/src/google.ts
|
|
11148
11420
|
import crypto14 from "crypto";
|
|
11149
|
-
import { eq as eq18, and as
|
|
11421
|
+
import { eq as eq18, and as and9, desc as desc8, sql as sql6 } from "drizzle-orm";
|
|
11150
11422
|
|
|
11151
11423
|
// ../integration-google/src/constants.ts
|
|
11152
11424
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -12156,7 +12428,23 @@ async function getValidToken(store, domain, connectionType, clientId, clientSecr
|
|
|
12156
12428
|
};
|
|
12157
12429
|
}
|
|
12158
12430
|
async function googleRoutes(app, opts) {
|
|
12159
|
-
|
|
12431
|
+
if (opts.googleStateSecret === void 0) {
|
|
12432
|
+
app.log.warn(
|
|
12433
|
+
"googleStateSecret is not configured \u2014 Google OAuth routes will not be registered. Set GOOGLE_STATE_SECRET to enable Google integrations."
|
|
12434
|
+
);
|
|
12435
|
+
return;
|
|
12436
|
+
}
|
|
12437
|
+
if (opts.googleStateSecret === "") {
|
|
12438
|
+
throw new Error(
|
|
12439
|
+
"googleStateSecret is empty. Set a non-empty secret (e.g. `openssl rand -hex 32`) via the GOOGLE_STATE_SECRET environment variable."
|
|
12440
|
+
);
|
|
12441
|
+
}
|
|
12442
|
+
if (opts.googleStateSecret === "insecure-default-secret") {
|
|
12443
|
+
throw new Error(
|
|
12444
|
+
"googleStateSecret is set to the legacy insecure default. Generate a real secret (e.g. `openssl rand -hex 32`) and set GOOGLE_STATE_SECRET."
|
|
12445
|
+
);
|
|
12446
|
+
}
|
|
12447
|
+
const stateSecret = opts.googleStateSecret;
|
|
12160
12448
|
function getAuthConfig() {
|
|
12161
12449
|
return opts.getGoogleAuthConfig?.() ?? {};
|
|
12162
12450
|
}
|
|
@@ -12356,14 +12644,14 @@ async function googleRoutes(app, opts) {
|
|
|
12356
12644
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
12357
12645
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
12358
12646
|
const conditions = [eq18(gscSearchData.projectId, project.id)];
|
|
12359
|
-
if (startDate) conditions.push(
|
|
12360
|
-
else if (cutoffDate) conditions.push(
|
|
12361
|
-
if (endDate) conditions.push(
|
|
12362
|
-
if (query) conditions.push(
|
|
12363
|
-
if (page) conditions.push(
|
|
12647
|
+
if (startDate) conditions.push(sql6`${gscSearchData.date} >= ${startDate}`);
|
|
12648
|
+
else if (cutoffDate) conditions.push(sql6`${gscSearchData.date} >= ${cutoffDate}`);
|
|
12649
|
+
if (endDate) conditions.push(sql6`${gscSearchData.date} <= ${endDate}`);
|
|
12650
|
+
if (query) conditions.push(sql6`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
12651
|
+
if (page) conditions.push(sql6`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
12364
12652
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
12365
12653
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
12366
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
12654
|
+
const rows = app.db.select().from(gscSearchData).where(and9(...conditions)).orderBy(desc8(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
12367
12655
|
return rows.map((r) => ({
|
|
12368
12656
|
date: r.date,
|
|
12369
12657
|
query: r.query,
|
|
@@ -12437,7 +12725,7 @@ async function googleRoutes(app, opts) {
|
|
|
12437
12725
|
const { url, limit } = request.query;
|
|
12438
12726
|
const conditions = [eq18(gscUrlInspections.projectId, project.id)];
|
|
12439
12727
|
if (url) conditions.push(eq18(gscUrlInspections.url, url));
|
|
12440
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
12728
|
+
const rows = app.db.select().from(gscUrlInspections).where(and9(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
12441
12729
|
return rows.map((r) => ({
|
|
12442
12730
|
id: r.id,
|
|
12443
12731
|
url: r.url,
|
|
@@ -12789,7 +13077,7 @@ async function googleRoutes(app, opts) {
|
|
|
12789
13077
|
|
|
12790
13078
|
// ../api-routes/src/bing.ts
|
|
12791
13079
|
import crypto15 from "crypto";
|
|
12792
|
-
import { eq as eq19, and as
|
|
13080
|
+
import { eq as eq19, and as and10, desc as desc9 } from "drizzle-orm";
|
|
12793
13081
|
|
|
12794
13082
|
// ../integration-bing/src/constants.ts
|
|
12795
13083
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -13203,7 +13491,7 @@ async function bingRoutes(app, opts) {
|
|
|
13203
13491
|
requireConnectionStore();
|
|
13204
13492
|
const project = resolveProject(app.db, request.params.name);
|
|
13205
13493
|
const { url, limit } = request.query;
|
|
13206
|
-
const whereClause = url ?
|
|
13494
|
+
const whereClause = url ? and10(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
|
|
13207
13495
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
13208
13496
|
return filtered.map((r) => ({
|
|
13209
13497
|
id: r.id,
|
|
@@ -13435,7 +13723,7 @@ async function bingRoutes(app, opts) {
|
|
|
13435
13723
|
import fs from "fs";
|
|
13436
13724
|
import path from "path";
|
|
13437
13725
|
import os2 from "os";
|
|
13438
|
-
import { eq as eq20, and as
|
|
13726
|
+
import { eq as eq20, and as and11 } from "drizzle-orm";
|
|
13439
13727
|
function getScreenshotDir() {
|
|
13440
13728
|
return path.join(os2.homedir(), ".canonry", "screenshots");
|
|
13441
13729
|
}
|
|
@@ -13508,7 +13796,7 @@ async function cdpRoutes(app, opts) {
|
|
|
13508
13796
|
async (request, reply) => {
|
|
13509
13797
|
const project = resolveProject(app.db, request.params.name);
|
|
13510
13798
|
const { runId } = request.params;
|
|
13511
|
-
const run = app.db.select().from(runs).where(
|
|
13799
|
+
const run = app.db.select().from(runs).where(and11(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
|
|
13512
13800
|
if (!run) {
|
|
13513
13801
|
const err = notFound("Run", runId);
|
|
13514
13802
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -13605,7 +13893,7 @@ async function cdpRoutes(app, opts) {
|
|
|
13605
13893
|
|
|
13606
13894
|
// ../api-routes/src/ga.ts
|
|
13607
13895
|
import crypto16 from "crypto";
|
|
13608
|
-
import { eq as eq21, desc as desc10, and as
|
|
13896
|
+
import { eq as eq21, desc as desc10, and as and12, sql as sql7 } from "drizzle-orm";
|
|
13609
13897
|
function gaLog(level, action, ctx) {
|
|
13610
13898
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
13611
13899
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -13900,10 +14188,10 @@ async function ga4Routes(app, opts) {
|
|
|
13900
14188
|
app.db.transaction((tx) => {
|
|
13901
14189
|
if (syncTraffic) {
|
|
13902
14190
|
tx.delete(gaTrafficSnapshots).where(
|
|
13903
|
-
|
|
14191
|
+
and12(
|
|
13904
14192
|
eq21(gaTrafficSnapshots.projectId, project.id),
|
|
13905
|
-
|
|
13906
|
-
|
|
14193
|
+
sql7`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
14194
|
+
sql7`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
13907
14195
|
)
|
|
13908
14196
|
).run();
|
|
13909
14197
|
for (const row of rows) {
|
|
@@ -13924,10 +14212,10 @@ async function ga4Routes(app, opts) {
|
|
|
13924
14212
|
}
|
|
13925
14213
|
if (syncAi) {
|
|
13926
14214
|
tx.delete(gaAiReferrals).where(
|
|
13927
|
-
|
|
14215
|
+
and12(
|
|
13928
14216
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13929
|
-
|
|
13930
|
-
|
|
14217
|
+
sql7`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
14218
|
+
sql7`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
13931
14219
|
)
|
|
13932
14220
|
).run();
|
|
13933
14221
|
for (const row of aiReferrals) {
|
|
@@ -13950,10 +14238,10 @@ async function ga4Routes(app, opts) {
|
|
|
13950
14238
|
}
|
|
13951
14239
|
if (syncSocial) {
|
|
13952
14240
|
tx.delete(gaSocialReferrals).where(
|
|
13953
|
-
|
|
14241
|
+
and12(
|
|
13954
14242
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13955
|
-
|
|
13956
|
-
|
|
14243
|
+
sql7`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
14244
|
+
sql7`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
13957
14245
|
)
|
|
13958
14246
|
).run();
|
|
13959
14247
|
for (const row of socialReferrals) {
|
|
@@ -14043,65 +14331,65 @@ async function ga4Routes(app, opts) {
|
|
|
14043
14331
|
const cutoff = windowCutoff(window);
|
|
14044
14332
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
14045
14333
|
const snapshotConditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
|
|
14046
|
-
if (cutoffDate) snapshotConditions.push(
|
|
14334
|
+
if (cutoffDate) snapshotConditions.push(sql7`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
14047
14335
|
const aiConditions = [eq21(gaAiReferrals.projectId, project.id)];
|
|
14048
|
-
if (cutoffDate) aiConditions.push(
|
|
14336
|
+
if (cutoffDate) aiConditions.push(sql7`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
14049
14337
|
const socialConditions = [eq21(gaSocialReferrals.projectId, project.id)];
|
|
14050
|
-
if (cutoffDate) socialConditions.push(
|
|
14338
|
+
if (cutoffDate) socialConditions.push(sql7`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
14051
14339
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
14052
14340
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
14053
14341
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
14054
14342
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
14055
14343
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
14056
14344
|
}).from(gaTrafficWindowSummaries).where(
|
|
14057
|
-
|
|
14345
|
+
and12(
|
|
14058
14346
|
eq21(gaTrafficWindowSummaries.projectId, project.id),
|
|
14059
14347
|
eq21(gaTrafficWindowSummaries.windowKey, window)
|
|
14060
14348
|
)
|
|
14061
14349
|
).get() : null;
|
|
14062
14350
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
14063
|
-
totalSessions:
|
|
14064
|
-
totalOrganicSessions:
|
|
14065
|
-
totalUsers:
|
|
14066
|
-
}).from(gaTrafficSnapshots).where(
|
|
14351
|
+
totalSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
14352
|
+
totalOrganicSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
14353
|
+
totalUsers: sql7`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
14354
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get() : null;
|
|
14067
14355
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
14068
14356
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
14069
14357
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
14070
14358
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
14071
14359
|
}).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
|
|
14072
14360
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
14073
|
-
totalDirectSessions:
|
|
14074
|
-
}).from(gaTrafficSnapshots).where(
|
|
14361
|
+
totalDirectSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
14362
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).get();
|
|
14075
14363
|
const summaryMeta = app.db.select({
|
|
14076
14364
|
periodStart: gaTrafficSummaries.periodStart,
|
|
14077
14365
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
14078
14366
|
}).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
|
|
14079
14367
|
const rows = app.db.select({
|
|
14080
|
-
landingPage:
|
|
14081
|
-
sessions:
|
|
14082
|
-
organicSessions:
|
|
14083
|
-
directSessions:
|
|
14084
|
-
users:
|
|
14085
|
-
}).from(gaTrafficSnapshots).where(
|
|
14368
|
+
landingPage: sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
14369
|
+
sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
|
|
14370
|
+
organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
14371
|
+
directSessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
14372
|
+
users: sql7`SUM(${gaTrafficSnapshots.users})`
|
|
14373
|
+
}).from(gaTrafficSnapshots).where(and12(...snapshotConditions)).groupBy(sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql7`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
14086
14374
|
const aiReferralRows = app.db.select({
|
|
14087
14375
|
source: gaAiReferrals.source,
|
|
14088
14376
|
medium: gaAiReferrals.medium,
|
|
14089
14377
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
14090
|
-
sessions:
|
|
14091
|
-
users:
|
|
14092
|
-
}).from(gaAiReferrals).where(
|
|
14378
|
+
sessions: sql7`SUM(${gaAiReferrals.sessions})`,
|
|
14379
|
+
users: sql7`SUM(${gaAiReferrals.users})`
|
|
14380
|
+
}).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
14093
14381
|
const aiReferralLandingPageRows = app.db.select({
|
|
14094
14382
|
source: gaAiReferrals.source,
|
|
14095
14383
|
medium: gaAiReferrals.medium,
|
|
14096
14384
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
14097
|
-
landingPage:
|
|
14098
|
-
sessions:
|
|
14099
|
-
users:
|
|
14100
|
-
}).from(gaAiReferrals).where(
|
|
14385
|
+
landingPage: sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
14386
|
+
sessions: sql7`SUM(${gaAiReferrals.sessions})`,
|
|
14387
|
+
users: sql7`SUM(${gaAiReferrals.users})`
|
|
14388
|
+
}).from(gaAiReferrals).where(and12(...aiConditions)).groupBy(
|
|
14101
14389
|
gaAiReferrals.source,
|
|
14102
14390
|
gaAiReferrals.medium,
|
|
14103
14391
|
gaAiReferrals.sourceDimension,
|
|
14104
|
-
|
|
14392
|
+
sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
14105
14393
|
).all();
|
|
14106
14394
|
const aiReferrals = pickWinningDimension(
|
|
14107
14395
|
aiReferralRows,
|
|
@@ -14112,10 +14400,10 @@ async function ga4Routes(app, opts) {
|
|
|
14112
14400
|
(r) => `${r.source}\0${r.medium}\0${r.landingPage}`
|
|
14113
14401
|
);
|
|
14114
14402
|
const aiDeduped = app.db.select({
|
|
14115
|
-
sessions:
|
|
14116
|
-
users:
|
|
14403
|
+
sessions: sql7`COALESCE(SUM(max_sessions), 0)`,
|
|
14404
|
+
users: sql7`COALESCE(SUM(max_users), 0)`
|
|
14117
14405
|
}).from(
|
|
14118
|
-
|
|
14406
|
+
sql7`(
|
|
14119
14407
|
SELECT date, source, medium,
|
|
14120
14408
|
MAX(dimension_sessions) AS max_sessions,
|
|
14121
14409
|
MAX(dimension_users) AS max_users
|
|
@@ -14124,7 +14412,7 @@ async function ga4Routes(app, opts) {
|
|
|
14124
14412
|
SUM(sessions) AS dimension_sessions,
|
|
14125
14413
|
SUM(users) AS dimension_users
|
|
14126
14414
|
FROM ga_ai_referrals
|
|
14127
|
-
WHERE project_id = ${project.id}${cutoffDate ?
|
|
14415
|
+
WHERE project_id = ${project.id}${cutoffDate ? sql7` AND date >= ${cutoffDate}` : sql7``}
|
|
14128
14416
|
GROUP BY date, source, medium, source_dimension
|
|
14129
14417
|
)
|
|
14130
14418
|
GROUP BY date, source, medium
|
|
@@ -14132,9 +14420,9 @@ async function ga4Routes(app, opts) {
|
|
|
14132
14420
|
).get();
|
|
14133
14421
|
const aiBySessionRows = app.db.select({
|
|
14134
14422
|
channelGroup: gaAiReferrals.channelGroup,
|
|
14135
|
-
sessions:
|
|
14136
|
-
users:
|
|
14137
|
-
}).from(gaAiReferrals).where(
|
|
14423
|
+
sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
14424
|
+
users: sql7`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
14425
|
+
}).from(gaAiReferrals).where(and12(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
14138
14426
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
14139
14427
|
let aiBySessionUsers = 0;
|
|
14140
14428
|
for (const row of aiBySessionRows) {
|
|
@@ -14146,13 +14434,13 @@ async function ga4Routes(app, opts) {
|
|
|
14146
14434
|
source: gaSocialReferrals.source,
|
|
14147
14435
|
medium: gaSocialReferrals.medium,
|
|
14148
14436
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
14149
|
-
sessions:
|
|
14150
|
-
users:
|
|
14151
|
-
}).from(gaSocialReferrals).where(
|
|
14437
|
+
sessions: sql7`SUM(${gaSocialReferrals.sessions})`,
|
|
14438
|
+
users: sql7`SUM(${gaSocialReferrals.users})`
|
|
14439
|
+
}).from(gaSocialReferrals).where(and12(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql7`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
14152
14440
|
const socialTotals = app.db.select({
|
|
14153
|
-
sessions:
|
|
14154
|
-
users:
|
|
14155
|
-
}).from(gaSocialReferrals).where(
|
|
14441
|
+
sessions: sql7`SUM(${gaSocialReferrals.sessions})`,
|
|
14442
|
+
users: sql7`SUM(${gaSocialReferrals.users})`
|
|
14443
|
+
}).from(gaSocialReferrals).where(and12(...socialConditions)).get();
|
|
14156
14444
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
14157
14445
|
const total = summaryRow?.totalSessions ?? 0;
|
|
14158
14446
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
@@ -14234,21 +14522,21 @@ async function ga4Routes(app, opts) {
|
|
|
14234
14522
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
14235
14523
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
14236
14524
|
const conditions = [eq21(gaAiReferrals.projectId, project.id)];
|
|
14237
|
-
if (cutoffDate) conditions.push(
|
|
14525
|
+
if (cutoffDate) conditions.push(sql7`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
14238
14526
|
const rows = app.db.select({
|
|
14239
14527
|
date: gaAiReferrals.date,
|
|
14240
14528
|
source: gaAiReferrals.source,
|
|
14241
14529
|
medium: gaAiReferrals.medium,
|
|
14242
|
-
landingPage:
|
|
14530
|
+
landingPage: sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
14243
14531
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
14244
|
-
sessions:
|
|
14245
|
-
users:
|
|
14246
|
-
}).from(gaAiReferrals).where(
|
|
14532
|
+
sessions: sql7`SUM(${gaAiReferrals.sessions})`,
|
|
14533
|
+
users: sql7`SUM(${gaAiReferrals.users})`
|
|
14534
|
+
}).from(gaAiReferrals).where(and12(...conditions)).groupBy(
|
|
14247
14535
|
gaAiReferrals.date,
|
|
14248
14536
|
gaAiReferrals.source,
|
|
14249
14537
|
gaAiReferrals.medium,
|
|
14250
14538
|
gaAiReferrals.sourceDimension,
|
|
14251
|
-
|
|
14539
|
+
sql7`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
14252
14540
|
).orderBy(gaAiReferrals.date).all();
|
|
14253
14541
|
return rows;
|
|
14254
14542
|
});
|
|
@@ -14257,7 +14545,7 @@ async function ga4Routes(app, opts) {
|
|
|
14257
14545
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
14258
14546
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
14259
14547
|
const conditions = [eq21(gaSocialReferrals.projectId, project.id)];
|
|
14260
|
-
if (cutoffDate) conditions.push(
|
|
14548
|
+
if (cutoffDate) conditions.push(sql7`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
14261
14549
|
const rows = app.db.select({
|
|
14262
14550
|
date: gaSocialReferrals.date,
|
|
14263
14551
|
source: gaSocialReferrals.source,
|
|
@@ -14265,7 +14553,7 @@ async function ga4Routes(app, opts) {
|
|
|
14265
14553
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
14266
14554
|
sessions: gaSocialReferrals.sessions,
|
|
14267
14555
|
users: gaSocialReferrals.users
|
|
14268
|
-
}).from(gaSocialReferrals).where(
|
|
14556
|
+
}).from(gaSocialReferrals).where(and12(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
14269
14557
|
return rows;
|
|
14270
14558
|
});
|
|
14271
14559
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -14278,10 +14566,10 @@ async function ga4Routes(app, opts) {
|
|
|
14278
14566
|
d.setDate(d.getDate() - n);
|
|
14279
14567
|
return fmt(d);
|
|
14280
14568
|
};
|
|
14281
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
14569
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(
|
|
14282
14570
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14283
|
-
|
|
14284
|
-
|
|
14571
|
+
sql7`${gaSocialReferrals.date} >= ${from}`,
|
|
14572
|
+
sql7`${gaSocialReferrals.date} < ${to}`
|
|
14285
14573
|
)).get();
|
|
14286
14574
|
const current7d = sumSocial(daysAgo2(7), fmt(today));
|
|
14287
14575
|
const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
|
|
@@ -14290,19 +14578,19 @@ async function ga4Routes(app, opts) {
|
|
|
14290
14578
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
14291
14579
|
const sourceCurrent = app.db.select({
|
|
14292
14580
|
source: gaSocialReferrals.source,
|
|
14293
|
-
sessions:
|
|
14294
|
-
}).from(gaSocialReferrals).where(
|
|
14581
|
+
sessions: sql7`SUM(${gaSocialReferrals.sessions})`
|
|
14582
|
+
}).from(gaSocialReferrals).where(and12(
|
|
14295
14583
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14296
|
-
|
|
14297
|
-
|
|
14584
|
+
sql7`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
14585
|
+
sql7`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
14298
14586
|
)).groupBy(gaSocialReferrals.source).all();
|
|
14299
14587
|
const sourcePrev = app.db.select({
|
|
14300
14588
|
source: gaSocialReferrals.source,
|
|
14301
|
-
sessions:
|
|
14302
|
-
}).from(gaSocialReferrals).where(
|
|
14589
|
+
sessions: sql7`SUM(${gaSocialReferrals.sessions})`
|
|
14590
|
+
}).from(gaSocialReferrals).where(and12(
|
|
14303
14591
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
14304
|
-
|
|
14305
|
-
|
|
14592
|
+
sql7`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
14593
|
+
sql7`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
14306
14594
|
)).groupBy(gaSocialReferrals.source).all();
|
|
14307
14595
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
14308
14596
|
let biggestMover = null;
|
|
@@ -14341,16 +14629,16 @@ async function ga4Routes(app, opts) {
|
|
|
14341
14629
|
return fmt(d);
|
|
14342
14630
|
};
|
|
14343
14631
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
14344
|
-
const sumTotal = (from, to) => app.db.select({ sessions:
|
|
14345
|
-
const sumOrganic = (from, to) => app.db.select({ sessions:
|
|
14346
|
-
const sumDirect = (from, to) => app.db.select({ sessions:
|
|
14347
|
-
const sumAi = (from, to) => app.db.select({ sessions:
|
|
14632
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14633
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14634
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and12(eq21(gaTrafficSnapshots.projectId, project.id), sql7`${gaTrafficSnapshots.date} >= ${from}`, sql7`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
14635
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14348
14636
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14349
|
-
|
|
14350
|
-
|
|
14637
|
+
sql7`${gaAiReferrals.date} >= ${from}`,
|
|
14638
|
+
sql7`${gaAiReferrals.date} < ${to}`,
|
|
14351
14639
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
14352
14640
|
)).get();
|
|
14353
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
14641
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql7`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${from}`, sql7`${gaSocialReferrals.date} < ${to}`)).get();
|
|
14354
14642
|
const todayStr = fmt(today);
|
|
14355
14643
|
const buildTrend = (sum) => {
|
|
14356
14644
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -14359,16 +14647,16 @@ async function ga4Routes(app, opts) {
|
|
|
14359
14647
|
const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
|
|
14360
14648
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
14361
14649
|
};
|
|
14362
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
14650
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14363
14651
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14364
|
-
|
|
14365
|
-
|
|
14652
|
+
sql7`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
|
|
14653
|
+
sql7`${gaAiReferrals.date} < ${todayStr}`,
|
|
14366
14654
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
14367
14655
|
)).groupBy(gaAiReferrals.source).all();
|
|
14368
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
14656
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql7`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and12(
|
|
14369
14657
|
eq21(gaAiReferrals.projectId, project.id),
|
|
14370
|
-
|
|
14371
|
-
|
|
14658
|
+
sql7`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
|
|
14659
|
+
sql7`${gaAiReferrals.date} < ${daysAgo2(7)}`,
|
|
14372
14660
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
14373
14661
|
)).groupBy(gaAiReferrals.source).all();
|
|
14374
14662
|
const findBiggestMover = (current, prev) => {
|
|
@@ -14385,8 +14673,8 @@ async function ga4Routes(app, opts) {
|
|
|
14385
14673
|
}
|
|
14386
14674
|
return mover;
|
|
14387
14675
|
};
|
|
14388
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
14389
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
14676
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql7`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql7`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
14677
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql7`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and12(eq21(gaSocialReferrals.projectId, project.id), sql7`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql7`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
14390
14678
|
return {
|
|
14391
14679
|
total: buildTrend(sumTotal),
|
|
14392
14680
|
organic: buildTrend(sumOrganic),
|
|
@@ -14402,13 +14690,13 @@ async function ga4Routes(app, opts) {
|
|
|
14402
14690
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
14403
14691
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
14404
14692
|
const conditions = [eq21(gaTrafficSnapshots.projectId, project.id)];
|
|
14405
|
-
if (cutoffDate) conditions.push(
|
|
14693
|
+
if (cutoffDate) conditions.push(sql7`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
14406
14694
|
const rows = app.db.select({
|
|
14407
14695
|
date: gaTrafficSnapshots.date,
|
|
14408
|
-
sessions:
|
|
14409
|
-
organicSessions:
|
|
14410
|
-
users:
|
|
14411
|
-
}).from(gaTrafficSnapshots).where(
|
|
14696
|
+
sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
|
|
14697
|
+
organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
14698
|
+
users: sql7`SUM(${gaTrafficSnapshots.users})`
|
|
14699
|
+
}).from(gaTrafficSnapshots).where(and12(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
14412
14700
|
return rows.map((r) => ({
|
|
14413
14701
|
date: r.date,
|
|
14414
14702
|
sessions: r.sessions ?? 0,
|
|
@@ -14420,11 +14708,11 @@ async function ga4Routes(app, opts) {
|
|
|
14420
14708
|
const project = resolveProject(app.db, request.params.name);
|
|
14421
14709
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
14422
14710
|
const trafficPages = app.db.select({
|
|
14423
|
-
landingPage:
|
|
14424
|
-
sessions:
|
|
14425
|
-
organicSessions:
|
|
14426
|
-
users:
|
|
14427
|
-
}).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(
|
|
14711
|
+
landingPage: sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
14712
|
+
sessions: sql7`SUM(${gaTrafficSnapshots.sessions})`,
|
|
14713
|
+
organicSessions: sql7`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
14714
|
+
users: sql7`SUM(${gaTrafficSnapshots.users})`
|
|
14715
|
+
}).from(gaTrafficSnapshots).where(eq21(gaTrafficSnapshots.projectId, project.id)).groupBy(sql7`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql7`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
14428
14716
|
return {
|
|
14429
14717
|
pages: trafficPages.map((r) => ({
|
|
14430
14718
|
landingPage: r.landingPage,
|
|
@@ -14640,10 +14928,10 @@ function buildAuthErrorMessage(res, responseText) {
|
|
|
14640
14928
|
}
|
|
14641
14929
|
return "WordPress credentials are invalid or lack permission for this action";
|
|
14642
14930
|
}
|
|
14643
|
-
async function fetchJson(connection, siteUrl,
|
|
14931
|
+
async function fetchJson(connection, siteUrl, path16, init) {
|
|
14644
14932
|
if (siteUrl.startsWith("http:")) {
|
|
14645
14933
|
}
|
|
14646
|
-
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${
|
|
14934
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path16}`, {
|
|
14647
14935
|
...init,
|
|
14648
14936
|
headers: {
|
|
14649
14937
|
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
@@ -16061,7 +16349,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
16061
16349
|
|
|
16062
16350
|
// ../api-routes/src/backlinks.ts
|
|
16063
16351
|
import crypto18 from "crypto";
|
|
16064
|
-
import { and as
|
|
16352
|
+
import { and as and14, asc as asc2, desc as desc11, eq as eq22, sql as sql8 } from "drizzle-orm";
|
|
16065
16353
|
|
|
16066
16354
|
// ../integration-commoncrawl/src/constants.ts
|
|
16067
16355
|
import os3 from "os";
|
|
@@ -16236,7 +16524,7 @@ function parseContentLength2(value) {
|
|
|
16236
16524
|
|
|
16237
16525
|
// ../integration-commoncrawl/src/plugin-resolver.ts
|
|
16238
16526
|
import fs3 from "fs";
|
|
16239
|
-
import { createRequire as
|
|
16527
|
+
import { createRequire as createRequire3 } from "module";
|
|
16240
16528
|
import path4 from "path";
|
|
16241
16529
|
function pluginDirFor(pkgJson) {
|
|
16242
16530
|
return path4.dirname(pkgJson);
|
|
@@ -16255,7 +16543,7 @@ function loadDuckdb(opts = {}) {
|
|
|
16255
16543
|
);
|
|
16256
16544
|
}
|
|
16257
16545
|
try {
|
|
16258
|
-
const pluginRequire =
|
|
16546
|
+
const pluginRequire = createRequire3(duckdbPkg);
|
|
16259
16547
|
return pluginRequire("@duckdb/node-api");
|
|
16260
16548
|
} catch {
|
|
16261
16549
|
throw missingDependency(
|
|
@@ -16345,7 +16633,7 @@ async function queryBacklinks(opts) {
|
|
|
16345
16633
|
const reversed = opts.targets.map(reverseDomain);
|
|
16346
16634
|
const targetList = reversed.map(quote).join(", ");
|
|
16347
16635
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
16348
|
-
const
|
|
16636
|
+
const sql15 = `
|
|
16349
16637
|
WITH vertices AS (
|
|
16350
16638
|
SELECT * FROM read_csv(
|
|
16351
16639
|
${quote(opts.vertexPath)},
|
|
@@ -16381,7 +16669,7 @@ async function queryBacklinks(opts) {
|
|
|
16381
16669
|
const conn = await instance.connect();
|
|
16382
16670
|
let rows;
|
|
16383
16671
|
try {
|
|
16384
|
-
const reader = await conn.runAndReadAll(
|
|
16672
|
+
const reader = await conn.runAndReadAll(sql15);
|
|
16385
16673
|
rows = reader.getRowObjects();
|
|
16386
16674
|
} finally {
|
|
16387
16675
|
conn.disconnectSync?.();
|
|
@@ -16458,7 +16746,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
16458
16746
|
}
|
|
16459
16747
|
|
|
16460
16748
|
// ../api-routes/src/backlinks-filter.ts
|
|
16461
|
-
import { and as
|
|
16749
|
+
import { and as and13, ne as ne2, notLike } from "drizzle-orm";
|
|
16462
16750
|
var BACKLINK_FILTER_PATTERNS = [
|
|
16463
16751
|
"*.google.com",
|
|
16464
16752
|
"*.googleusercontent.com",
|
|
@@ -16481,7 +16769,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
16481
16769
|
conditions.push(ne2(backlinkDomains.linkingDomain, pattern));
|
|
16482
16770
|
}
|
|
16483
16771
|
}
|
|
16484
|
-
const combined =
|
|
16772
|
+
const combined = and13(...conditions);
|
|
16485
16773
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
16486
16774
|
return combined;
|
|
16487
16775
|
}
|
|
@@ -16542,7 +16830,7 @@ function mapRunRow(row) {
|
|
|
16542
16830
|
};
|
|
16543
16831
|
}
|
|
16544
16832
|
function latestSummaryForProject(db, projectId, release) {
|
|
16545
|
-
const condition = release ?
|
|
16833
|
+
const condition = release ? and14(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
|
|
16546
16834
|
return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
|
|
16547
16835
|
}
|
|
16548
16836
|
function parseExcludeCrawlers(value) {
|
|
@@ -16551,18 +16839,18 @@ function parseExcludeCrawlers(value) {
|
|
|
16551
16839
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
16552
16840
|
}
|
|
16553
16841
|
function computeFilteredSummary(db, base) {
|
|
16554
|
-
const baseDomainCondition =
|
|
16842
|
+
const baseDomainCondition = and14(
|
|
16555
16843
|
eq22(backlinkDomains.projectId, base.projectId),
|
|
16556
16844
|
eq22(backlinkDomains.release, base.release)
|
|
16557
16845
|
);
|
|
16558
|
-
const filteredCondition =
|
|
16846
|
+
const filteredCondition = and14(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
16559
16847
|
const unfilteredAgg = db.select({
|
|
16560
|
-
count:
|
|
16561
|
-
total:
|
|
16848
|
+
count: sql8`count(*)`,
|
|
16849
|
+
total: sql8`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
16562
16850
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
16563
16851
|
const filteredAgg = db.select({
|
|
16564
|
-
count:
|
|
16565
|
-
total:
|
|
16852
|
+
count: sql8`count(*)`,
|
|
16853
|
+
total: sql8`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
16566
16854
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
16567
16855
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc11(backlinkDomains.numHosts)).limit(10).all();
|
|
16568
16856
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -16731,12 +17019,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
16731
17019
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
16732
17020
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
16733
17021
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
16734
|
-
const baseDomainCondition =
|
|
17022
|
+
const baseDomainCondition = and14(
|
|
16735
17023
|
eq22(backlinkDomains.projectId, project.id),
|
|
16736
17024
|
eq22(backlinkDomains.release, targetRelease)
|
|
16737
17025
|
);
|
|
16738
|
-
const domainCondition = excludeCrawlers ?
|
|
16739
|
-
const totalRow = app.db.select({ count:
|
|
17026
|
+
const domainCondition = excludeCrawlers ? and14(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
17027
|
+
const totalRow = app.db.select({ count: sql8`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
16740
17028
|
const rows = app.db.select({
|
|
16741
17029
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
16742
17030
|
numHosts: backlinkDomains.numHosts
|
|
@@ -16771,7 +17059,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
16771
17059
|
|
|
16772
17060
|
// ../api-routes/src/traffic.ts
|
|
16773
17061
|
import crypto20 from "crypto";
|
|
16774
|
-
import { and as
|
|
17062
|
+
import { and as and15, desc as desc12, eq as eq23, gte as gte2, lte as lte2, sql as sql9 } from "drizzle-orm";
|
|
16775
17063
|
|
|
16776
17064
|
// ../integration-cloud-run/src/auth.ts
|
|
16777
17065
|
import crypto19 from "crypto";
|
|
@@ -17270,8 +17558,8 @@ var ASSET_PATH_PREFIXES = [
|
|
|
17270
17558
|
"/img/",
|
|
17271
17559
|
"/static/"
|
|
17272
17560
|
];
|
|
17273
|
-
function normalizeTrafficPathPattern(
|
|
17274
|
-
const cleanPath =
|
|
17561
|
+
function normalizeTrafficPathPattern(path16) {
|
|
17562
|
+
const cleanPath = path16.trim() || "/";
|
|
17275
17563
|
const pathOnly = cleanPath.split("?")[0] || "/";
|
|
17276
17564
|
const segments = pathOnly.split("/").map((segment) => {
|
|
17277
17565
|
if (!segment) return segment;
|
|
@@ -17320,8 +17608,8 @@ function resolveAiReferralLandingPath(event, evidenceType) {
|
|
|
17320
17608
|
}
|
|
17321
17609
|
return normalizeTrafficPathPattern(event.path);
|
|
17322
17610
|
}
|
|
17323
|
-
function isLikelySubresourcePath(
|
|
17324
|
-
const cleanPath =
|
|
17611
|
+
function isLikelySubresourcePath(path16) {
|
|
17612
|
+
const cleanPath = path16.split("?")[0] || "/";
|
|
17325
17613
|
return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
|
|
17326
17614
|
}
|
|
17327
17615
|
function actorKey(event) {
|
|
@@ -17507,11 +17795,11 @@ function buildEventId2(event) {
|
|
|
17507
17795
|
function normalizeWordpressTrafficEvent(event) {
|
|
17508
17796
|
if (!event.observed_at) return null;
|
|
17509
17797
|
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
17510
|
-
const
|
|
17511
|
-
if (!
|
|
17798
|
+
const path16 = event.path?.trim();
|
|
17799
|
+
if (!path16) return null;
|
|
17512
17800
|
const queryString = trimOrNull(event.query_string);
|
|
17513
17801
|
const host = trimOrNull(event.host);
|
|
17514
|
-
const requestUrl = host ? `https://${host}${
|
|
17802
|
+
const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : `${path16}${queryString ? `?${queryString}` : ""}`;
|
|
17515
17803
|
return {
|
|
17516
17804
|
sourceType: TrafficSourceTypes.wordpress,
|
|
17517
17805
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -17521,7 +17809,7 @@ function normalizeWordpressTrafficEvent(event) {
|
|
|
17521
17809
|
method: trimOrNull(event.method),
|
|
17522
17810
|
requestUrl,
|
|
17523
17811
|
host,
|
|
17524
|
-
path:
|
|
17812
|
+
path: path16,
|
|
17525
17813
|
queryString,
|
|
17526
17814
|
status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
|
|
17527
17815
|
userAgent: trimOrNull(event.user_agent),
|
|
@@ -17685,15 +17973,15 @@ function stringLabels(input) {
|
|
|
17685
17973
|
);
|
|
17686
17974
|
}
|
|
17687
17975
|
function normalizeVercelLogRow(row) {
|
|
17688
|
-
const
|
|
17689
|
-
if (!
|
|
17976
|
+
const path16 = row.requestPath;
|
|
17977
|
+
if (!path16) return null;
|
|
17690
17978
|
const observedAt = row.timestamp;
|
|
17691
17979
|
if (!observedAt) return null;
|
|
17692
17980
|
const requestId = row.requestId;
|
|
17693
17981
|
if (!requestId) return null;
|
|
17694
17982
|
const host = emptyToNull(row.domain);
|
|
17695
17983
|
const queryString = serializeSearchParams(row.requestSearchParams);
|
|
17696
|
-
const requestUrl = host ? `https://${host}${
|
|
17984
|
+
const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : null;
|
|
17697
17985
|
return {
|
|
17698
17986
|
sourceType: TrafficSourceTypes.vercel,
|
|
17699
17987
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -17703,7 +17991,7 @@ function normalizeVercelLogRow(row) {
|
|
|
17703
17991
|
method: row.requestMethod ?? null,
|
|
17704
17992
|
requestUrl,
|
|
17705
17993
|
host,
|
|
17706
|
-
path:
|
|
17994
|
+
path: path16,
|
|
17707
17995
|
queryString,
|
|
17708
17996
|
status: resolveStatus(row),
|
|
17709
17997
|
userAgent: emptyToNull(row.clientUserAgent),
|
|
@@ -17839,7 +18127,7 @@ var DEFAULT_WP_MAX_PAGES = 20;
|
|
|
17839
18127
|
var DEFAULT_VERCEL_MAX_PAGES = 50;
|
|
17840
18128
|
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
17841
18129
|
var DEFAULT_BACKFILL_DAYS = 30;
|
|
17842
|
-
var MAX_BACKFILL_DAYS =
|
|
18130
|
+
var MAX_BACKFILL_DAYS = 90;
|
|
17843
18131
|
var BACKFILL_MAX_PAGES = 1e3;
|
|
17844
18132
|
var BACKFILL_SAMPLE_LIMIT = 500;
|
|
17845
18133
|
function parseSourceConfig(row) {
|
|
@@ -17919,21 +18207,21 @@ async function runBackfillTask(options) {
|
|
|
17919
18207
|
try {
|
|
17920
18208
|
app.db.transaction((tx) => {
|
|
17921
18209
|
tx.delete(crawlerEventsHourly).where(
|
|
17922
|
-
|
|
18210
|
+
and15(
|
|
17923
18211
|
eq23(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
17924
18212
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
17925
18213
|
lte2(crawlerEventsHourly.tsHour, windowEndIso)
|
|
17926
18214
|
)
|
|
17927
18215
|
).run();
|
|
17928
18216
|
tx.delete(aiReferralEventsHourly).where(
|
|
17929
|
-
|
|
18217
|
+
and15(
|
|
17930
18218
|
eq23(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
17931
18219
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
17932
18220
|
lte2(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
17933
18221
|
)
|
|
17934
18222
|
).run();
|
|
17935
18223
|
tx.delete(rawEventSamples).where(
|
|
17936
|
-
|
|
18224
|
+
and15(
|
|
17937
18225
|
eq23(rawEventSamples.sourceId, sourceRow.id),
|
|
17938
18226
|
gte2(rawEventSamples.ts, windowStartIso),
|
|
17939
18227
|
lte2(rawEventSamples.ts, windowEndIso)
|
|
@@ -18356,7 +18644,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18356
18644
|
endTime: windowEnd.toISOString(),
|
|
18357
18645
|
pageSize,
|
|
18358
18646
|
maxPages,
|
|
18359
|
-
firstSync: isFirstSync
|
|
18647
|
+
firstSync: isFirstSync,
|
|
18648
|
+
requestUrlSubstrings: [project.canonicalDomain]
|
|
18360
18649
|
});
|
|
18361
18650
|
allEvents = page.events;
|
|
18362
18651
|
} catch (e) {
|
|
@@ -18508,7 +18797,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18508
18797
|
crawlerEventsHourly.status
|
|
18509
18798
|
],
|
|
18510
18799
|
set: {
|
|
18511
|
-
hits:
|
|
18800
|
+
hits: sql9`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
18512
18801
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
18513
18802
|
updatedAt: finishedAt
|
|
18514
18803
|
}
|
|
@@ -18543,7 +18832,7 @@ async function trafficRoutes(app, opts) {
|
|
|
18543
18832
|
aiReferralEventsHourly.status
|
|
18544
18833
|
],
|
|
18545
18834
|
set: {
|
|
18546
|
-
sessionsOrHits:
|
|
18835
|
+
sessionsOrHits: sql9`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
18547
18836
|
updatedAt: finishedAt
|
|
18548
18837
|
}
|
|
18549
18838
|
}).run();
|
|
@@ -18682,7 +18971,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18682
18971
|
// ring-buffer reseed at the end takes the most-recent IDs from the
|
|
18683
18972
|
// dedupedEvents anyway.
|
|
18684
18973
|
firstSync: false,
|
|
18685
|
-
orderBy: "timestamp asc"
|
|
18974
|
+
orderBy: "timestamp asc",
|
|
18975
|
+
requestUrlSubstrings: [project.canonicalDomain]
|
|
18686
18976
|
});
|
|
18687
18977
|
return page.events;
|
|
18688
18978
|
};
|
|
@@ -18794,26 +19084,26 @@ async function trafficRoutes(app, opts) {
|
|
|
18794
19084
|
return response;
|
|
18795
19085
|
});
|
|
18796
19086
|
function buildSourceDetail(projectId, row, since) {
|
|
18797
|
-
const crawlerTotals = app.db.select({ total:
|
|
18798
|
-
|
|
19087
|
+
const crawlerTotals = app.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
19088
|
+
and15(
|
|
18799
19089
|
eq23(crawlerEventsHourly.sourceId, row.id),
|
|
18800
19090
|
gte2(crawlerEventsHourly.tsHour, since)
|
|
18801
19091
|
)
|
|
18802
19092
|
).get();
|
|
18803
|
-
const aiTotals = app.db.select({ total:
|
|
18804
|
-
|
|
19093
|
+
const aiTotals = app.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
19094
|
+
and15(
|
|
18805
19095
|
eq23(aiReferralEventsHourly.sourceId, row.id),
|
|
18806
19096
|
gte2(aiReferralEventsHourly.tsHour, since)
|
|
18807
19097
|
)
|
|
18808
19098
|
).get();
|
|
18809
|
-
const sampleTotals = app.db.select({ total:
|
|
18810
|
-
|
|
19099
|
+
const sampleTotals = app.db.select({ total: sql9`COUNT(*)` }).from(rawEventSamples).where(
|
|
19100
|
+
and15(
|
|
18811
19101
|
eq23(rawEventSamples.sourceId, row.id),
|
|
18812
19102
|
gte2(rawEventSamples.ts, since)
|
|
18813
19103
|
)
|
|
18814
19104
|
).get();
|
|
18815
19105
|
const latestRun = app.db.select().from(runs).where(
|
|
18816
|
-
|
|
19106
|
+
and15(
|
|
18817
19107
|
eq23(runs.projectId, projectId),
|
|
18818
19108
|
eq23(runs.kind, RunKinds["traffic-sync"]),
|
|
18819
19109
|
eq23(runs.sourceId, row.id)
|
|
@@ -18907,8 +19197,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18907
19197
|
lte2(crawlerEventsHourly.tsHour, untilIso)
|
|
18908
19198
|
];
|
|
18909
19199
|
if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
18910
|
-
const crawlerWhere =
|
|
18911
|
-
const total = app.db.select({ total:
|
|
19200
|
+
const crawlerWhere = and15(...crawlerFilters);
|
|
19201
|
+
const total = app.db.select({ total: sql9`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
18912
19202
|
crawlerTotal = Number(total?.total ?? 0);
|
|
18913
19203
|
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
18914
19204
|
for (const r of rows) {
|
|
@@ -18932,8 +19222,8 @@ async function trafficRoutes(app, opts) {
|
|
|
18932
19222
|
lte2(aiReferralEventsHourly.tsHour, untilIso)
|
|
18933
19223
|
];
|
|
18934
19224
|
if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
18935
|
-
const aiWhere =
|
|
18936
|
-
const total = app.db.select({ total:
|
|
19225
|
+
const aiWhere = and15(...aiFilters);
|
|
19226
|
+
const total = app.db.select({ total: sql9`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
18937
19227
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
18938
19228
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
18939
19229
|
for (const r of rows) {
|
|
@@ -18966,6 +19256,75 @@ async function trafficRoutes(app, opts) {
|
|
|
18966
19256
|
});
|
|
18967
19257
|
}
|
|
18968
19258
|
|
|
19259
|
+
// ../api-routes/src/doctor/checks/agent.ts
|
|
19260
|
+
import fs6 from "fs";
|
|
19261
|
+
import path7 from "path";
|
|
19262
|
+
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
19263
|
+
var skillsInstalledCheck = {
|
|
19264
|
+
id: "agent.skills.installed",
|
|
19265
|
+
category: CheckCategories.agent,
|
|
19266
|
+
scope: CheckScopes.global,
|
|
19267
|
+
title: "Agent skills installed (~/.claude/skills/)",
|
|
19268
|
+
run: () => {
|
|
19269
|
+
const home = process.env.HOME;
|
|
19270
|
+
if (!home) {
|
|
19271
|
+
return {
|
|
19272
|
+
status: CheckStatuses.skipped,
|
|
19273
|
+
code: "agent.skills.no-home",
|
|
19274
|
+
summary: "Cannot determine $HOME \u2014 skip skills filesystem check.",
|
|
19275
|
+
remediation: null
|
|
19276
|
+
};
|
|
19277
|
+
}
|
|
19278
|
+
const skillsBase = path7.join(home, ".claude", "skills");
|
|
19279
|
+
const installed = [];
|
|
19280
|
+
const missing = [];
|
|
19281
|
+
for (const name of REQUIRED_SKILLS) {
|
|
19282
|
+
const dir = path7.join(skillsBase, name);
|
|
19283
|
+
if (isInstalled(dir)) installed.push(name);
|
|
19284
|
+
else missing.push(name);
|
|
19285
|
+
}
|
|
19286
|
+
const details = {
|
|
19287
|
+
checkedPath: skillsBase,
|
|
19288
|
+
installed,
|
|
19289
|
+
missing
|
|
19290
|
+
};
|
|
19291
|
+
if (missing.length === 0) {
|
|
19292
|
+
return {
|
|
19293
|
+
status: CheckStatuses.ok,
|
|
19294
|
+
code: "agent.skills.installed",
|
|
19295
|
+
summary: `Both canonry and aero skills are installed in ${skillsBase}.`,
|
|
19296
|
+
remediation: null,
|
|
19297
|
+
details
|
|
19298
|
+
};
|
|
19299
|
+
}
|
|
19300
|
+
if (installed.length === 0) {
|
|
19301
|
+
return {
|
|
19302
|
+
status: CheckStatuses.warn,
|
|
19303
|
+
code: "agent.skills.not-installed",
|
|
19304
|
+
summary: "Agent skills are not installed for Claude Code on this machine. Claude sessions on this host will not auto-load canonry/aero reference docs.",
|
|
19305
|
+
remediation: "Run `canonry skills install --dir ~` (or `canonry skills install --user`) to install both skills to ~/.claude/skills/ and ~/.codex/skills/.",
|
|
19306
|
+
details
|
|
19307
|
+
};
|
|
19308
|
+
}
|
|
19309
|
+
return {
|
|
19310
|
+
status: CheckStatuses.warn,
|
|
19311
|
+
code: "agent.skills.partial",
|
|
19312
|
+
summary: `Only ${installed.length} of ${REQUIRED_SKILLS.length} agent skills are installed (${installed.join(", ")}); ${missing.join(", ")} missing.`,
|
|
19313
|
+
remediation: `Run \`canonry skills install ${missing.join(" ")} --dir ~\` to fill the gap.`,
|
|
19314
|
+
details
|
|
19315
|
+
};
|
|
19316
|
+
}
|
|
19317
|
+
};
|
|
19318
|
+
function isInstalled(dir) {
|
|
19319
|
+
try {
|
|
19320
|
+
if (!fs6.existsSync(dir)) return false;
|
|
19321
|
+
return fs6.existsSync(path7.join(dir, "SKILL.md"));
|
|
19322
|
+
} catch {
|
|
19323
|
+
return false;
|
|
19324
|
+
}
|
|
19325
|
+
}
|
|
19326
|
+
var AGENT_CHECKS = [skillsInstalledCheck];
|
|
19327
|
+
|
|
18969
19328
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
18970
19329
|
var BING_AUTH_CHECKS = [
|
|
18971
19330
|
{
|
|
@@ -19587,7 +19946,7 @@ var providersConfiguredCheck = {
|
|
|
19587
19946
|
var PROVIDERS_CHECKS = [providersConfiguredCheck];
|
|
19588
19947
|
|
|
19589
19948
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
19590
|
-
import { and as
|
|
19949
|
+
import { and as and16, eq as eq24, gte as gte3, ne as ne3, sql as sql10 } from "drizzle-orm";
|
|
19591
19950
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
19592
19951
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
19593
19952
|
function skippedNoProject2() {
|
|
@@ -19601,7 +19960,7 @@ function skippedNoProject2() {
|
|
|
19601
19960
|
function loadProbes(ctx) {
|
|
19602
19961
|
if (!ctx.project) return [];
|
|
19603
19962
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
19604
|
-
|
|
19963
|
+
and16(
|
|
19605
19964
|
eq24(trafficSources.projectId, ctx.project.id),
|
|
19606
19965
|
ne3(trafficSources.status, TrafficSourceStatuses.archived)
|
|
19607
19966
|
)
|
|
@@ -19681,16 +20040,16 @@ var recentDataCheck = {
|
|
|
19681
20040
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
19682
20041
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
19683
20042
|
const recentCrawlers = Number(
|
|
19684
|
-
ctx.db.select({ total:
|
|
19685
|
-
|
|
20043
|
+
ctx.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
20044
|
+
and16(
|
|
19686
20045
|
eq24(crawlerEventsHourly.projectId, ctx.project.id),
|
|
19687
20046
|
gte3(crawlerEventsHourly.tsHour, warnCutoff)
|
|
19688
20047
|
)
|
|
19689
20048
|
).get()?.total ?? 0
|
|
19690
20049
|
);
|
|
19691
20050
|
const recentReferrals = Number(
|
|
19692
|
-
ctx.db.select({ total:
|
|
19693
|
-
|
|
20051
|
+
ctx.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
20052
|
+
and16(
|
|
19694
20053
|
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
19695
20054
|
gte3(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
19696
20055
|
)
|
|
@@ -19705,16 +20064,16 @@ var recentDataCheck = {
|
|
|
19705
20064
|
};
|
|
19706
20065
|
}
|
|
19707
20066
|
const olderCrawlers = Number(
|
|
19708
|
-
ctx.db.select({ total:
|
|
19709
|
-
|
|
20067
|
+
ctx.db.select({ total: sql10`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
20068
|
+
and16(
|
|
19710
20069
|
eq24(crawlerEventsHourly.projectId, ctx.project.id),
|
|
19711
20070
|
gte3(crawlerEventsHourly.tsHour, failCutoff)
|
|
19712
20071
|
)
|
|
19713
20072
|
).get()?.total ?? 0
|
|
19714
20073
|
);
|
|
19715
20074
|
const olderReferrals = Number(
|
|
19716
|
-
ctx.db.select({ total:
|
|
19717
|
-
|
|
20075
|
+
ctx.db.select({ total: sql10`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
20076
|
+
and16(
|
|
19718
20077
|
eq24(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
19719
20078
|
gte3(aiReferralEventsHourly.tsHour, failCutoff)
|
|
19720
20079
|
)
|
|
@@ -19895,7 +20254,8 @@ var ALL_CHECKS = [
|
|
|
19895
20254
|
...BING_AUTH_CHECKS,
|
|
19896
20255
|
...GA_AUTH_CHECKS,
|
|
19897
20256
|
...PROVIDERS_CHECKS,
|
|
19898
|
-
...TRAFFIC_SOURCE_CHECKS
|
|
20257
|
+
...TRAFFIC_SOURCE_CHECKS,
|
|
20258
|
+
...AGENT_CHECKS
|
|
19899
20259
|
];
|
|
19900
20260
|
var CHECK_BY_ID = Object.fromEntries(
|
|
19901
20261
|
ALL_CHECKS.map((check) => [check.id, check])
|
|
@@ -20012,7 +20372,7 @@ async function doctorRoutes(app, opts) {
|
|
|
20012
20372
|
|
|
20013
20373
|
// ../api-routes/src/discovery/routes.ts
|
|
20014
20374
|
import crypto21 from "crypto";
|
|
20015
|
-
import { and as
|
|
20375
|
+
import { and as and17, desc as desc13, eq as eq25, gte as gte4, inArray as inArray8 } from "drizzle-orm";
|
|
20016
20376
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
20017
20377
|
async function discoveryRoutes(app, opts) {
|
|
20018
20378
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -20044,7 +20404,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
20044
20404
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20045
20405
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
20046
20406
|
const decision = app.db.transaction((tx) => {
|
|
20047
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
20407
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and17(
|
|
20048
20408
|
eq25(discoverySessions.projectId, project.id),
|
|
20049
20409
|
eq25(discoverySessions.icpDescription, icpDescription),
|
|
20050
20410
|
inArray8(discoverySessions.status, [
|
|
@@ -20508,6 +20868,7 @@ async function apiRoutes(app, opts) {
|
|
|
20508
20868
|
await api.register(projectRoutes, {
|
|
20509
20869
|
onProjectDeleted: opts.onProjectDeleted,
|
|
20510
20870
|
onProjectUpserted: opts.onProjectUpserted,
|
|
20871
|
+
onAliasesChanged: opts.onAliasesChanged,
|
|
20511
20872
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
20512
20873
|
});
|
|
20513
20874
|
await api.register(queryRoutes, {
|
|
@@ -20522,7 +20883,9 @@ async function apiRoutes(app, opts) {
|
|
|
20522
20883
|
await api.register(applyRoutes, {
|
|
20523
20884
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
20524
20885
|
onProjectUpserted: opts.onProjectUpserted,
|
|
20886
|
+
onAliasesChanged: opts.onAliasesChanged,
|
|
20525
20887
|
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
20888
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks,
|
|
20526
20889
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
20527
20890
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
20528
20891
|
propertyId,
|
|
@@ -20553,7 +20916,9 @@ async function apiRoutes(app, opts) {
|
|
|
20553
20916
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
20554
20917
|
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
20555
20918
|
});
|
|
20556
|
-
await api.register(notificationRoutes
|
|
20919
|
+
await api.register(notificationRoutes, {
|
|
20920
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks
|
|
20921
|
+
});
|
|
20557
20922
|
await api.register(telemetryRoutes, {
|
|
20558
20923
|
getTelemetryStatus: opts.getTelemetryStatus,
|
|
20559
20924
|
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
@@ -20808,7 +21173,7 @@ function isVertexConfig(config) {
|
|
|
20808
21173
|
function resolveModel(config) {
|
|
20809
21174
|
return config.model || DEFAULT_MODEL;
|
|
20810
21175
|
}
|
|
20811
|
-
function
|
|
21176
|
+
function createClient2(config) {
|
|
20812
21177
|
if (isVertexConfig(config)) {
|
|
20813
21178
|
return new GoogleGenAI({
|
|
20814
21179
|
vertexai: true,
|
|
@@ -20848,7 +21213,7 @@ async function healthcheck(config) {
|
|
|
20848
21213
|
if (!validation.ok) return validation;
|
|
20849
21214
|
try {
|
|
20850
21215
|
const model = resolveModel(config);
|
|
20851
|
-
const client =
|
|
21216
|
+
const client = createClient2(config);
|
|
20852
21217
|
const result = await withRetry(
|
|
20853
21218
|
() => client.models.generateContent({
|
|
20854
21219
|
model,
|
|
@@ -20875,7 +21240,7 @@ async function healthcheck(config) {
|
|
|
20875
21240
|
async function executeTrackedQuery(input) {
|
|
20876
21241
|
const model = resolveModel(input.config);
|
|
20877
21242
|
const prompt = buildPrompt(input.query, input.location);
|
|
20878
|
-
const client =
|
|
21243
|
+
const client = createClient2(input.config);
|
|
20879
21244
|
try {
|
|
20880
21245
|
const result = await withRetry(
|
|
20881
21246
|
() => client.models.generateContent({
|
|
@@ -21040,7 +21405,7 @@ function extractDomainFromUri(uri) {
|
|
|
21040
21405
|
}
|
|
21041
21406
|
async function generateText(prompt, config) {
|
|
21042
21407
|
const model = resolveModel(config);
|
|
21043
|
-
const client =
|
|
21408
|
+
const client = createClient2(config);
|
|
21044
21409
|
const result = await withRetry(
|
|
21045
21410
|
() => client.models.generateContent({
|
|
21046
21411
|
model,
|
|
@@ -22195,7 +22560,7 @@ var localAdapter = {
|
|
|
22195
22560
|
};
|
|
22196
22561
|
|
|
22197
22562
|
// ../provider-cdp/src/adapter.ts
|
|
22198
|
-
import
|
|
22563
|
+
import path9 from "path";
|
|
22199
22564
|
import os4 from "os";
|
|
22200
22565
|
|
|
22201
22566
|
// ../provider-cdp/src/connection.ts
|
|
@@ -22560,12 +22925,12 @@ function sleep2(ms) {
|
|
|
22560
22925
|
}
|
|
22561
22926
|
|
|
22562
22927
|
// ../provider-cdp/src/screenshot.ts
|
|
22563
|
-
import
|
|
22564
|
-
import
|
|
22928
|
+
import fs7 from "fs";
|
|
22929
|
+
import path8 from "path";
|
|
22565
22930
|
async function captureElementScreenshot(client, selector, outputPath) {
|
|
22566
|
-
const dir =
|
|
22567
|
-
if (!
|
|
22568
|
-
|
|
22931
|
+
const dir = path8.dirname(outputPath);
|
|
22932
|
+
if (!fs7.existsSync(dir)) {
|
|
22933
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
22569
22934
|
}
|
|
22570
22935
|
let clip;
|
|
22571
22936
|
try {
|
|
@@ -22599,7 +22964,7 @@ async function captureElementScreenshot(client, selector, outputPath) {
|
|
|
22599
22964
|
}
|
|
22600
22965
|
const { data } = await client.Page.captureScreenshot(screenshotParams);
|
|
22601
22966
|
const buffer = Buffer.from(data, "base64");
|
|
22602
|
-
|
|
22967
|
+
fs7.writeFileSync(outputPath, buffer);
|
|
22603
22968
|
return outputPath;
|
|
22604
22969
|
}
|
|
22605
22970
|
|
|
@@ -22660,7 +23025,7 @@ function getConnection(config) {
|
|
|
22660
23025
|
return conn;
|
|
22661
23026
|
}
|
|
22662
23027
|
function getScreenshotDir2() {
|
|
22663
|
-
return
|
|
23028
|
+
return path9.join(os4.homedir(), ".canonry", "screenshots");
|
|
22664
23029
|
}
|
|
22665
23030
|
var cdpChatgptAdapter = {
|
|
22666
23031
|
name: "cdp:chatgpt",
|
|
@@ -22724,7 +23089,7 @@ var cdpChatgptAdapter = {
|
|
|
22724
23089
|
const answerText = await target.extractAnswer(client);
|
|
22725
23090
|
const groundingSources = await target.extractCitations(client);
|
|
22726
23091
|
const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
22727
|
-
const screenshotPath =
|
|
23092
|
+
const screenshotPath = path9.join(getScreenshotDir2(), `${screenshotId}.png`);
|
|
22728
23093
|
let capturedScreenshotPath;
|
|
22729
23094
|
try {
|
|
22730
23095
|
capturedScreenshotPath = await captureElementScreenshot(
|
|
@@ -23357,10 +23722,10 @@ function removeWordpressConnection(config, projectName) {
|
|
|
23357
23722
|
|
|
23358
23723
|
// src/job-runner.ts
|
|
23359
23724
|
import crypto24 from "crypto";
|
|
23360
|
-
import
|
|
23361
|
-
import
|
|
23725
|
+
import fs8 from "fs";
|
|
23726
|
+
import path10 from "path";
|
|
23362
23727
|
import os5 from "os";
|
|
23363
|
-
import { and as
|
|
23728
|
+
import { and as and18, eq as eq27, inArray as inArray9, sql as sql11 } from "drizzle-orm";
|
|
23364
23729
|
|
|
23365
23730
|
// src/run-telemetry.ts
|
|
23366
23731
|
import crypto23 from "crypto";
|
|
@@ -23467,11 +23832,15 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
23467
23832
|
function escapeRegExp5(value) {
|
|
23468
23833
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23469
23834
|
}
|
|
23470
|
-
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains) {
|
|
23835
|
+
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
|
|
23471
23836
|
if (!answerText || answerText.length < 20) return [];
|
|
23472
23837
|
const ownBrandKeys = new Set(
|
|
23473
23838
|
ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
|
|
23474
23839
|
);
|
|
23840
|
+
for (const name of ownBrandNames) {
|
|
23841
|
+
const key = brandKeyFromText(name);
|
|
23842
|
+
if (key.length >= 4) ownBrandKeys.add(key);
|
|
23843
|
+
}
|
|
23475
23844
|
const knownCompetitorKeys = new Set(
|
|
23476
23845
|
[...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
|
|
23477
23846
|
);
|
|
@@ -23739,7 +24108,7 @@ var JobRunner = class {
|
|
|
23739
24108
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
23740
24109
|
}
|
|
23741
24110
|
if (existingRun.status === "queued") {
|
|
23742
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
24111
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and18(eq27(runs.id, runId), eq27(runs.status, "queued"))).run();
|
|
23743
24112
|
}
|
|
23744
24113
|
this.throwIfRunCancelled(runId);
|
|
23745
24114
|
const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
@@ -23764,13 +24133,17 @@ var JobRunner = class {
|
|
|
23764
24133
|
}
|
|
23765
24134
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
23766
24135
|
const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
|
|
23767
|
-
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(
|
|
24136
|
+
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and18(eq27(queries.projectId, projectId), inArray9(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
|
|
23768
24137
|
const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
|
|
23769
24138
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
23770
24139
|
const allDomains = effectiveDomains({
|
|
23771
24140
|
canonicalDomain: project.canonicalDomain,
|
|
23772
24141
|
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
23773
24142
|
});
|
|
24143
|
+
const allBrandNames = effectiveBrandNames({
|
|
24144
|
+
displayName: project.displayName,
|
|
24145
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
24146
|
+
});
|
|
23774
24147
|
const executionContext = {
|
|
23775
24148
|
providerCount: activeProviders.length,
|
|
23776
24149
|
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
@@ -23831,7 +24204,7 @@ var JobRunner = class {
|
|
|
23831
24204
|
const citationState = determineCitationState(normalized, allDomains);
|
|
23832
24205
|
const answerMentioned = determineAnswerMentioned(
|
|
23833
24206
|
normalized.answerText,
|
|
23834
|
-
|
|
24207
|
+
allBrandNames,
|
|
23835
24208
|
allDomains
|
|
23836
24209
|
);
|
|
23837
24210
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
@@ -23839,15 +24212,16 @@ var JobRunner = class {
|
|
|
23839
24212
|
normalized.answerText,
|
|
23840
24213
|
allDomains,
|
|
23841
24214
|
normalized.citedDomains,
|
|
23842
|
-
competitorDomains
|
|
24215
|
+
competitorDomains,
|
|
24216
|
+
allBrandNames
|
|
23843
24217
|
);
|
|
23844
24218
|
let screenshotRelPath = null;
|
|
23845
|
-
if (raw.screenshotPath &&
|
|
24219
|
+
if (raw.screenshotPath && fs8.existsSync(raw.screenshotPath)) {
|
|
23846
24220
|
const snapshotId = crypto24.randomUUID();
|
|
23847
|
-
const screenshotDir =
|
|
23848
|
-
if (!
|
|
23849
|
-
const destPath =
|
|
23850
|
-
|
|
24221
|
+
const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
|
|
24222
|
+
if (!fs8.existsSync(screenshotDir)) fs8.mkdirSync(screenshotDir, { recursive: true });
|
|
24223
|
+
const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
|
|
24224
|
+
fs8.renameSync(raw.screenshotPath, destPath);
|
|
23851
24225
|
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
23852
24226
|
this.db.insert(querySnapshots).values({
|
|
23853
24227
|
id: snapshotId,
|
|
@@ -24032,7 +24406,7 @@ var JobRunner = class {
|
|
|
24032
24406
|
updatedAt: now
|
|
24033
24407
|
}).onConflictDoUpdate({
|
|
24034
24408
|
target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
|
|
24035
|
-
set: { count:
|
|
24409
|
+
set: { count: sql11`${usageCounters.count} + ${count}`, updatedAt: now }
|
|
24036
24410
|
}).run();
|
|
24037
24411
|
}
|
|
24038
24412
|
flushProviderUsage(projectId, providerDispatchCounts) {
|
|
@@ -24102,7 +24476,7 @@ function buildPhases(input) {
|
|
|
24102
24476
|
|
|
24103
24477
|
// src/gsc-sync.ts
|
|
24104
24478
|
import crypto25 from "crypto";
|
|
24105
|
-
import { eq as eq28, and as
|
|
24479
|
+
import { eq as eq28, and as and19, sql as sql12 } from "drizzle-orm";
|
|
24106
24480
|
var log2 = createLogger("GscSync");
|
|
24107
24481
|
function formatDate3(d) {
|
|
24108
24482
|
return d.toISOString().split("T")[0];
|
|
@@ -24154,10 +24528,10 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24154
24528
|
});
|
|
24155
24529
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
24156
24530
|
db.delete(gscSearchData).where(
|
|
24157
|
-
|
|
24531
|
+
and19(
|
|
24158
24532
|
eq28(gscSearchData.projectId, projectId),
|
|
24159
|
-
|
|
24160
|
-
|
|
24533
|
+
sql12`${gscSearchData.date} >= ${startDate}`,
|
|
24534
|
+
sql12`${gscSearchData.date} <= ${endDate}`
|
|
24161
24535
|
)
|
|
24162
24536
|
).run();
|
|
24163
24537
|
const batchSize = 500;
|
|
@@ -24243,7 +24617,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24243
24617
|
}
|
|
24244
24618
|
}
|
|
24245
24619
|
const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
|
|
24246
|
-
db.delete(gscCoverageSnapshots).where(
|
|
24620
|
+
db.delete(gscCoverageSnapshots).where(and19(eq28(gscCoverageSnapshots.projectId, projectId), eq28(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
24247
24621
|
db.insert(gscCoverageSnapshots).values({
|
|
24248
24622
|
id: crypto25.randomUUID(),
|
|
24249
24623
|
projectId,
|
|
@@ -24266,7 +24640,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
24266
24640
|
|
|
24267
24641
|
// src/gsc-inspect-sitemap.ts
|
|
24268
24642
|
import crypto26 from "crypto";
|
|
24269
|
-
import { eq as eq29, and as
|
|
24643
|
+
import { eq as eq29, and as and20 } from "drizzle-orm";
|
|
24270
24644
|
|
|
24271
24645
|
// src/sitemap-parser.ts
|
|
24272
24646
|
var log3 = createLogger("SitemapParser");
|
|
@@ -24482,7 +24856,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
24482
24856
|
}
|
|
24483
24857
|
}
|
|
24484
24858
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
24485
|
-
db.delete(gscCoverageSnapshots).where(
|
|
24859
|
+
db.delete(gscCoverageSnapshots).where(and20(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
24486
24860
|
db.insert(gscCoverageSnapshots).values({
|
|
24487
24861
|
id: crypto26.randomUUID(),
|
|
24488
24862
|
projectId,
|
|
@@ -24692,8 +25066,8 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
24692
25066
|
|
|
24693
25067
|
// src/commoncrawl-sync.ts
|
|
24694
25068
|
import crypto28 from "crypto";
|
|
24695
|
-
import
|
|
24696
|
-
import { and as
|
|
25069
|
+
import path11 from "path";
|
|
25070
|
+
import { and as and21, eq as eq31, sql as sql13 } from "drizzle-orm";
|
|
24697
25071
|
var log6 = createLogger("CommonCrawlSync");
|
|
24698
25072
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
24699
25073
|
function defaultDeps() {
|
|
@@ -24721,9 +25095,9 @@ async function executeReleaseSync(db, syncId, opts) {
|
|
|
24721
25095
|
error: null
|
|
24722
25096
|
}).where(eq31(ccReleaseSyncs.id, syncId)).run();
|
|
24723
25097
|
const paths = ccReleasePaths(release);
|
|
24724
|
-
const releaseCacheDir =
|
|
24725
|
-
const vertexPath =
|
|
24726
|
-
const edgesPath =
|
|
25098
|
+
const releaseCacheDir = path11.join(deps.cacheDir, release);
|
|
25099
|
+
const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
|
|
25100
|
+
const edgesPath = path11.join(releaseCacheDir, paths.edgesFilename);
|
|
24727
25101
|
const [vertex, edges] = await Promise.all([
|
|
24728
25102
|
deps.downloadFile({ url: paths.vertexUrl, destPath: vertexPath }),
|
|
24729
25103
|
deps.downloadFile({ url: paths.edgesUrl, destPath: edgesPath })
|
|
@@ -24883,8 +25257,8 @@ function computeSummary(rows) {
|
|
|
24883
25257
|
|
|
24884
25258
|
// src/backlink-extract.ts
|
|
24885
25259
|
import crypto29 from "crypto";
|
|
24886
|
-
import
|
|
24887
|
-
import { and as
|
|
25260
|
+
import fs9 from "fs";
|
|
25261
|
+
import { and as and22, desc as desc15, eq as eq32 } from "drizzle-orm";
|
|
24888
25262
|
var log7 = createLogger("BacklinkExtract");
|
|
24889
25263
|
function defaultDeps2() {
|
|
24890
25264
|
return {
|
|
@@ -24912,7 +25286,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
24912
25286
|
if (!sync.vertexPath || !sync.edgesPath) {
|
|
24913
25287
|
throw new Error(`Release ${sync.release} is missing cached file paths`);
|
|
24914
25288
|
}
|
|
24915
|
-
if (!
|
|
25289
|
+
if (!fs9.existsSync(sync.vertexPath) || !fs9.existsSync(sync.edgesPath)) {
|
|
24916
25290
|
throw new Error(
|
|
24917
25291
|
`Cache for release ${sync.release} is missing from disk (expected at ${sync.vertexPath}). The sync record exists in the database, but the ~16 GB dump was deleted or never present on this machine. Re-sync this release from the Backlinks admin page to restore the cache.`
|
|
24918
25292
|
);
|
|
@@ -24930,7 +25304,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
24930
25304
|
const targetDomain = project.canonicalDomain;
|
|
24931
25305
|
db.transaction((tx) => {
|
|
24932
25306
|
tx.delete(backlinkDomains).where(
|
|
24933
|
-
|
|
25307
|
+
and22(eq32(backlinkDomains.projectId, projectId), eq32(backlinkDomains.release, release))
|
|
24934
25308
|
).run();
|
|
24935
25309
|
if (rows.length > 0) {
|
|
24936
25310
|
const values = rows.map((r) => ({
|
|
@@ -25001,9 +25375,10 @@ function computeSummary2(rows) {
|
|
|
25001
25375
|
|
|
25002
25376
|
// src/discovery-run.ts
|
|
25003
25377
|
import crypto30 from "crypto";
|
|
25004
|
-
import { eq as eq33 } from "drizzle-orm";
|
|
25378
|
+
import { and as and23, eq as eq33 } from "drizzle-orm";
|
|
25005
25379
|
var log8 = createLogger("DiscoveryRun");
|
|
25006
25380
|
var DEFAULT_SEED_COUNT = 30;
|
|
25381
|
+
var QUERIES_PER_INTENT_BUCKET = 6;
|
|
25007
25382
|
async function executeDiscoveryRun(opts) {
|
|
25008
25383
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25009
25384
|
opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq33(runs.id, opts.runId)).run();
|
|
@@ -25208,6 +25583,7 @@ function buildLocationConstraint(locations) {
|
|
|
25208
25583
|
}
|
|
25209
25584
|
function buildSeedPrompt(input) {
|
|
25210
25585
|
const locationConstraint = buildLocationConstraint(input.locations ?? []);
|
|
25586
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
25211
25587
|
return [
|
|
25212
25588
|
"You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
|
|
25213
25589
|
"",
|
|
@@ -25215,14 +25591,15 @@ function buildSeedPrompt(input) {
|
|
|
25215
25591
|
`ICP: ${input.icpDescription}`,
|
|
25216
25592
|
...locationConstraint.length > 0 ? ["", ...locationConstraint] : [],
|
|
25217
25593
|
"",
|
|
25218
|
-
"Brainstorm
|
|
25219
|
-
' - Comparison queries ("best X for Y")',
|
|
25220
|
-
" - Specific feature / capability queries",
|
|
25221
|
-
" - Pricing / vendor-shortlist queries",
|
|
25222
|
-
" - Workflow / how-to queries",
|
|
25223
|
-
" - Adjacent jobs-to-be-done queries",
|
|
25594
|
+
"Brainstorm queries a member of this ICP would type into an AI answer engine (Gemini, ChatGPT, Perplexity). Generate candidates across the five intent buckets below \u2014 these are SEMANTICALLY DISTINCT search intents, not stylistic variants. A query should fit one bucket cleanly. Diversity across buckets is the point: it keeps the list from collapsing into near-synonyms of a single intent.",
|
|
25224
25595
|
"",
|
|
25225
|
-
|
|
25596
|
+
' 1. Informational \u2014 the searcher wants to understand a concept, market, or problem. Templates: "what is X", "how does X work", "X explained", "why X matters".',
|
|
25597
|
+
` 2. Commercial \u2014 the searcher is researching category leaders before a purchase. Templates: "best X for Y", "top X ${currentYear}", "leading X providers", "X for [use case]".`,
|
|
25598
|
+
' 3. Navigational \u2014 the searcher is looking for a specific brand, place, or directory. Templates: "X near me", "X reviews", "X website", "X directory".',
|
|
25599
|
+
' 4. Comparative \u2014 the searcher is weighing named alternatives head-to-head. Templates: "X vs Y", "X or Y for Z", "alternatives to X".',
|
|
25600
|
+
' 5. Transactional \u2014 the searcher is ready to act on a purchase or booking. Templates: "book X", "X pricing", "X discount code", "buy X online".',
|
|
25601
|
+
"",
|
|
25602
|
+
`Generate EXACTLY ${QUERIES_PER_INTENT_BUCKET} queries per bucket \u2014 ${QUERIES_PER_INTENT_BUCKET * 5} total. Return ONE query per line. Plain text only \u2014 no numbering, bullets, quotes, bucket labels, or commentary.`
|
|
25226
25603
|
].join("\n");
|
|
25227
25604
|
}
|
|
25228
25605
|
function parseQueryLines(text, max) {
|
|
@@ -25257,33 +25634,40 @@ function writeDiscoveryInsight(db, input) {
|
|
|
25257
25634
|
aspirational: buckets.aspirational,
|
|
25258
25635
|
totalProbes
|
|
25259
25636
|
});
|
|
25260
|
-
db.
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
|
|
25269
|
-
|
|
25270
|
-
|
|
25271
|
-
|
|
25272
|
-
|
|
25273
|
-
|
|
25274
|
-
|
|
25275
|
-
|
|
25276
|
-
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
|
|
25280
|
-
|
|
25281
|
-
|
|
25282
|
-
|
|
25283
|
-
|
|
25284
|
-
|
|
25285
|
-
|
|
25286
|
-
|
|
25637
|
+
db.transaction((tx) => {
|
|
25638
|
+
tx.update(insights).set({ dismissed: true }).where(and23(
|
|
25639
|
+
eq33(insights.projectId, input.projectId),
|
|
25640
|
+
eq33(insights.type, "discovery.basket-divergence"),
|
|
25641
|
+
eq33(insights.dismissed, false)
|
|
25642
|
+
)).run();
|
|
25643
|
+
tx.insert(insights).values({
|
|
25644
|
+
id: crypto30.randomUUID(),
|
|
25645
|
+
projectId: input.projectId,
|
|
25646
|
+
runId: input.runId,
|
|
25647
|
+
type: "discovery.basket-divergence",
|
|
25648
|
+
severity,
|
|
25649
|
+
title,
|
|
25650
|
+
// query/provider fields don't fit the visibility-snapshot model for
|
|
25651
|
+
// a session-level insight. Use the session marker so the
|
|
25652
|
+
// (query, provider) index stays distinct across sessions; PR 5 will
|
|
25653
|
+
// formalize a session-scoped insight subtype.
|
|
25654
|
+
query: `discovery:${input.sessionId}`,
|
|
25655
|
+
provider: input.seedProvider,
|
|
25656
|
+
recommendation: JSON.stringify({
|
|
25657
|
+
action: "review-discovered-basket",
|
|
25658
|
+
summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
|
|
25659
|
+
bucketCounts: buckets,
|
|
25660
|
+
topCompetitors
|
|
25661
|
+
}),
|
|
25662
|
+
cause: JSON.stringify({
|
|
25663
|
+
sessionId: input.sessionId,
|
|
25664
|
+
totalProbes,
|
|
25665
|
+
seedProvider: input.seedProvider
|
|
25666
|
+
}),
|
|
25667
|
+
dismissed: false,
|
|
25668
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25669
|
+
}).run();
|
|
25670
|
+
});
|
|
25287
25671
|
}
|
|
25288
25672
|
function buildDiscoveryInsightTitle(input) {
|
|
25289
25673
|
const parts = [];
|
|
@@ -25294,6 +25678,525 @@ function buildDiscoveryInsightTitle(input) {
|
|
|
25294
25678
|
return parts.join(" \u2022 ");
|
|
25295
25679
|
}
|
|
25296
25680
|
|
|
25681
|
+
// src/commands/backfill.ts
|
|
25682
|
+
import { and as and24, eq as eq34, inArray as inArray10 } from "drizzle-orm";
|
|
25683
|
+
var SNAPSHOT_BATCH_SIZE = 500;
|
|
25684
|
+
async function backfillAnswerVisibilityCommand(opts) {
|
|
25685
|
+
const config = loadConfig();
|
|
25686
|
+
const db = createClient(config.database);
|
|
25687
|
+
migrate(db);
|
|
25688
|
+
const projectFilter = opts?.project?.trim();
|
|
25689
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
25690
|
+
let examined = 0;
|
|
25691
|
+
let updated = 0;
|
|
25692
|
+
let mentioned = 0;
|
|
25693
|
+
let reparsed = 0;
|
|
25694
|
+
let providerErrors = 0;
|
|
25695
|
+
if (scopedProjects.length > 0) {
|
|
25696
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and24(
|
|
25697
|
+
eq34(runs.kind, RunKinds["answer-visibility"]),
|
|
25698
|
+
inArray10(runs.projectId, scopedProjects.map((project) => project.id))
|
|
25699
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq34(runs.kind, RunKinds["answer-visibility"])).all();
|
|
25700
|
+
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
25701
|
+
for (const run of runRows) {
|
|
25702
|
+
const existing = runIdsByProject.get(run.projectId);
|
|
25703
|
+
if (existing) existing.push(run.id);
|
|
25704
|
+
else runIdsByProject.set(run.projectId, [run.id]);
|
|
25705
|
+
}
|
|
25706
|
+
for (const project of scopedProjects) {
|
|
25707
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
25708
|
+
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
25709
|
+
if (runIds.length === 0) continue;
|
|
25710
|
+
const projectDomains = effectiveDomains({
|
|
25711
|
+
canonicalDomain: project.canonicalDomain,
|
|
25712
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
25713
|
+
});
|
|
25714
|
+
const projectBrandNames = effectiveBrandNames({
|
|
25715
|
+
displayName: project.displayName,
|
|
25716
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
25717
|
+
});
|
|
25718
|
+
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
25719
|
+
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
25720
|
+
const snapshotRows = db.select({
|
|
25721
|
+
id: querySnapshots.id,
|
|
25722
|
+
provider: querySnapshots.provider,
|
|
25723
|
+
citationState: querySnapshots.citationState,
|
|
25724
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
25725
|
+
answerText: querySnapshots.answerText,
|
|
25726
|
+
citedDomains: querySnapshots.citedDomains,
|
|
25727
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
25728
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
25729
|
+
rawResponse: querySnapshots.rawResponse
|
|
25730
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
|
|
25731
|
+
const pendingUpdates = [];
|
|
25732
|
+
for (const snapshot of snapshotRows) {
|
|
25733
|
+
examined++;
|
|
25734
|
+
const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
|
|
25735
|
+
if (reparsedResult) reparsed++;
|
|
25736
|
+
if (reparsedResult?.providerError) providerErrors++;
|
|
25737
|
+
const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
|
|
25738
|
+
const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
25739
|
+
if (nextValue) mentioned++;
|
|
25740
|
+
const nextPatch = {};
|
|
25741
|
+
if (snapshot.answerMentioned !== nextValue) {
|
|
25742
|
+
nextPatch.answerMentioned = nextValue;
|
|
25743
|
+
}
|
|
25744
|
+
if ((snapshot.answerText ?? "") !== answerText) {
|
|
25745
|
+
nextPatch.answerText = answerText;
|
|
25746
|
+
}
|
|
25747
|
+
if (reparsedResult) {
|
|
25748
|
+
const normalized = {
|
|
25749
|
+
provider: snapshot.provider,
|
|
25750
|
+
answerText,
|
|
25751
|
+
citedDomains: reparsedResult.citedDomains,
|
|
25752
|
+
groundingSources: reparsedResult.groundingSources,
|
|
25753
|
+
searchQueries: reparsedResult.searchQueries
|
|
25754
|
+
};
|
|
25755
|
+
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
25756
|
+
const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
|
|
25757
|
+
const nextCompetitorOverlap = JSON.stringify(
|
|
25758
|
+
computeCompetitorOverlap(normalized, competitorDomains)
|
|
25759
|
+
);
|
|
25760
|
+
const nextRecommendedCompetitors = JSON.stringify(
|
|
25761
|
+
extractRecommendedCompetitors(
|
|
25762
|
+
normalized.answerText,
|
|
25763
|
+
projectDomains,
|
|
25764
|
+
normalized.citedDomains,
|
|
25765
|
+
competitorDomains,
|
|
25766
|
+
projectBrandNames
|
|
25767
|
+
)
|
|
25768
|
+
);
|
|
25769
|
+
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
25770
|
+
snapshot.rawResponse,
|
|
25771
|
+
reparsedResult
|
|
25772
|
+
);
|
|
25773
|
+
if (snapshot.citationState !== nextCitationState) {
|
|
25774
|
+
nextPatch.citationState = nextCitationState;
|
|
25775
|
+
}
|
|
25776
|
+
if (snapshot.citedDomains !== nextCitedDomains) {
|
|
25777
|
+
nextPatch.citedDomains = nextCitedDomains;
|
|
25778
|
+
}
|
|
25779
|
+
if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
|
|
25780
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
25781
|
+
}
|
|
25782
|
+
if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
|
|
25783
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
25784
|
+
}
|
|
25785
|
+
if (snapshot.rawResponse !== nextRawResponse) {
|
|
25786
|
+
nextPatch.rawResponse = nextRawResponse;
|
|
25787
|
+
}
|
|
25788
|
+
}
|
|
25789
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
25790
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
25791
|
+
}
|
|
25792
|
+
}
|
|
25793
|
+
if (pendingUpdates.length > 0) {
|
|
25794
|
+
db.transaction((tx) => {
|
|
25795
|
+
for (const update of pendingUpdates) {
|
|
25796
|
+
tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
|
|
25797
|
+
}
|
|
25798
|
+
});
|
|
25799
|
+
updated += pendingUpdates.length;
|
|
25800
|
+
}
|
|
25801
|
+
}
|
|
25802
|
+
}
|
|
25803
|
+
}
|
|
25804
|
+
const result = {
|
|
25805
|
+
project: projectFilter ?? null,
|
|
25806
|
+
projects: scopedProjects.length,
|
|
25807
|
+
examined,
|
|
25808
|
+
updated,
|
|
25809
|
+
mentioned,
|
|
25810
|
+
reparsed,
|
|
25811
|
+
providerErrors
|
|
25812
|
+
};
|
|
25813
|
+
if (opts?.format === "json") {
|
|
25814
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25815
|
+
return;
|
|
25816
|
+
}
|
|
25817
|
+
console.log("Answer visibility backfill complete.\n");
|
|
25818
|
+
if (projectFilter) {
|
|
25819
|
+
console.log(` Project: ${projectFilter}`);
|
|
25820
|
+
}
|
|
25821
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
25822
|
+
console.log(` Examined: ${examined}`);
|
|
25823
|
+
console.log(` Updated: ${updated}`);
|
|
25824
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
25825
|
+
console.log(` Reparsed: ${reparsed}`);
|
|
25826
|
+
console.log(` Errors: ${providerErrors}`);
|
|
25827
|
+
}
|
|
25828
|
+
function backfillNormalizedPaths(db, opts) {
|
|
25829
|
+
const baseConditions = [];
|
|
25830
|
+
if (opts?.projectId) {
|
|
25831
|
+
baseConditions.push(eq34(gaTrafficSnapshots.projectId, opts.projectId));
|
|
25832
|
+
}
|
|
25833
|
+
const rows = db.select({
|
|
25834
|
+
id: gaTrafficSnapshots.id,
|
|
25835
|
+
landingPage: gaTrafficSnapshots.landingPage,
|
|
25836
|
+
landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
|
|
25837
|
+
}).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
|
|
25838
|
+
let updated = 0;
|
|
25839
|
+
let unchanged = 0;
|
|
25840
|
+
if (rows.length > 0) {
|
|
25841
|
+
db.transaction((tx) => {
|
|
25842
|
+
for (const row of rows) {
|
|
25843
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
25844
|
+
if (next === null) {
|
|
25845
|
+
unchanged++;
|
|
25846
|
+
continue;
|
|
25847
|
+
}
|
|
25848
|
+
if (row.landingPageNormalized === next) {
|
|
25849
|
+
unchanged++;
|
|
25850
|
+
continue;
|
|
25851
|
+
}
|
|
25852
|
+
tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq34(gaTrafficSnapshots.id, row.id)).run();
|
|
25853
|
+
updated++;
|
|
25854
|
+
}
|
|
25855
|
+
});
|
|
25856
|
+
}
|
|
25857
|
+
return { examined: rows.length, updated, unchanged };
|
|
25858
|
+
}
|
|
25859
|
+
async function backfillNormalizedPathsCommand(opts) {
|
|
25860
|
+
const config = loadConfig();
|
|
25861
|
+
const db = createClient(config.database);
|
|
25862
|
+
migrate(db);
|
|
25863
|
+
const projectFilter = opts?.project?.trim();
|
|
25864
|
+
let projectId;
|
|
25865
|
+
if (projectFilter) {
|
|
25866
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
|
|
25867
|
+
if (!project) {
|
|
25868
|
+
const result2 = {
|
|
25869
|
+
project: projectFilter,
|
|
25870
|
+
examined: 0,
|
|
25871
|
+
updated: 0,
|
|
25872
|
+
unchanged: 0
|
|
25873
|
+
};
|
|
25874
|
+
if (opts?.format === "json") {
|
|
25875
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
25876
|
+
return;
|
|
25877
|
+
}
|
|
25878
|
+
console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
|
|
25879
|
+
return;
|
|
25880
|
+
}
|
|
25881
|
+
projectId = project.id;
|
|
25882
|
+
}
|
|
25883
|
+
const { examined, updated, unchanged } = backfillNormalizedPaths(db, { projectId });
|
|
25884
|
+
const result = {
|
|
25885
|
+
project: projectFilter ?? null,
|
|
25886
|
+
examined,
|
|
25887
|
+
updated,
|
|
25888
|
+
unchanged
|
|
25889
|
+
};
|
|
25890
|
+
if (opts?.format === "json") {
|
|
25891
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25892
|
+
return;
|
|
25893
|
+
}
|
|
25894
|
+
console.log("Normalized-path backfill complete.\n");
|
|
25895
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
25896
|
+
console.log(` Examined: ${examined}`);
|
|
25897
|
+
console.log(` Updated: ${updated}`);
|
|
25898
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
25899
|
+
}
|
|
25900
|
+
function backfillAiReferralPaths(db, opts) {
|
|
25901
|
+
const baseConditions = [];
|
|
25902
|
+
if (opts?.projectId) {
|
|
25903
|
+
baseConditions.push(eq34(gaAiReferrals.projectId, opts.projectId));
|
|
25904
|
+
}
|
|
25905
|
+
const rows = db.select({
|
|
25906
|
+
id: gaAiReferrals.id,
|
|
25907
|
+
landingPage: gaAiReferrals.landingPage,
|
|
25908
|
+
landingPageNormalized: gaAiReferrals.landingPageNormalized
|
|
25909
|
+
}).from(gaAiReferrals).where(baseConditions.length > 0 ? and24(...baseConditions) : void 0).all();
|
|
25910
|
+
let updated = 0;
|
|
25911
|
+
let unchanged = 0;
|
|
25912
|
+
if (rows.length > 0) {
|
|
25913
|
+
db.transaction((tx) => {
|
|
25914
|
+
for (const row of rows) {
|
|
25915
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
25916
|
+
if (next === null) {
|
|
25917
|
+
unchanged++;
|
|
25918
|
+
continue;
|
|
25919
|
+
}
|
|
25920
|
+
if (row.landingPageNormalized === next) {
|
|
25921
|
+
unchanged++;
|
|
25922
|
+
continue;
|
|
25923
|
+
}
|
|
25924
|
+
tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq34(gaAiReferrals.id, row.id)).run();
|
|
25925
|
+
updated++;
|
|
25926
|
+
}
|
|
25927
|
+
});
|
|
25928
|
+
}
|
|
25929
|
+
return { examined: rows.length, updated, unchanged };
|
|
25930
|
+
}
|
|
25931
|
+
async function backfillAiReferralPathsCommand(opts) {
|
|
25932
|
+
const config = loadConfig();
|
|
25933
|
+
const db = createClient(config.database);
|
|
25934
|
+
migrate(db);
|
|
25935
|
+
const projectFilter = opts?.project?.trim();
|
|
25936
|
+
let projectId;
|
|
25937
|
+
if (projectFilter) {
|
|
25938
|
+
const project = db.select({ id: projects.id }).from(projects).where(eq34(projects.name, projectFilter)).get();
|
|
25939
|
+
if (!project) {
|
|
25940
|
+
const result2 = {
|
|
25941
|
+
project: projectFilter,
|
|
25942
|
+
examined: 0,
|
|
25943
|
+
updated: 0,
|
|
25944
|
+
unchanged: 0
|
|
25945
|
+
};
|
|
25946
|
+
if (opts?.format === "json") {
|
|
25947
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
25948
|
+
return;
|
|
25949
|
+
}
|
|
25950
|
+
console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
|
|
25951
|
+
return;
|
|
25952
|
+
}
|
|
25953
|
+
projectId = project.id;
|
|
25954
|
+
}
|
|
25955
|
+
const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
|
|
25956
|
+
const result = {
|
|
25957
|
+
project: projectFilter ?? null,
|
|
25958
|
+
examined,
|
|
25959
|
+
updated,
|
|
25960
|
+
unchanged
|
|
25961
|
+
};
|
|
25962
|
+
if (opts?.format === "json") {
|
|
25963
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25964
|
+
return;
|
|
25965
|
+
}
|
|
25966
|
+
console.log("AI referral landing-page backfill complete.\n");
|
|
25967
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
25968
|
+
console.log(` Examined: ${examined}`);
|
|
25969
|
+
console.log(` Updated: ${updated}`);
|
|
25970
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
25971
|
+
}
|
|
25972
|
+
function backfillProjectAnswerMentions(db, projectId) {
|
|
25973
|
+
const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
|
|
25974
|
+
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
25975
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
25976
|
+
const runRows = db.select({ id: runs.id }).from(runs).where(and24(eq34(runs.kind, RunKinds["answer-visibility"]), eq34(runs.projectId, projectId))).all();
|
|
25977
|
+
const runIds = runRows.map((r) => r.id);
|
|
25978
|
+
let examined = 0;
|
|
25979
|
+
let updated = 0;
|
|
25980
|
+
let mentioned = 0;
|
|
25981
|
+
if (runIds.length === 0) return { examined, updated, mentioned };
|
|
25982
|
+
const projectDomains = effectiveDomains({
|
|
25983
|
+
canonicalDomain: project.canonicalDomain,
|
|
25984
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
25985
|
+
});
|
|
25986
|
+
const projectBrandNames = effectiveBrandNames({
|
|
25987
|
+
displayName: project.displayName,
|
|
25988
|
+
aliases: parseJsonColumn(project.aliases, [])
|
|
25989
|
+
});
|
|
25990
|
+
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
25991
|
+
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
25992
|
+
const snapshotRows = db.select({
|
|
25993
|
+
id: querySnapshots.id,
|
|
25994
|
+
provider: querySnapshots.provider,
|
|
25995
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
25996
|
+
answerText: querySnapshots.answerText,
|
|
25997
|
+
citedDomains: querySnapshots.citedDomains,
|
|
25998
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
25999
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
26000
|
+
rawResponse: querySnapshots.rawResponse
|
|
26001
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, batchRunIds)).all();
|
|
26002
|
+
const pendingUpdates = [];
|
|
26003
|
+
for (const snapshot of snapshotRows) {
|
|
26004
|
+
examined++;
|
|
26005
|
+
const answerText = snapshot.answerText ?? "";
|
|
26006
|
+
const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
26007
|
+
if (nextAnswerMentioned) mentioned++;
|
|
26008
|
+
const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
|
|
26009
|
+
const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
|
|
26010
|
+
const normalized = {
|
|
26011
|
+
provider: snapshot.provider,
|
|
26012
|
+
answerText,
|
|
26013
|
+
citedDomains,
|
|
26014
|
+
groundingSources,
|
|
26015
|
+
searchQueries: []
|
|
26016
|
+
};
|
|
26017
|
+
const nextCompetitorOverlap = JSON.stringify(
|
|
26018
|
+
computeCompetitorOverlap(normalized, competitorDomains)
|
|
26019
|
+
);
|
|
26020
|
+
const nextRecommendedCompetitors = JSON.stringify(
|
|
26021
|
+
extractRecommendedCompetitors(
|
|
26022
|
+
answerText,
|
|
26023
|
+
projectDomains,
|
|
26024
|
+
citedDomains,
|
|
26025
|
+
competitorDomains,
|
|
26026
|
+
projectBrandNames
|
|
26027
|
+
)
|
|
26028
|
+
);
|
|
26029
|
+
const nextPatch = {};
|
|
26030
|
+
if (snapshot.answerMentioned !== nextAnswerMentioned) {
|
|
26031
|
+
nextPatch.answerMentioned = nextAnswerMentioned;
|
|
26032
|
+
}
|
|
26033
|
+
if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
|
|
26034
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
26035
|
+
}
|
|
26036
|
+
if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
|
|
26037
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
26038
|
+
}
|
|
26039
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
26040
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
26041
|
+
}
|
|
26042
|
+
}
|
|
26043
|
+
if (pendingUpdates.length > 0) {
|
|
26044
|
+
db.transaction((tx) => {
|
|
26045
|
+
for (const update of pendingUpdates) {
|
|
26046
|
+
tx.update(querySnapshots).set(update.patch).where(eq34(querySnapshots.id, update.id)).run();
|
|
26047
|
+
}
|
|
26048
|
+
});
|
|
26049
|
+
updated += pendingUpdates.length;
|
|
26050
|
+
}
|
|
26051
|
+
}
|
|
26052
|
+
return { examined, updated, mentioned };
|
|
26053
|
+
}
|
|
26054
|
+
async function backfillAnswerMentionsCommand(opts) {
|
|
26055
|
+
const config = loadConfig();
|
|
26056
|
+
const db = createClient(config.database);
|
|
26057
|
+
migrate(db);
|
|
26058
|
+
const projectFilter = opts?.project?.trim();
|
|
26059
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq34(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
26060
|
+
let examined = 0;
|
|
26061
|
+
let updated = 0;
|
|
26062
|
+
let mentioned = 0;
|
|
26063
|
+
for (const project of scopedProjects) {
|
|
26064
|
+
const result2 = backfillProjectAnswerMentions(db, project.id);
|
|
26065
|
+
examined += result2.examined;
|
|
26066
|
+
updated += result2.updated;
|
|
26067
|
+
mentioned += result2.mentioned;
|
|
26068
|
+
}
|
|
26069
|
+
const result = {
|
|
26070
|
+
project: projectFilter ?? null,
|
|
26071
|
+
projects: scopedProjects.length,
|
|
26072
|
+
examined,
|
|
26073
|
+
updated,
|
|
26074
|
+
mentioned
|
|
26075
|
+
};
|
|
26076
|
+
if (opts?.format === "json") {
|
|
26077
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26078
|
+
return;
|
|
26079
|
+
}
|
|
26080
|
+
console.log("Answer mentions backfill complete.\n");
|
|
26081
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
26082
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
26083
|
+
console.log(` Examined: ${examined}`);
|
|
26084
|
+
console.log(` Updated: ${updated}`);
|
|
26085
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
26086
|
+
}
|
|
26087
|
+
function readStoredGroundingSources(rawResponse) {
|
|
26088
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
26089
|
+
const sources = envelope.groundingSources;
|
|
26090
|
+
if (!Array.isArray(sources)) return [];
|
|
26091
|
+
const result = [];
|
|
26092
|
+
for (const source of sources) {
|
|
26093
|
+
if (source && typeof source === "object") {
|
|
26094
|
+
const uri = source.uri;
|
|
26095
|
+
const title = source.title;
|
|
26096
|
+
if (typeof uri === "string") {
|
|
26097
|
+
result.push({ uri, title: typeof title === "string" ? title : "" });
|
|
26098
|
+
}
|
|
26099
|
+
}
|
|
26100
|
+
}
|
|
26101
|
+
return result;
|
|
26102
|
+
}
|
|
26103
|
+
async function backfillInsightsCommand(project, opts) {
|
|
26104
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-7AWRUNI2.js");
|
|
26105
|
+
const config = loadConfig();
|
|
26106
|
+
const db = createClient(config.database);
|
|
26107
|
+
migrate(db);
|
|
26108
|
+
const service = new IntelligenceService2(db);
|
|
26109
|
+
const isJson = opts?.format === "json";
|
|
26110
|
+
const isDryRun = opts?.dryRun === true;
|
|
26111
|
+
if (!isJson) {
|
|
26112
|
+
const scope = opts?.since ? ` (since ${opts.since})` : "";
|
|
26113
|
+
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
26114
|
+
process.stderr.write(`Backfilling insights for "${project}"${scope}${mode}...
|
|
26115
|
+
`);
|
|
26116
|
+
}
|
|
26117
|
+
const result = service.backfill(project, {
|
|
26118
|
+
fromRunId: opts?.fromRun,
|
|
26119
|
+
toRunId: opts?.toRun,
|
|
26120
|
+
since: opts?.since,
|
|
26121
|
+
dryRun: isDryRun
|
|
26122
|
+
}, (info) => {
|
|
26123
|
+
if (!isJson) {
|
|
26124
|
+
process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
|
|
26125
|
+
`);
|
|
26126
|
+
}
|
|
26127
|
+
});
|
|
26128
|
+
const output = {
|
|
26129
|
+
project,
|
|
26130
|
+
processed: result.processed,
|
|
26131
|
+
skipped: result.skipped,
|
|
26132
|
+
totalInsights: result.totalInsights
|
|
26133
|
+
};
|
|
26134
|
+
if (result.dryRun) {
|
|
26135
|
+
output.dryRun = true;
|
|
26136
|
+
output.delta = result.delta;
|
|
26137
|
+
}
|
|
26138
|
+
if (isJson) {
|
|
26139
|
+
console.log(JSON.stringify(output, null, 2));
|
|
26140
|
+
return;
|
|
26141
|
+
}
|
|
26142
|
+
console.log(`
|
|
26143
|
+
Backfill ${isDryRun ? "preview" : "complete"}.`);
|
|
26144
|
+
console.log(` Processed: ${result.processed}`);
|
|
26145
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
26146
|
+
console.log(` Insights: ${result.totalInsights}`);
|
|
26147
|
+
if (result.delta) {
|
|
26148
|
+
console.log(` Delta: -${result.delta.wouldDelete} existing +${result.delta.wouldCreate} new (net ${result.delta.netChange >= 0 ? "+" : ""}${result.delta.netChange})`);
|
|
26149
|
+
console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
|
|
26150
|
+
}
|
|
26151
|
+
}
|
|
26152
|
+
function reparseProviderSnapshot(provider, rawResponse) {
|
|
26153
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
26154
|
+
const apiResponse = resolveStoredApiResponse(envelope);
|
|
26155
|
+
if (!apiResponse) return null;
|
|
26156
|
+
switch (provider) {
|
|
26157
|
+
case ProviderNames.openai:
|
|
26158
|
+
return reparseStoredResult2(apiResponse);
|
|
26159
|
+
case ProviderNames.claude:
|
|
26160
|
+
return reparseStoredResult3(apiResponse);
|
|
26161
|
+
case ProviderNames.gemini:
|
|
26162
|
+
return reparseStoredResult(apiResponse);
|
|
26163
|
+
case ProviderNames.perplexity:
|
|
26164
|
+
return reparseStoredResult4(apiResponse);
|
|
26165
|
+
default:
|
|
26166
|
+
return null;
|
|
26167
|
+
}
|
|
26168
|
+
}
|
|
26169
|
+
function resolveStoredApiResponse(parsed) {
|
|
26170
|
+
const nested = parsed.apiResponse;
|
|
26171
|
+
if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
26172
|
+
return nested;
|
|
26173
|
+
}
|
|
26174
|
+
if (looksLikeProviderApiResponse(parsed)) {
|
|
26175
|
+
return parsed;
|
|
26176
|
+
}
|
|
26177
|
+
return null;
|
|
26178
|
+
}
|
|
26179
|
+
function looksLikeProviderApiResponse(value) {
|
|
26180
|
+
return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
|
|
26181
|
+
}
|
|
26182
|
+
function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
|
|
26183
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
26184
|
+
const apiResponse = resolveStoredApiResponse(parsed);
|
|
26185
|
+
const envelope = apiResponse === parsed ? {} : { ...parsed };
|
|
26186
|
+
delete envelope.answerText;
|
|
26187
|
+
delete envelope.citedDomains;
|
|
26188
|
+
delete envelope.competitorOverlap;
|
|
26189
|
+
delete envelope.recommendedCompetitors;
|
|
26190
|
+
delete envelope.providerError;
|
|
26191
|
+
return JSON.stringify({
|
|
26192
|
+
...envelope,
|
|
26193
|
+
groundingSources: reparsed.groundingSources,
|
|
26194
|
+
searchQueries: reparsed.searchQueries,
|
|
26195
|
+
...reparsed.providerError ? { providerError: reparsed.providerError } : {},
|
|
26196
|
+
...apiResponse ? { apiResponse } : {}
|
|
26197
|
+
});
|
|
26198
|
+
}
|
|
26199
|
+
|
|
25297
26200
|
// src/provider-registry.ts
|
|
25298
26201
|
var ProviderRegistry = class {
|
|
25299
26202
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -25347,7 +26250,7 @@ var ProviderRegistry = class {
|
|
|
25347
26250
|
|
|
25348
26251
|
// src/scheduler.ts
|
|
25349
26252
|
import cron from "node-cron";
|
|
25350
|
-
import { and as
|
|
26253
|
+
import { and as and25, eq as eq35 } from "drizzle-orm";
|
|
25351
26254
|
var log9 = createLogger("Scheduler");
|
|
25352
26255
|
function taskKey(projectId, kind) {
|
|
25353
26256
|
return `${projectId}::${kind}`;
|
|
@@ -25362,7 +26265,7 @@ var Scheduler = class {
|
|
|
25362
26265
|
}
|
|
25363
26266
|
/** Load all enabled schedules from DB and register cron jobs. */
|
|
25364
26267
|
start() {
|
|
25365
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
26268
|
+
const allSchedules = this.db.select().from(schedules).where(eq35(schedules.enabled, 1)).all();
|
|
25366
26269
|
for (const schedule of allSchedules) {
|
|
25367
26270
|
const missedRunAt = schedule.nextRunAt;
|
|
25368
26271
|
this.registerCronTask(schedule);
|
|
@@ -25392,7 +26295,7 @@ var Scheduler = class {
|
|
|
25392
26295
|
this.stopTask(key, existing, "Stopped");
|
|
25393
26296
|
this.tasks.delete(key);
|
|
25394
26297
|
}
|
|
25395
|
-
const schedule = this.db.select().from(schedules).where(
|
|
26298
|
+
const schedule = this.db.select().from(schedules).where(and25(eq35(schedules.projectId, projectId), eq35(schedules.kind, kind))).get();
|
|
25396
26299
|
if (schedule && schedule.enabled === 1) {
|
|
25397
26300
|
this.registerCronTask(schedule);
|
|
25398
26301
|
}
|
|
@@ -25433,14 +26336,14 @@ var Scheduler = class {
|
|
|
25433
26336
|
this.db.update(schedules).set({
|
|
25434
26337
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
25435
26338
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
25436
|
-
}).where(
|
|
26339
|
+
}).where(eq35(schedules.id, scheduleId)).run();
|
|
25437
26340
|
const label = schedule.preset ?? cronExpr;
|
|
25438
26341
|
log9.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
25439
26342
|
}
|
|
25440
26343
|
triggerRun(scheduleId, projectId, kind) {
|
|
25441
26344
|
try {
|
|
25442
26345
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25443
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
26346
|
+
const currentSchedule = this.db.select().from(schedules).where(eq35(schedules.id, scheduleId)).get();
|
|
25444
26347
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
25445
26348
|
log9.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
25446
26349
|
this.remove(projectId, kind);
|
|
@@ -25448,7 +26351,7 @@ var Scheduler = class {
|
|
|
25448
26351
|
}
|
|
25449
26352
|
const task = this.tasks.get(taskKey(projectId, kind));
|
|
25450
26353
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
25451
|
-
const project = this.db.select().from(projects).where(
|
|
26354
|
+
const project = this.db.select().from(projects).where(eq35(projects.id, projectId)).get();
|
|
25452
26355
|
if (!project) {
|
|
25453
26356
|
log9.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
25454
26357
|
this.remove(projectId, kind);
|
|
@@ -25468,7 +26371,7 @@ var Scheduler = class {
|
|
|
25468
26371
|
lastRunAt: now,
|
|
25469
26372
|
nextRunAt,
|
|
25470
26373
|
updatedAt: now
|
|
25471
|
-
}).where(
|
|
26374
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25472
26375
|
log9.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
25473
26376
|
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
25474
26377
|
return;
|
|
@@ -25496,7 +26399,7 @@ var Scheduler = class {
|
|
|
25496
26399
|
this.db.update(schedules).set({
|
|
25497
26400
|
nextRunAt,
|
|
25498
26401
|
updatedAt: now
|
|
25499
|
-
}).where(
|
|
26402
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25500
26403
|
return;
|
|
25501
26404
|
}
|
|
25502
26405
|
const runId = queueResult.runId;
|
|
@@ -25504,7 +26407,7 @@ var Scheduler = class {
|
|
|
25504
26407
|
lastRunAt: now,
|
|
25505
26408
|
nextRunAt,
|
|
25506
26409
|
updatedAt: now
|
|
25507
|
-
}).where(
|
|
26410
|
+
}).where(eq35(schedules.id, currentSchedule.id)).run();
|
|
25508
26411
|
const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
|
|
25509
26412
|
const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
|
|
25510
26413
|
log9.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
@@ -25516,7 +26419,7 @@ var Scheduler = class {
|
|
|
25516
26419
|
};
|
|
25517
26420
|
|
|
25518
26421
|
// src/notifier.ts
|
|
25519
|
-
import { eq as
|
|
26422
|
+
import { eq as eq36, desc as desc16, and as and26, inArray as inArray11, or as or5 } from "drizzle-orm";
|
|
25520
26423
|
import crypto31 from "crypto";
|
|
25521
26424
|
var log10 = createLogger("Notifier");
|
|
25522
26425
|
var Notifier = class {
|
|
@@ -25529,18 +26432,18 @@ var Notifier = class {
|
|
|
25529
26432
|
/** Called after a run completes (success, partial, or failed). */
|
|
25530
26433
|
async onRunCompleted(runId, projectId) {
|
|
25531
26434
|
log10.info("run.completed", { runId, projectId });
|
|
25532
|
-
const notifs = this.db.select().from(notifications).where(
|
|
26435
|
+
const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
25533
26436
|
if (notifs.length === 0) {
|
|
25534
26437
|
log10.info("notifications.none-enabled", { projectId });
|
|
25535
26438
|
return;
|
|
25536
26439
|
}
|
|
25537
26440
|
log10.info("notifications.found", { projectId, count: notifs.length });
|
|
25538
|
-
const run = this.db.select().from(runs).where(
|
|
26441
|
+
const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25539
26442
|
if (!run) {
|
|
25540
26443
|
log10.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
25541
26444
|
return;
|
|
25542
26445
|
}
|
|
25543
|
-
const project = this.db.select().from(projects).where(
|
|
26446
|
+
const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
|
|
25544
26447
|
if (!project) {
|
|
25545
26448
|
log10.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
25546
26449
|
return;
|
|
@@ -25587,11 +26490,11 @@ var Notifier = class {
|
|
|
25587
26490
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
25588
26491
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
25589
26492
|
if (insightEvents.length === 0) return;
|
|
25590
|
-
const notifs = this.db.select().from(notifications).where(
|
|
26493
|
+
const notifs = this.db.select().from(notifications).where(eq36(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
|
|
25591
26494
|
if (notifs.length === 0) return;
|
|
25592
|
-
const run = this.db.select().from(runs).where(
|
|
26495
|
+
const run = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25593
26496
|
if (!run) return;
|
|
25594
|
-
const project = this.db.select().from(projects).where(
|
|
26497
|
+
const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
|
|
25595
26498
|
if (!project) return;
|
|
25596
26499
|
for (const notif of notifs) {
|
|
25597
26500
|
const config = parseJsonColumn(notif.config, { url: "", events: [] });
|
|
@@ -25621,12 +26524,12 @@ var Notifier = class {
|
|
|
25621
26524
|
}
|
|
25622
26525
|
}
|
|
25623
26526
|
computeTransitions(runId, projectId) {
|
|
25624
|
-
const thisRun = this.db.select().from(runs).where(
|
|
26527
|
+
const thisRun = this.db.select().from(runs).where(eq36(runs.id, runId)).get();
|
|
25625
26528
|
if (!thisRun) return [];
|
|
25626
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
26529
|
+
const groupSiblings = this.db.select().from(runs).where(and26(
|
|
26530
|
+
eq36(runs.projectId, projectId),
|
|
26531
|
+
eq36(runs.kind, thisRun.kind),
|
|
26532
|
+
eq36(runs.createdAt, thisRun.createdAt)
|
|
25630
26533
|
)).all();
|
|
25631
26534
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
25632
26535
|
if (stillPending) return [];
|
|
@@ -25642,17 +26545,17 @@ var Notifier = class {
|
|
|
25642
26545
|
return candidate.id > best.id ? candidate : best;
|
|
25643
26546
|
});
|
|
25644
26547
|
if (winner.id !== runId) return [];
|
|
25645
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
26548
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
|
|
25646
26549
|
const locationCount = Math.max(
|
|
25647
26550
|
1,
|
|
25648
26551
|
parseJsonColumn(projectLocations?.locations ?? null, []).length
|
|
25649
26552
|
);
|
|
25650
26553
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
25651
26554
|
const recentRuns = this.db.select().from(runs).where(
|
|
25652
|
-
|
|
25653
|
-
|
|
25654
|
-
|
|
25655
|
-
|
|
26555
|
+
and26(
|
|
26556
|
+
eq36(runs.projectId, projectId),
|
|
26557
|
+
eq36(runs.kind, thisRun.kind),
|
|
26558
|
+
or5(eq36(runs.status, "completed"), eq36(runs.status, "partial"))
|
|
25656
26559
|
)
|
|
25657
26560
|
).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
25658
26561
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
@@ -25669,13 +26572,13 @@ var Notifier = class {
|
|
|
25669
26572
|
provider: querySnapshots.provider,
|
|
25670
26573
|
location: querySnapshots.location,
|
|
25671
26574
|
citationState: querySnapshots.citationState
|
|
25672
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
26575
|
+
}).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(inArray11(querySnapshots.runId, currentRunIds)).all();
|
|
25673
26576
|
const previousSnapshots = this.db.select({
|
|
25674
26577
|
queryId: querySnapshots.queryId,
|
|
25675
26578
|
provider: querySnapshots.provider,
|
|
25676
26579
|
location: querySnapshots.location,
|
|
25677
26580
|
citationState: querySnapshots.citationState
|
|
25678
|
-
}).from(querySnapshots).where(
|
|
26581
|
+
}).from(querySnapshots).where(inArray11(querySnapshots.runId, previousRunIds)).all();
|
|
25679
26582
|
const prevMap = /* @__PURE__ */ new Map();
|
|
25680
26583
|
for (const s of previousSnapshots) {
|
|
25681
26584
|
if (s.queryId == null) continue;
|
|
@@ -25749,7 +26652,7 @@ var Notifier = class {
|
|
|
25749
26652
|
};
|
|
25750
26653
|
|
|
25751
26654
|
// src/run-coordinator.ts
|
|
25752
|
-
import { eq as
|
|
26655
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
25753
26656
|
var log11 = createLogger("RunCoordinator");
|
|
25754
26657
|
var RunCoordinator = class {
|
|
25755
26658
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
@@ -25760,7 +26663,7 @@ var RunCoordinator = class {
|
|
|
25760
26663
|
this.onAeroEvent = onAeroEvent;
|
|
25761
26664
|
}
|
|
25762
26665
|
async onRunCompleted(runId, projectId) {
|
|
25763
|
-
const runRow = this.db.select().from(runs).where(
|
|
26666
|
+
const runRow = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
|
|
25764
26667
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
25765
26668
|
let insightCount = 0;
|
|
25766
26669
|
let criticalOrHigh = 0;
|
|
@@ -25815,7 +26718,7 @@ var RunCoordinator = class {
|
|
|
25815
26718
|
* so the Aero queue is never starved of a follow-up.
|
|
25816
26719
|
*/
|
|
25817
26720
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
25818
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
26721
|
+
const session = this.db.select().from(discoverySessions).where(eq37(discoverySessions.runId, runId)).get();
|
|
25819
26722
|
const competitorMap = session ? parseJsonColumn(session.competitorMap, []) : [];
|
|
25820
26723
|
return {
|
|
25821
26724
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -25838,11 +26741,11 @@ var RunCoordinator = class {
|
|
|
25838
26741
|
|
|
25839
26742
|
// src/agent/session-registry.ts
|
|
25840
26743
|
import crypto33 from "crypto";
|
|
25841
|
-
import { eq as
|
|
26744
|
+
import { eq as eq39 } from "drizzle-orm";
|
|
25842
26745
|
|
|
25843
26746
|
// src/agent/session.ts
|
|
25844
|
-
import
|
|
25845
|
-
import
|
|
26747
|
+
import fs12 from "fs";
|
|
26748
|
+
import path14 from "path";
|
|
25846
26749
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
25847
26750
|
import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
|
|
25848
26751
|
|
|
@@ -25943,26 +26846,26 @@ function buildAgentProvidersResponse(config) {
|
|
|
25943
26846
|
}
|
|
25944
26847
|
|
|
25945
26848
|
// src/agent/skill-paths.ts
|
|
25946
|
-
import
|
|
25947
|
-
import
|
|
26849
|
+
import fs10 from "fs";
|
|
26850
|
+
import path12 from "path";
|
|
25948
26851
|
import { fileURLToPath } from "url";
|
|
25949
26852
|
function resolveAeroSkillDir(pkgDir) {
|
|
25950
|
-
const here = pkgDir ??
|
|
26853
|
+
const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
|
|
25951
26854
|
const candidates = [
|
|
25952
|
-
|
|
25953
|
-
|
|
25954
|
-
|
|
26855
|
+
path12.join(here, "../assets/agent-workspace/skills/aero"),
|
|
26856
|
+
path12.join(here, "../../assets/agent-workspace/skills/aero"),
|
|
26857
|
+
path12.join(here, "../../../../skills/aero")
|
|
25955
26858
|
];
|
|
25956
26859
|
for (const candidate of candidates) {
|
|
25957
|
-
if (
|
|
26860
|
+
if (fs10.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
|
|
25958
26861
|
}
|
|
25959
26862
|
throw new Error(`Aero skill not found. Searched:
|
|
25960
26863
|
${candidates.join("\n ")}`);
|
|
25961
26864
|
}
|
|
25962
26865
|
|
|
25963
26866
|
// src/agent/skill-tools.ts
|
|
25964
|
-
import
|
|
25965
|
-
import
|
|
26867
|
+
import fs11 from "fs";
|
|
26868
|
+
import path13 from "path";
|
|
25966
26869
|
import { Type } from "@sinclair/typebox";
|
|
25967
26870
|
var MAX_DOC_CHARS = 2e4;
|
|
25968
26871
|
function textResult(details) {
|
|
@@ -25983,13 +26886,13 @@ function parseDescription(body) {
|
|
|
25983
26886
|
return "(no description)";
|
|
25984
26887
|
}
|
|
25985
26888
|
function scanSkillDocs(skillDir) {
|
|
25986
|
-
const refsDir =
|
|
25987
|
-
if (!
|
|
26889
|
+
const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
|
|
26890
|
+
if (!fs11.existsSync(refsDir)) return [];
|
|
25988
26891
|
const entries = [];
|
|
25989
|
-
for (const file of
|
|
26892
|
+
for (const file of fs11.readdirSync(refsDir)) {
|
|
25990
26893
|
if (!file.endsWith(".md")) continue;
|
|
25991
|
-
const filePath =
|
|
25992
|
-
const body =
|
|
26894
|
+
const filePath = path13.join(refsDir, file);
|
|
26895
|
+
const body = fs11.readFileSync(filePath, "utf-8");
|
|
25993
26896
|
entries.push({
|
|
25994
26897
|
slug: file.replace(/\.md$/, ""),
|
|
25995
26898
|
description: parseDescription(body),
|
|
@@ -26032,8 +26935,8 @@ function buildReadSkillDocTool() {
|
|
|
26032
26935
|
availableSlugs: docs.map((d) => d.slug)
|
|
26033
26936
|
});
|
|
26034
26937
|
}
|
|
26035
|
-
const filePath =
|
|
26036
|
-
const content =
|
|
26938
|
+
const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
|
|
26939
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
26037
26940
|
if (content.length > MAX_DOC_CHARS) {
|
|
26038
26941
|
return textResult({
|
|
26039
26942
|
slug: match.slug,
|
|
@@ -26127,10 +27030,10 @@ function ensureBuiltinsRegistered() {
|
|
|
26127
27030
|
}
|
|
26128
27031
|
function loadAeroSystemPrompt(pkgDir) {
|
|
26129
27032
|
const skillDir = resolveAeroSkillDir(pkgDir);
|
|
26130
|
-
const skillBody =
|
|
26131
|
-
const soulPath =
|
|
26132
|
-
if (!
|
|
26133
|
-
const soulBody =
|
|
27033
|
+
const skillBody = fs12.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
|
|
27034
|
+
const soulPath = path14.join(skillDir, "soul.md");
|
|
27035
|
+
if (!fs12.existsSync(soulPath)) return skillBody;
|
|
27036
|
+
const soulBody = fs12.readFileSync(soulPath, "utf-8");
|
|
26134
27037
|
return `${soulBody.trimEnd()}
|
|
26135
27038
|
|
|
26136
27039
|
---
|
|
@@ -26188,7 +27091,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
26188
27091
|
|
|
26189
27092
|
// src/agent/memory-store.ts
|
|
26190
27093
|
import crypto32 from "crypto";
|
|
26191
|
-
import { and as
|
|
27094
|
+
import { and as and27, desc as desc17, eq as eq38, like as like2, sql as sql14 } from "drizzle-orm";
|
|
26192
27095
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
26193
27096
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
26194
27097
|
function rowToDto2(row) {
|
|
@@ -26202,7 +27105,7 @@ function rowToDto2(row) {
|
|
|
26202
27105
|
};
|
|
26203
27106
|
}
|
|
26204
27107
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
26205
|
-
const query = db.select().from(agentMemory).where(
|
|
27108
|
+
const query = db.select().from(agentMemory).where(eq38(agentMemory.projectId, projectId)).orderBy(desc17(agentMemory.updatedAt));
|
|
26206
27109
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
26207
27110
|
return rows.map(rowToDto2);
|
|
26208
27111
|
}
|
|
@@ -26233,12 +27136,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
26233
27136
|
updatedAt: now
|
|
26234
27137
|
}
|
|
26235
27138
|
}).run();
|
|
26236
|
-
const row = db.select().from(agentMemory).where(
|
|
27139
|
+
const row = db.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, args.key))).get();
|
|
26237
27140
|
if (!row) throw new Error("memory upsert produced no row");
|
|
26238
27141
|
return rowToDto2(row);
|
|
26239
27142
|
}
|
|
26240
27143
|
function deleteMemoryEntry(db, projectId, key) {
|
|
26241
|
-
const result = db.delete(agentMemory).where(
|
|
27144
|
+
const result = db.delete(agentMemory).where(and27(eq38(agentMemory.projectId, projectId), eq38(agentMemory.key, key))).run();
|
|
26242
27145
|
const changes = result.changes ?? 0;
|
|
26243
27146
|
return changes > 0;
|
|
26244
27147
|
}
|
|
@@ -26267,16 +27170,16 @@ function writeCompactionNote(db, args) {
|
|
|
26267
27170
|
}).run();
|
|
26268
27171
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
26269
27172
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
26270
|
-
|
|
26271
|
-
|
|
27173
|
+
and27(
|
|
27174
|
+
eq38(agentMemory.projectId, args.projectId),
|
|
26272
27175
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
26273
27176
|
)
|
|
26274
27177
|
).orderBy(desc17(agentMemory.updatedAt)).all();
|
|
26275
27178
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
26276
27179
|
if (stale.length > 0) {
|
|
26277
|
-
tx.delete(agentMemory).where(
|
|
27180
|
+
tx.delete(agentMemory).where(sql14`${agentMemory.id} IN (${sql14.join(stale.map((s) => sql14`${s}`), sql14`, `)})`).run();
|
|
26278
27181
|
}
|
|
26279
|
-
const row = tx.select().from(agentMemory).where(
|
|
27182
|
+
const row = tx.select().from(agentMemory).where(and27(eq38(agentMemory.projectId, args.projectId), eq38(agentMemory.key, key))).get();
|
|
26280
27183
|
if (row) inserted = rowToDto2(row);
|
|
26281
27184
|
});
|
|
26282
27185
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -26458,7 +27361,7 @@ var SessionRegistry = class {
|
|
|
26458
27361
|
modelProvider: effectiveProvider,
|
|
26459
27362
|
modelId: effectiveModelId,
|
|
26460
27363
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26461
|
-
}).where(
|
|
27364
|
+
}).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26462
27365
|
}
|
|
26463
27366
|
const agent2 = createAeroSession({
|
|
26464
27367
|
projectName,
|
|
@@ -26672,7 +27575,7 @@ ${lines.join("\n")}
|
|
|
26672
27575
|
modelProvider: nextProvider,
|
|
26673
27576
|
modelId: nextModelId,
|
|
26674
27577
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26675
|
-
}).where(
|
|
27578
|
+
}).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26676
27579
|
}
|
|
26677
27580
|
/** Persist a session's transcript back to the DB. Call after any run settles. */
|
|
26678
27581
|
save(projectName) {
|
|
@@ -26834,11 +27737,11 @@ ${lines.join("\n")}
|
|
|
26834
27737
|
return id;
|
|
26835
27738
|
}
|
|
26836
27739
|
tryResolveProjectId(projectName) {
|
|
26837
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
27740
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq39(projects.name, projectName)).get();
|
|
26838
27741
|
return row?.id;
|
|
26839
27742
|
}
|
|
26840
27743
|
loadRow(projectId) {
|
|
26841
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
27744
|
+
const row = this.opts.db.select().from(agentSessions).where(eq39(agentSessions.projectId, projectId)).get();
|
|
26842
27745
|
return row ?? null;
|
|
26843
27746
|
}
|
|
26844
27747
|
insertRow(params) {
|
|
@@ -26857,14 +27760,14 @@ ${lines.join("\n")}
|
|
|
26857
27760
|
}
|
|
26858
27761
|
updateRow(projectId, patch) {
|
|
26859
27762
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26860
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
27763
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq39(agentSessions.projectId, projectId)).run();
|
|
26861
27764
|
}
|
|
26862
27765
|
};
|
|
26863
27766
|
|
|
26864
27767
|
// src/agent/agent-routes.ts
|
|
26865
|
-
import { eq as
|
|
27768
|
+
import { eq as eq40 } from "drizzle-orm";
|
|
26866
27769
|
function resolveProject2(db, name) {
|
|
26867
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
27770
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq40(projects.name, name)).get();
|
|
26868
27771
|
if (!row) throw notFound("project", name);
|
|
26869
27772
|
return row;
|
|
26870
27773
|
}
|
|
@@ -26873,7 +27776,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
26873
27776
|
"/projects/:name/agent/transcript",
|
|
26874
27777
|
async (request) => {
|
|
26875
27778
|
const project = resolveProject2(opts.db, request.params.name);
|
|
26876
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
27779
|
+
const row = opts.db.select().from(agentSessions).where(eq40(agentSessions.projectId, project.id)).get();
|
|
26877
27780
|
if (!row) {
|
|
26878
27781
|
return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
26879
27782
|
}
|
|
@@ -26897,7 +27800,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
26897
27800
|
async (request) => {
|
|
26898
27801
|
const project = resolveProject2(opts.db, request.params.name);
|
|
26899
27802
|
opts.sessionRegistry.reset(project.name);
|
|
26900
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
27803
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq40(agentSessions.projectId, project.id)).run();
|
|
26901
27804
|
return { status: "reset" };
|
|
26902
27805
|
}
|
|
26903
27806
|
);
|
|
@@ -27015,7 +27918,7 @@ import { runAeoAudit } from "@ainyc/aeo-audit";
|
|
|
27015
27918
|
|
|
27016
27919
|
// src/site-fetch.ts
|
|
27017
27920
|
import https2 from "https";
|
|
27018
|
-
var
|
|
27921
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
27019
27922
|
var MAX_TEXT_LENGTH = 4e3;
|
|
27020
27923
|
var MAX_BODY_BYTES = 512e3;
|
|
27021
27924
|
var USER_AGENT = "Canonry/1.0 (site-analysis)";
|
|
@@ -27032,15 +27935,15 @@ function extractHostname(domain) {
|
|
|
27032
27935
|
function fetchWithPinnedAddress(target) {
|
|
27033
27936
|
return new Promise((resolve) => {
|
|
27034
27937
|
const port = target.url.port ? Number(target.url.port) : 443;
|
|
27035
|
-
const
|
|
27938
|
+
const path16 = target.url.pathname + target.url.search;
|
|
27036
27939
|
const req = https2.request(
|
|
27037
27940
|
{
|
|
27038
27941
|
hostname: target.address,
|
|
27039
27942
|
family: target.family,
|
|
27040
27943
|
port,
|
|
27041
|
-
path:
|
|
27944
|
+
path: path16,
|
|
27042
27945
|
method: "GET",
|
|
27043
|
-
timeout:
|
|
27946
|
+
timeout: FETCH_TIMEOUT_MS2,
|
|
27044
27947
|
servername: target.url.hostname,
|
|
27045
27948
|
// SNI for TLS
|
|
27046
27949
|
headers: {
|
|
@@ -27361,7 +28264,7 @@ var SnapshotService = class {
|
|
|
27361
28264
|
provider: provider.adapter.name,
|
|
27362
28265
|
displayName: provider.adapter.displayName,
|
|
27363
28266
|
model: raw.model,
|
|
27364
|
-
mentioned: determineAnswerMentioned(normalized.answerText, ctx.companyName, answerVisibilityDomains),
|
|
28267
|
+
mentioned: determineAnswerMentioned(normalized.answerText, [ctx.companyName], answerVisibilityDomains),
|
|
27365
28268
|
cited: citesTargetDomain(normalized.citedDomains, normalized.groundingSources, ctx.domain),
|
|
27366
28269
|
describedAccurately: "unknown",
|
|
27367
28270
|
accuracyNotes: null,
|
|
@@ -27728,8 +28631,8 @@ function clipText(value, length) {
|
|
|
27728
28631
|
}
|
|
27729
28632
|
|
|
27730
28633
|
// src/server.ts
|
|
27731
|
-
var
|
|
27732
|
-
var { version:
|
|
28634
|
+
var _require3 = createRequire4(import.meta.url);
|
|
28635
|
+
var { version: PKG_VERSION2 } = _require3("../package.json");
|
|
27733
28636
|
var log14 = createLogger("Server");
|
|
27734
28637
|
var DEFAULT_QUOTA = {
|
|
27735
28638
|
maxConcurrency: 2,
|
|
@@ -27920,7 +28823,7 @@ async function createServer(opts) {
|
|
|
27920
28823
|
intelligenceService,
|
|
27921
28824
|
(runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
|
|
27922
28825
|
async (ctx) => {
|
|
27923
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
28826
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq41(projects.id, ctx.projectId)).get();
|
|
27924
28827
|
if (!project) return;
|
|
27925
28828
|
let content;
|
|
27926
28829
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -27943,8 +28846,8 @@ async function createServer(opts) {
|
|
|
27943
28846
|
);
|
|
27944
28847
|
jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
|
|
27945
28848
|
const snapshotService = new SnapshotService(registry);
|
|
27946
|
-
const orphanedOpenClawDir =
|
|
27947
|
-
if (
|
|
28849
|
+
const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
|
|
28850
|
+
if (fs13.existsSync(orphanedOpenClawDir)) {
|
|
27948
28851
|
app.log.warn(
|
|
27949
28852
|
{ path: orphanedOpenClawDir },
|
|
27950
28853
|
"OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
|
|
@@ -28121,7 +29024,7 @@ async function createServer(opts) {
|
|
|
28121
29024
|
const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
|
|
28122
29025
|
if (opts.config.apiKey) {
|
|
28123
29026
|
const keyHash = hashApiKey(opts.config.apiKey);
|
|
28124
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
29027
|
+
const existing = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, keyHash)).get();
|
|
28125
29028
|
if (!existing) {
|
|
28126
29029
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
28127
29030
|
opts.db.insert(apiKeys).values({
|
|
@@ -28173,7 +29076,7 @@ async function createServer(opts) {
|
|
|
28173
29076
|
};
|
|
28174
29077
|
const getDefaultApiKey = () => {
|
|
28175
29078
|
if (!opts.config.apiKey) return void 0;
|
|
28176
|
-
return opts.db.select().from(apiKeys).where(
|
|
29079
|
+
return opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
|
|
28177
29080
|
};
|
|
28178
29081
|
const createPasswordSession = (reply) => {
|
|
28179
29082
|
const key = getDefaultApiKey();
|
|
@@ -28230,12 +29133,12 @@ async function createServer(opts) {
|
|
|
28230
29133
|
return reply.send({ authenticated: true });
|
|
28231
29134
|
}
|
|
28232
29135
|
if (apiKey) {
|
|
28233
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
29136
|
+
const key = opts.db.select().from(apiKeys).where(eq41(apiKeys.keyHash, hashApiKey(apiKey))).get();
|
|
28234
29137
|
if (!key || key.revokedAt) {
|
|
28235
29138
|
const err2 = authInvalid();
|
|
28236
29139
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
28237
29140
|
}
|
|
28238
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
29141
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq41(apiKeys.id, key.id)).run();
|
|
28239
29142
|
const sessionId = createSession(key.id);
|
|
28240
29143
|
reply.header("set-cookie", serializeSessionCookie({
|
|
28241
29144
|
name: SESSION_COOKIE_NAME,
|
|
@@ -28289,6 +29192,12 @@ async function createServer(opts) {
|
|
|
28289
29192
|
skipAuth: false,
|
|
28290
29193
|
sessionCookieName: SESSION_COOKIE_NAME,
|
|
28291
29194
|
resolveSessionApiKeyId,
|
|
29195
|
+
// Local canonry serve runs on the operator's machine, where pointing a
|
|
29196
|
+
// webhook at localhost (Discord test container, Pipedream-mock dev server,
|
|
29197
|
+
// etc.) is a legitimate workflow. Default to allowing it for the local
|
|
29198
|
+
// installer; cloud deployments inherit the secure default of `false` by
|
|
29199
|
+
// not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
|
|
29200
|
+
allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
|
|
28292
29201
|
// Local-only Aero agent routes. Registered here so they inherit api-routes'
|
|
28293
29202
|
// auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
|
|
28294
29203
|
registerAuthenticatedRoutes: async (scope) => {
|
|
@@ -28409,7 +29318,7 @@ async function createServer(opts) {
|
|
|
28409
29318
|
discoverLatestRelease,
|
|
28410
29319
|
openApiInfo: {
|
|
28411
29320
|
title: "Canonry API",
|
|
28412
|
-
version:
|
|
29321
|
+
version: PKG_VERSION2,
|
|
28413
29322
|
includeCanonryLocal: true
|
|
28414
29323
|
},
|
|
28415
29324
|
providerSummary,
|
|
@@ -28556,6 +29465,19 @@ async function createServer(opts) {
|
|
|
28556
29465
|
onProjectDeleted: (projectId) => {
|
|
28557
29466
|
scheduler.removeAllForProject(projectId);
|
|
28558
29467
|
},
|
|
29468
|
+
onAliasesChanged: (projectId, projectName) => {
|
|
29469
|
+
setImmediate(() => {
|
|
29470
|
+
try {
|
|
29471
|
+
const result = backfillProjectAnswerMentions(opts.db, projectId);
|
|
29472
|
+
app.log.info(
|
|
29473
|
+
{ projectId, projectName, ...result },
|
|
29474
|
+
"aliases changed \u2014 recomputed mention fields on historical snapshots"
|
|
29475
|
+
);
|
|
29476
|
+
} catch (err) {
|
|
29477
|
+
app.log.error({ err, projectId, projectName }, "alias-triggered backfill failed");
|
|
29478
|
+
}
|
|
29479
|
+
});
|
|
29480
|
+
},
|
|
28559
29481
|
getTelemetryStatus: () => {
|
|
28560
29482
|
const enabled = isTelemetryEnabled();
|
|
28561
29483
|
return {
|
|
@@ -28640,10 +29562,10 @@ async function createServer(opts) {
|
|
|
28640
29562
|
return snapshotService.createReport(input);
|
|
28641
29563
|
}
|
|
28642
29564
|
});
|
|
28643
|
-
const dirname =
|
|
28644
|
-
const assetsDir =
|
|
28645
|
-
if (
|
|
28646
|
-
const indexPath =
|
|
29565
|
+
const dirname = path15.dirname(fileURLToPath2(import.meta.url));
|
|
29566
|
+
const assetsDir = path15.join(dirname, "..", "assets");
|
|
29567
|
+
if (fs13.existsSync(assetsDir)) {
|
|
29568
|
+
const indexPath = path15.join(assetsDir, "index.html");
|
|
28647
29569
|
const injectConfig = (html) => {
|
|
28648
29570
|
const clientConfig = {};
|
|
28649
29571
|
if (basePath) clientConfig.basePath = basePath;
|
|
@@ -28661,8 +29583,8 @@ async function createServer(opts) {
|
|
|
28661
29583
|
index: false
|
|
28662
29584
|
});
|
|
28663
29585
|
const serveIndex = (_request, reply) => {
|
|
28664
|
-
if (
|
|
28665
|
-
const html =
|
|
29586
|
+
if (fs13.existsSync(indexPath)) {
|
|
29587
|
+
const html = fs13.readFileSync(indexPath, "utf-8");
|
|
28666
29588
|
return reply.type("text/html").send(injectConfig(html));
|
|
28667
29589
|
}
|
|
28668
29590
|
return reply.status(404).send({ error: "Dashboard not built" });
|
|
@@ -28682,23 +29604,28 @@ async function createServer(opts) {
|
|
|
28682
29604
|
if (basePath && !url.startsWith(basePath)) {
|
|
28683
29605
|
return reply.status(404).send({ error: "Not found", path: request.url });
|
|
28684
29606
|
}
|
|
28685
|
-
if (
|
|
28686
|
-
const html =
|
|
29607
|
+
if (fs13.existsSync(indexPath)) {
|
|
29608
|
+
const html = fs13.readFileSync(indexPath, "utf-8");
|
|
28687
29609
|
return reply.type("text/html").send(injectConfig(html));
|
|
28688
29610
|
}
|
|
28689
29611
|
return reply.status(404).send({ error: "Not found" });
|
|
28690
29612
|
});
|
|
28691
29613
|
}
|
|
28692
|
-
const healthHandler =
|
|
28693
|
-
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
28697
|
-
|
|
29614
|
+
const healthHandler = () => {
|
|
29615
|
+
const update = checkLatestVersionForServer();
|
|
29616
|
+
return {
|
|
29617
|
+
status: "ok",
|
|
29618
|
+
service: "canonry",
|
|
29619
|
+
version: PKG_VERSION2,
|
|
29620
|
+
...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
|
|
29621
|
+
...update ? { updateAvailable: update } : {}
|
|
29622
|
+
};
|
|
29623
|
+
};
|
|
28698
29624
|
app.get("/health", healthHandler);
|
|
28699
29625
|
if (basePath) {
|
|
28700
29626
|
app.get(`${basePath}health`, healthHandler);
|
|
28701
29627
|
}
|
|
29628
|
+
checkLatestVersionForServer();
|
|
28702
29629
|
scheduler.start();
|
|
28703
29630
|
app.addHook("onClose", async () => {
|
|
28704
29631
|
scheduler.stop();
|
|
@@ -28759,16 +29686,17 @@ export {
|
|
|
28759
29686
|
showFirstRunNotice,
|
|
28760
29687
|
detectAndTrackUpgrade,
|
|
28761
29688
|
trackEvent,
|
|
28762
|
-
|
|
28763
|
-
|
|
28764
|
-
|
|
28765
|
-
|
|
28766
|
-
|
|
28767
|
-
|
|
28768
|
-
|
|
29689
|
+
backfillAnswerVisibilityCommand,
|
|
29690
|
+
backfillNormalizedPaths,
|
|
29691
|
+
backfillNormalizedPathsCommand,
|
|
29692
|
+
backfillAiReferralPaths,
|
|
29693
|
+
backfillAiReferralPathsCommand,
|
|
29694
|
+
backfillAnswerMentionsCommand,
|
|
29695
|
+
backfillInsightsCommand,
|
|
28769
29696
|
renderReportHtml,
|
|
28770
29697
|
setGoogleAuthConfig,
|
|
28771
29698
|
formatAuditFactorScore,
|
|
29699
|
+
checkLatestVersionForCli,
|
|
28772
29700
|
listAgentProviders,
|
|
28773
29701
|
coerceAgentProvider,
|
|
28774
29702
|
createServer
|