@canonry/canonry 4.116.0 → 4.117.1
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 +0 -1
- package/assets/agent-workspace/skills/aero/references/reporting.md +10 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +6 -0
- package/assets/assets/{BacklinksPage-Dy6Xc-LT.js → BacklinksPage-Bkafm10m.js} +1 -1
- package/assets/assets/{ChartPrimitives-CHJh5M8x.js → ChartPrimitives-NV_GgqFn.js} +1 -1
- package/assets/assets/{ProjectPage-Bjfa4CI5.js → ProjectPage-0-NnRxys.js} +1 -1
- package/assets/assets/{RunRow-DmvQnWPf.js → RunRow-df0d24S6.js} +1 -1
- package/assets/assets/{RunsPage-BxkxQEKE.js → RunsPage-CU6-4YTM.js} +1 -1
- package/assets/assets/{SettingsPage-DCyqHqDh.js → SettingsPage-BKw6B6cd.js} +1 -1
- package/assets/assets/{TrafficPage-BT_ae5aO.js → TrafficPage-CzpiYHKj.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Cps-tSHn.js → TrafficSourceDetailPage-CC5nuqM1.js} +1 -1
- package/assets/assets/{arrow-left-D8vdqhx4.js → arrow-left-CbQcW2pL.js} +1 -1
- package/assets/assets/{extract-error-message-BOY5txUA.js → extract-error-message-CvqIZ-le.js} +1 -1
- package/assets/assets/index-LjcWHaNZ.js +210 -0
- package/assets/assets/{trash-2-ap_Fa4zA.js → trash-2-3J2u0woR.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-RVH6QPTA.js → chunk-BMBHUFMI.js} +50 -1
- package/dist/{chunk-IQPMK36Y.js → chunk-IAJ5TQ4S.js} +155 -3
- package/dist/{chunk-GV665WOU.js → chunk-NY7SC4ES.js} +6 -5
- package/dist/{chunk-WEAXJ25Y.js → chunk-V4SW7SVX.js} +1083 -667
- package/dist/cli.js +93 -4
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-HEN5HLUM.js → intelligence-service-C5YLASIP.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/index-DxjY63VC.js +0 -210
|
@@ -173,6 +173,7 @@ import {
|
|
|
173
173
|
gscSitemapListResponseDtoSchema,
|
|
174
174
|
gscUrlInspectionDtoSchema,
|
|
175
175
|
hasLocationLabel,
|
|
176
|
+
hostOf,
|
|
176
177
|
indexingRequestResponseDtoSchema,
|
|
177
178
|
internalError,
|
|
178
179
|
isReadOnlyKey,
|
|
@@ -259,8 +260,10 @@ import {
|
|
|
259
260
|
trafficSyncResponseSchema,
|
|
260
261
|
unsupportedKind,
|
|
261
262
|
validationError,
|
|
263
|
+
visibilityCompareDtoSchema,
|
|
262
264
|
visibilityStateFromAnswerMentioned,
|
|
263
265
|
visibilityStatsDtoSchema,
|
|
266
|
+
wilsonInterval,
|
|
264
267
|
windowCutoff,
|
|
265
268
|
winnabilityClassLabel,
|
|
266
269
|
winnabilityClassSchema,
|
|
@@ -277,10 +280,10 @@ import {
|
|
|
277
280
|
wordpressSchemaDeployResultDtoSchema,
|
|
278
281
|
wordpressSchemaStatusResultDtoSchema,
|
|
279
282
|
wordpressStatusDtoSchema
|
|
280
|
-
} from "./chunk-
|
|
283
|
+
} from "./chunk-IAJ5TQ4S.js";
|
|
281
284
|
|
|
282
285
|
// src/intelligence-service.ts
|
|
283
|
-
import { eq as
|
|
286
|
+
import { eq as eq39, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray15, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
284
287
|
|
|
285
288
|
// ../db/src/client.ts
|
|
286
289
|
import { mkdirSync } from "fs";
|
|
@@ -1665,6 +1668,32 @@ CREATE TABLE IF NOT EXISTS _migrations (
|
|
|
1665
1668
|
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
1666
1669
|
);
|
|
1667
1670
|
`;
|
|
1671
|
+
function relinkOrphanedSnapshotQueryIds(tx) {
|
|
1672
|
+
const orphans = tx.all(sql`
|
|
1673
|
+
SELECT qs.id AS snapId, qs.query_text AS text, r.project_id AS projectId
|
|
1674
|
+
FROM query_snapshots qs
|
|
1675
|
+
JOIN runs r ON r.id = qs.run_id
|
|
1676
|
+
WHERE qs.query_id IS NULL AND qs.query_text IS NOT NULL
|
|
1677
|
+
`);
|
|
1678
|
+
if (orphans.length === 0) return;
|
|
1679
|
+
const queryRows = tx.all(sql`SELECT id, project_id AS projectId, query FROM queries`);
|
|
1680
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
1681
|
+
for (const row of queryRows) {
|
|
1682
|
+
let perProject = byProject.get(row.projectId);
|
|
1683
|
+
if (!perProject) {
|
|
1684
|
+
perProject = /* @__PURE__ */ new Map();
|
|
1685
|
+
byProject.set(row.projectId, perProject);
|
|
1686
|
+
}
|
|
1687
|
+
const key = normalizeQueryText(row.query);
|
|
1688
|
+
if (!perProject.has(key)) perProject.set(key, row.id);
|
|
1689
|
+
}
|
|
1690
|
+
for (const orphan of orphans) {
|
|
1691
|
+
const queryId = byProject.get(orphan.projectId)?.get(normalizeQueryText(orphan.text));
|
|
1692
|
+
if (queryId) {
|
|
1693
|
+
tx.run(sql`UPDATE query_snapshots SET query_id = ${queryId} WHERE id = ${orphan.snapId}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1668
1697
|
var MIGRATION_VERSIONS = [
|
|
1669
1698
|
{
|
|
1670
1699
|
version: 2,
|
|
@@ -3555,6 +3584,19 @@ var MIGRATION_VERSIONS = [
|
|
|
3555
3584
|
`ALTER TABLE ai_referral_events_hourly ADD COLUMN paid_sessions_or_hits INTEGER NOT NULL DEFAULT 0`,
|
|
3556
3585
|
`ALTER TABLE ai_referral_events_hourly ADD COLUMN organic_sessions_or_hits INTEGER NOT NULL DEFAULT 0`
|
|
3557
3586
|
]
|
|
3587
|
+
},
|
|
3588
|
+
{
|
|
3589
|
+
// One-time self-heal for installs whose snapshot->query FKs were orphaned
|
|
3590
|
+
// by the pre-fix delete-all + reinsert replace paths. Data-only UPDATEs
|
|
3591
|
+
// (no schema change) via run() — TS matching because SQLite lower() is
|
|
3592
|
+
// ASCII-only; see the doc comment on relinkOrphanedSnapshotQueryIds and
|
|
3593
|
+
// the downgrade-safety RUN_HOOK_ALLOWLIST justification.
|
|
3594
|
+
version: 98,
|
|
3595
|
+
name: "relink-orphaned-snapshot-query-ids",
|
|
3596
|
+
statements: [],
|
|
3597
|
+
run: (tx) => {
|
|
3598
|
+
relinkOrphanedSnapshotQueryIds(tx);
|
|
3599
|
+
}
|
|
3558
3600
|
}
|
|
3559
3601
|
];
|
|
3560
3602
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5647,8 +5689,8 @@ var METRIC_LABELS = {
|
|
|
5647
5689
|
WEBSITE_CLICKS: "Website clicks",
|
|
5648
5690
|
CALL_CLICKS: "Call clicks"
|
|
5649
5691
|
};
|
|
5650
|
-
function metricLabel(
|
|
5651
|
-
return METRIC_LABELS[
|
|
5692
|
+
function metricLabel(metric2) {
|
|
5693
|
+
return METRIC_LABELS[metric2] ?? metric2;
|
|
5652
5694
|
}
|
|
5653
5695
|
function formatAmenityList(amenities) {
|
|
5654
5696
|
if (amenities.length === 1) return amenities[0];
|
|
@@ -5742,12 +5784,12 @@ function analyzeGbp(signals) {
|
|
|
5742
5784
|
}
|
|
5743
5785
|
function pickWorstMetricDrop(loc) {
|
|
5744
5786
|
let worst = null;
|
|
5745
|
-
for (const
|
|
5746
|
-
const delta = loc.metricDeltaPct[
|
|
5747
|
-
const prior = loc.metricPrior7d[
|
|
5787
|
+
for (const metric2 of GBP_HEADLINE_METRICS) {
|
|
5788
|
+
const delta = loc.metricDeltaPct[metric2];
|
|
5789
|
+
const prior = loc.metricPrior7d[metric2] ?? 0;
|
|
5748
5790
|
if (delta == null || delta > -GBP_METRIC_DROP_PCT || prior < GBP_METRIC_MIN_BASELINE) continue;
|
|
5749
5791
|
if (!worst || delta < worst.deltaPct) {
|
|
5750
|
-
worst = { metric, deltaPct: delta, recent: loc.metricRecent7d[
|
|
5792
|
+
worst = { metric: metric2, deltaPct: delta, recent: loc.metricRecent7d[metric2] ?? 0, prior };
|
|
5751
5793
|
}
|
|
5752
5794
|
}
|
|
5753
5795
|
return worst;
|
|
@@ -6462,18 +6504,88 @@ function aliasArraysEqual(a, b) {
|
|
|
6462
6504
|
}
|
|
6463
6505
|
|
|
6464
6506
|
// ../api-routes/src/queries.ts
|
|
6507
|
+
import crypto6 from "crypto";
|
|
6508
|
+
import { and, eq as eq5, inArray as inArray2, sql as sql4 } from "drizzle-orm";
|
|
6509
|
+
|
|
6510
|
+
// ../api-routes/src/query-replace.ts
|
|
6465
6511
|
import crypto5 from "crypto";
|
|
6466
|
-
import {
|
|
6512
|
+
import { eq as eq4, inArray } from "drizzle-orm";
|
|
6467
6513
|
function preserveSnapshotQueryText(tx, projectId, queryIds) {
|
|
6468
6514
|
const candidates = queryIds && queryIds.length > 0 ? tx.select({ id: queries.id, text: queries.query }).from(queries).where(inArray(queries.id, queryIds)).all() : tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
|
|
6469
6515
|
for (const q of candidates) {
|
|
6470
6516
|
tx.update(querySnapshots).set({ queryText: q.text }).where(eq4(querySnapshots.queryId, q.id)).run();
|
|
6471
6517
|
}
|
|
6472
6518
|
}
|
|
6519
|
+
function diffProjectQueries(existing, incomingTexts) {
|
|
6520
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6521
|
+
for (const row of existing) {
|
|
6522
|
+
const key = normalizeQueryText(row.text);
|
|
6523
|
+
const group = groups.get(key);
|
|
6524
|
+
if (group) group.push(row);
|
|
6525
|
+
else groups.set(key, [row]);
|
|
6526
|
+
}
|
|
6527
|
+
const kept = [];
|
|
6528
|
+
const duplicates = [];
|
|
6529
|
+
const removed = [];
|
|
6530
|
+
const insertedTexts = [];
|
|
6531
|
+
const seenIncoming = /* @__PURE__ */ new Set();
|
|
6532
|
+
for (const text2 of incomingTexts) {
|
|
6533
|
+
const key = normalizeQueryText(text2);
|
|
6534
|
+
if (seenIncoming.has(key)) continue;
|
|
6535
|
+
seenIncoming.add(key);
|
|
6536
|
+
const group = groups.get(key);
|
|
6537
|
+
if (group) {
|
|
6538
|
+
groups.delete(key);
|
|
6539
|
+
const [canonical, ...extras] = group;
|
|
6540
|
+
kept.push({ id: canonical.id, currentText: canonical.text, incomingText: text2 });
|
|
6541
|
+
for (const extra of extras) {
|
|
6542
|
+
duplicates.push({ id: extra.id, text: extra.text, keptId: canonical.id });
|
|
6543
|
+
}
|
|
6544
|
+
} else {
|
|
6545
|
+
insertedTexts.push(text2);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
for (const group of groups.values()) {
|
|
6549
|
+
for (const row of group) removed.push({ id: row.id, text: row.text });
|
|
6550
|
+
}
|
|
6551
|
+
return { kept, duplicates, removed, insertedTexts };
|
|
6552
|
+
}
|
|
6553
|
+
function replaceProjectQueries(tx, projectId, incomingTexts, now) {
|
|
6554
|
+
const existing = tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
|
|
6555
|
+
const diff = diffProjectQueries(existing, incomingTexts);
|
|
6556
|
+
if (diff.duplicates.length > 0) {
|
|
6557
|
+
for (const dup of diff.duplicates) {
|
|
6558
|
+
tx.update(querySnapshots).set({ queryId: dup.keptId }).where(eq4(querySnapshots.queryId, dup.id)).run();
|
|
6559
|
+
}
|
|
6560
|
+
tx.delete(queries).where(inArray(queries.id, diff.duplicates.map((d) => d.id))).run();
|
|
6561
|
+
}
|
|
6562
|
+
if (diff.removed.length > 0) {
|
|
6563
|
+
const removedIds = diff.removed.map((r) => r.id);
|
|
6564
|
+
preserveSnapshotQueryText(tx, projectId, removedIds);
|
|
6565
|
+
tx.delete(queries).where(inArray(queries.id, removedIds)).run();
|
|
6566
|
+
}
|
|
6567
|
+
for (const keptRow of diff.kept) {
|
|
6568
|
+
if (keptRow.currentText !== keptRow.incomingText) {
|
|
6569
|
+
tx.update(queries).set({ query: keptRow.incomingText }).where(eq4(queries.id, keptRow.id)).run();
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
for (const text2 of diff.insertedTexts) {
|
|
6573
|
+
tx.insert(queries).values({
|
|
6574
|
+
id: crypto5.randomUUID(),
|
|
6575
|
+
projectId,
|
|
6576
|
+
query: text2,
|
|
6577
|
+
provenance: "cli",
|
|
6578
|
+
createdAt: now
|
|
6579
|
+
}).run();
|
|
6580
|
+
}
|
|
6581
|
+
return diff;
|
|
6582
|
+
}
|
|
6583
|
+
|
|
6584
|
+
// ../api-routes/src/queries.ts
|
|
6473
6585
|
async function queryRoutes(app, opts) {
|
|
6474
6586
|
app.get("/projects/:name/queries", async (request, reply) => {
|
|
6475
6587
|
const project = resolveProject(app.db, request.params.name);
|
|
6476
|
-
const rows = app.db.select().from(queries).where(
|
|
6588
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6477
6589
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6478
6590
|
});
|
|
6479
6591
|
app.put("/projects/:name/queries", async (request, reply) => {
|
|
@@ -6484,17 +6596,7 @@ async function queryRoutes(app, opts) {
|
|
|
6484
6596
|
}
|
|
6485
6597
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6486
6598
|
app.db.transaction((tx) => {
|
|
6487
|
-
|
|
6488
|
-
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
6489
|
-
for (const q of body.queries) {
|
|
6490
|
-
tx.insert(queries).values({
|
|
6491
|
-
id: crypto5.randomUUID(),
|
|
6492
|
-
projectId: project.id,
|
|
6493
|
-
query: q,
|
|
6494
|
-
provenance: "cli",
|
|
6495
|
-
createdAt: now
|
|
6496
|
-
}).run();
|
|
6497
|
-
}
|
|
6599
|
+
replaceProjectQueries(tx, project.id, body.queries, now);
|
|
6498
6600
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6499
6601
|
projectId: project.id,
|
|
6500
6602
|
actor: "api",
|
|
@@ -6503,7 +6605,7 @@ async function queryRoutes(app, opts) {
|
|
|
6503
6605
|
diff: { queries: body.queries }
|
|
6504
6606
|
}));
|
|
6505
6607
|
});
|
|
6506
|
-
const rows = app.db.select().from(queries).where(
|
|
6608
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6507
6609
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6508
6610
|
});
|
|
6509
6611
|
app.post("/projects/:name/queries/replace-preview", async (request, reply) => {
|
|
@@ -6512,20 +6614,22 @@ async function queryRoutes(app, opts) {
|
|
|
6512
6614
|
if (!body || !Array.isArray(body.queries)) {
|
|
6513
6615
|
throw validationError('Body must contain a "queries" array');
|
|
6514
6616
|
}
|
|
6515
|
-
const currentRows = app.db.select().from(queries).where(
|
|
6617
|
+
const currentRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6516
6618
|
const currentTexts = currentRows.map((r) => r.query);
|
|
6517
|
-
const
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
const
|
|
6522
|
-
const
|
|
6619
|
+
const diff = diffProjectQueries(
|
|
6620
|
+
currentRows.map((r) => ({ id: r.id, text: r.query })),
|
|
6621
|
+
body.queries
|
|
6622
|
+
);
|
|
6623
|
+
const removed = diff.removed.map((r) => r.text);
|
|
6624
|
+
const added = diff.insertedTexts;
|
|
6625
|
+
const unchanged = diff.kept.map((k) => k.currentText);
|
|
6626
|
+
const removedIds = diff.removed.map((r) => r.id);
|
|
6523
6627
|
let snapshotsDetached = 0;
|
|
6524
6628
|
let affectedQueries = 0;
|
|
6525
|
-
if (
|
|
6526
|
-
const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(
|
|
6629
|
+
if (removedIds.length > 0) {
|
|
6630
|
+
const snapshotCount = app.db.select({ n: sql4`count(*)` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
|
|
6527
6631
|
snapshotsDetached = snapshotCount?.n ?? 0;
|
|
6528
|
-
const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(
|
|
6632
|
+
const distinctAffected = app.db.select({ n: sql4`count(distinct ${querySnapshots.queryId})` }).from(querySnapshots).where(inArray2(querySnapshots.queryId, removedIds)).get();
|
|
6529
6633
|
affectedQueries = distinctAffected?.n ?? 0;
|
|
6530
6634
|
}
|
|
6531
6635
|
return reply.send({
|
|
@@ -6542,14 +6646,14 @@ async function queryRoutes(app, opts) {
|
|
|
6542
6646
|
if (!body || !Array.isArray(body.queries) || body.queries.length === 0) {
|
|
6543
6647
|
throw validationError('Body must contain a non-empty "queries" array');
|
|
6544
6648
|
}
|
|
6545
|
-
const existing = app.db.select().from(queries).where(
|
|
6649
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6546
6650
|
const toDelete = new Set(body.queries);
|
|
6547
6651
|
const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
|
|
6548
6652
|
if (idsToDelete.length > 0) {
|
|
6549
6653
|
app.db.transaction((tx) => {
|
|
6550
6654
|
preserveSnapshotQueryText(tx, project.id, idsToDelete);
|
|
6551
6655
|
for (const id of idsToDelete) {
|
|
6552
|
-
tx.delete(queries).where(
|
|
6656
|
+
tx.delete(queries).where(eq5(queries.id, id)).run();
|
|
6553
6657
|
}
|
|
6554
6658
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6555
6659
|
projectId: project.id,
|
|
@@ -6560,18 +6664,18 @@ async function queryRoutes(app, opts) {
|
|
|
6560
6664
|
}));
|
|
6561
6665
|
});
|
|
6562
6666
|
}
|
|
6563
|
-
const rows = app.db.select().from(queries).where(
|
|
6667
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6564
6668
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6565
6669
|
});
|
|
6566
6670
|
app.delete("/projects/:name/queries/:id", async (request, reply) => {
|
|
6567
6671
|
const project = resolveProject(app.db, request.params.name);
|
|
6568
|
-
const query = app.db.select().from(queries).where(and(
|
|
6672
|
+
const query = app.db.select().from(queries).where(and(eq5(queries.projectId, project.id), eq5(queries.id, request.params.id))).get();
|
|
6569
6673
|
if (!query) {
|
|
6570
6674
|
throw notFound("Query", request.params.id);
|
|
6571
6675
|
}
|
|
6572
6676
|
app.db.transaction((tx) => {
|
|
6573
6677
|
preserveSnapshotQueryText(tx, project.id, [query.id]);
|
|
6574
|
-
tx.delete(queries).where(
|
|
6678
|
+
tx.delete(queries).where(eq5(queries.id, query.id)).run();
|
|
6575
6679
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6576
6680
|
projectId: project.id,
|
|
6577
6681
|
actor: "api",
|
|
@@ -6590,13 +6694,13 @@ async function queryRoutes(app, opts) {
|
|
|
6590
6694
|
throw validationError('Body must contain a "queries" array');
|
|
6591
6695
|
}
|
|
6592
6696
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6593
|
-
const existing = app.db.select().from(queries).where(
|
|
6697
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6594
6698
|
const existingSet = new Set(existing.map((q) => q.query));
|
|
6595
6699
|
const added = [];
|
|
6596
6700
|
for (const q of body.queries) {
|
|
6597
6701
|
if (!existingSet.has(q)) {
|
|
6598
6702
|
app.db.insert(queries).values({
|
|
6599
|
-
id:
|
|
6703
|
+
id: crypto6.randomUUID(),
|
|
6600
6704
|
projectId: project.id,
|
|
6601
6705
|
query: q,
|
|
6602
6706
|
provenance: "cli",
|
|
@@ -6615,7 +6719,7 @@ async function queryRoutes(app, opts) {
|
|
|
6615
6719
|
diff: { added }
|
|
6616
6720
|
}));
|
|
6617
6721
|
}
|
|
6618
|
-
const rows = app.db.select().from(queries).where(
|
|
6722
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6619
6723
|
return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
|
|
6620
6724
|
});
|
|
6621
6725
|
app.post("/projects/:name/queries/generate", async (request, reply) => {
|
|
@@ -6642,7 +6746,7 @@ async function queryRoutes(app, opts) {
|
|
|
6642
6746
|
if (!opts.onGenerateQueries) {
|
|
6643
6747
|
throw notImplemented("Query generation is not supported in this deployment");
|
|
6644
6748
|
}
|
|
6645
|
-
const existingRows = app.db.select().from(queries).where(
|
|
6749
|
+
const existingRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6646
6750
|
const existingQueries = existingRows.map((r) => r.query);
|
|
6647
6751
|
try {
|
|
6648
6752
|
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
@@ -6660,7 +6764,7 @@ async function queryRoutes(app, opts) {
|
|
|
6660
6764
|
});
|
|
6661
6765
|
app.get("/projects/:name/keywords", async (request, reply) => {
|
|
6662
6766
|
const project = resolveProject(app.db, request.params.name);
|
|
6663
|
-
const rows = app.db.select().from(queries).where(
|
|
6767
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6664
6768
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6665
6769
|
});
|
|
6666
6770
|
app.put("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6671,17 +6775,7 @@ async function queryRoutes(app, opts) {
|
|
|
6671
6775
|
}
|
|
6672
6776
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6673
6777
|
app.db.transaction((tx) => {
|
|
6674
|
-
|
|
6675
|
-
tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
|
|
6676
|
-
for (const keyword of body.keywords) {
|
|
6677
|
-
tx.insert(queries).values({
|
|
6678
|
-
id: crypto5.randomUUID(),
|
|
6679
|
-
projectId: project.id,
|
|
6680
|
-
query: keyword,
|
|
6681
|
-
provenance: "cli",
|
|
6682
|
-
createdAt: now
|
|
6683
|
-
}).run();
|
|
6684
|
-
}
|
|
6778
|
+
replaceProjectQueries(tx, project.id, body.keywords, now);
|
|
6685
6779
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6686
6780
|
projectId: project.id,
|
|
6687
6781
|
actor: "api",
|
|
@@ -6690,7 +6784,7 @@ async function queryRoutes(app, opts) {
|
|
|
6690
6784
|
diff: { queries: body.keywords }
|
|
6691
6785
|
}));
|
|
6692
6786
|
});
|
|
6693
|
-
const rows = app.db.select().from(queries).where(
|
|
6787
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6694
6788
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6695
6789
|
});
|
|
6696
6790
|
app.delete("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6699,14 +6793,14 @@ async function queryRoutes(app, opts) {
|
|
|
6699
6793
|
if (!body || !Array.isArray(body.keywords) || body.keywords.length === 0) {
|
|
6700
6794
|
throw validationError('Body must contain a non-empty "keywords" array');
|
|
6701
6795
|
}
|
|
6702
|
-
const existing = app.db.select().from(queries).where(
|
|
6796
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6703
6797
|
const toDelete = new Set(body.keywords);
|
|
6704
6798
|
const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
|
|
6705
6799
|
if (idsToDelete.length > 0) {
|
|
6706
6800
|
app.db.transaction((tx) => {
|
|
6707
6801
|
preserveSnapshotQueryText(tx, project.id, idsToDelete);
|
|
6708
6802
|
for (const id of idsToDelete) {
|
|
6709
|
-
tx.delete(queries).where(
|
|
6803
|
+
tx.delete(queries).where(eq5(queries.id, id)).run();
|
|
6710
6804
|
}
|
|
6711
6805
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6712
6806
|
projectId: project.id,
|
|
@@ -6717,7 +6811,7 @@ async function queryRoutes(app, opts) {
|
|
|
6717
6811
|
}));
|
|
6718
6812
|
});
|
|
6719
6813
|
}
|
|
6720
|
-
const rows = app.db.select().from(queries).where(
|
|
6814
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6721
6815
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6722
6816
|
});
|
|
6723
6817
|
app.post("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -6727,13 +6821,13 @@ async function queryRoutes(app, opts) {
|
|
|
6727
6821
|
throw validationError('Body must contain a "keywords" array');
|
|
6728
6822
|
}
|
|
6729
6823
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6730
|
-
const existing = app.db.select().from(queries).where(
|
|
6824
|
+
const existing = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6731
6825
|
const existingSet = new Set(existing.map((q) => q.query));
|
|
6732
6826
|
const added = [];
|
|
6733
6827
|
for (const keyword of body.keywords) {
|
|
6734
6828
|
if (!existingSet.has(keyword)) {
|
|
6735
6829
|
app.db.insert(queries).values({
|
|
6736
|
-
id:
|
|
6830
|
+
id: crypto6.randomUUID(),
|
|
6737
6831
|
projectId: project.id,
|
|
6738
6832
|
query: keyword,
|
|
6739
6833
|
provenance: "cli",
|
|
@@ -6752,7 +6846,7 @@ async function queryRoutes(app, opts) {
|
|
|
6752
6846
|
diff: { added }
|
|
6753
6847
|
}));
|
|
6754
6848
|
}
|
|
6755
|
-
const rows = app.db.select().from(queries).where(
|
|
6849
|
+
const rows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6756
6850
|
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
|
|
6757
6851
|
});
|
|
6758
6852
|
app.post("/projects/:name/keywords/generate", async (request, reply) => {
|
|
@@ -6779,7 +6873,7 @@ async function queryRoutes(app, opts) {
|
|
|
6779
6873
|
if (!opts.onGenerateQueries) {
|
|
6780
6874
|
throw notImplemented("Keyword generation is not supported in this deployment");
|
|
6781
6875
|
}
|
|
6782
|
-
const existingRows = app.db.select().from(queries).where(
|
|
6876
|
+
const existingRows = app.db.select().from(queries).where(eq5(queries.projectId, project.id)).all();
|
|
6783
6877
|
const existingQueries = existingRows.map((r) => r.query);
|
|
6784
6878
|
try {
|
|
6785
6879
|
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
@@ -6798,8 +6892,8 @@ async function queryRoutes(app, opts) {
|
|
|
6798
6892
|
}
|
|
6799
6893
|
|
|
6800
6894
|
// ../api-routes/src/competitors.ts
|
|
6801
|
-
import
|
|
6802
|
-
import { and as and2, eq as
|
|
6895
|
+
import crypto7 from "crypto";
|
|
6896
|
+
import { and as and2, eq as eq6 } from "drizzle-orm";
|
|
6803
6897
|
function normalizeCompetitor(domain) {
|
|
6804
6898
|
const reg = registrableDomain(domain);
|
|
6805
6899
|
if (reg) return reg;
|
|
@@ -6824,7 +6918,7 @@ function serializeCompetitor(row) {
|
|
|
6824
6918
|
async function competitorRoutes(app) {
|
|
6825
6919
|
app.get("/projects/:name/competitors", async (request, reply) => {
|
|
6826
6920
|
const project = resolveProject(app.db, request.params.name);
|
|
6827
|
-
const rows = app.db.select().from(competitors).where(
|
|
6921
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6828
6922
|
return reply.send(rows.map(serializeCompetitor));
|
|
6829
6923
|
});
|
|
6830
6924
|
app.put("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6836,10 +6930,10 @@ async function competitorRoutes(app) {
|
|
|
6836
6930
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6837
6931
|
const normalizedCompetitors = normalizeCompetitorList(body.competitors);
|
|
6838
6932
|
app.db.transaction((tx) => {
|
|
6839
|
-
tx.delete(competitors).where(
|
|
6933
|
+
tx.delete(competitors).where(eq6(competitors.projectId, project.id)).run();
|
|
6840
6934
|
for (const domain of normalizedCompetitors) {
|
|
6841
6935
|
tx.insert(competitors).values({
|
|
6842
|
-
id:
|
|
6936
|
+
id: crypto7.randomUUID(),
|
|
6843
6937
|
projectId: project.id,
|
|
6844
6938
|
domain,
|
|
6845
6939
|
provenance: "cli",
|
|
@@ -6854,7 +6948,7 @@ async function competitorRoutes(app) {
|
|
|
6854
6948
|
diff: { competitors: normalizedCompetitors }
|
|
6855
6949
|
});
|
|
6856
6950
|
});
|
|
6857
|
-
const rows = app.db.select().from(competitors).where(
|
|
6951
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6858
6952
|
return reply.send(rows.map(serializeCompetitor));
|
|
6859
6953
|
});
|
|
6860
6954
|
app.post("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6863,13 +6957,13 @@ async function competitorRoutes(app) {
|
|
|
6863
6957
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6864
6958
|
const requested = normalizeCompetitorList(body.competitors);
|
|
6865
6959
|
app.db.transaction((tx) => {
|
|
6866
|
-
const existing = tx.select().from(competitors).where(
|
|
6960
|
+
const existing = tx.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6867
6961
|
const existingSet = new Set(existing.map((c) => c.domain));
|
|
6868
6962
|
const added = requested.filter((domain) => !existingSet.has(domain));
|
|
6869
6963
|
if (added.length === 0) return;
|
|
6870
6964
|
for (const domain of added) {
|
|
6871
6965
|
tx.insert(competitors).values({
|
|
6872
|
-
id:
|
|
6966
|
+
id: crypto7.randomUUID(),
|
|
6873
6967
|
projectId: project.id,
|
|
6874
6968
|
domain,
|
|
6875
6969
|
provenance: "cli",
|
|
@@ -6886,7 +6980,7 @@ async function competitorRoutes(app) {
|
|
|
6886
6980
|
diff: { added }
|
|
6887
6981
|
});
|
|
6888
6982
|
});
|
|
6889
|
-
const rows = app.db.select().from(competitors).where(
|
|
6983
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6890
6984
|
return reply.send(rows.map(serializeCompetitor));
|
|
6891
6985
|
});
|
|
6892
6986
|
app.delete("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -6894,11 +6988,11 @@ async function competitorRoutes(app) {
|
|
|
6894
6988
|
const body = parseCompetitorBatch(request.body);
|
|
6895
6989
|
const requested = new Set(normalizeCompetitorList(body.competitors));
|
|
6896
6990
|
app.db.transaction((tx) => {
|
|
6897
|
-
const existing = tx.select().from(competitors).where(
|
|
6991
|
+
const existing = tx.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6898
6992
|
const rowsToDelete = existing.filter((c) => requested.has(c.domain));
|
|
6899
6993
|
if (rowsToDelete.length === 0) return;
|
|
6900
6994
|
for (const row of rowsToDelete) {
|
|
6901
|
-
tx.delete(competitors).where(
|
|
6995
|
+
tx.delete(competitors).where(eq6(competitors.id, row.id)).run();
|
|
6902
6996
|
}
|
|
6903
6997
|
writeAuditLog(tx, {
|
|
6904
6998
|
projectId: project.id,
|
|
@@ -6908,17 +7002,17 @@ async function competitorRoutes(app) {
|
|
|
6908
7002
|
diff: { deleted: rowsToDelete.map((row) => row.domain) }
|
|
6909
7003
|
});
|
|
6910
7004
|
});
|
|
6911
|
-
const rows = app.db.select().from(competitors).where(
|
|
7005
|
+
const rows = app.db.select().from(competitors).where(eq6(competitors.projectId, project.id)).all();
|
|
6912
7006
|
return reply.send(rows.map(serializeCompetitor));
|
|
6913
7007
|
});
|
|
6914
7008
|
app.delete("/projects/:name/competitors/:id", async (request, reply) => {
|
|
6915
7009
|
const project = resolveProject(app.db, request.params.name);
|
|
6916
|
-
const competitor = app.db.select().from(competitors).where(and2(
|
|
7010
|
+
const competitor = app.db.select().from(competitors).where(and2(eq6(competitors.projectId, project.id), eq6(competitors.id, request.params.id))).get();
|
|
6917
7011
|
if (!competitor) {
|
|
6918
7012
|
throw notFound("Competitor", request.params.id);
|
|
6919
7013
|
}
|
|
6920
7014
|
app.db.transaction((tx) => {
|
|
6921
|
-
tx.delete(competitors).where(
|
|
7015
|
+
tx.delete(competitors).where(eq6(competitors.id, competitor.id)).run();
|
|
6922
7016
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
6923
7017
|
projectId: project.id,
|
|
6924
7018
|
actor: "api",
|
|
@@ -6943,23 +7037,23 @@ function parseCompetitorBatch(value) {
|
|
|
6943
7037
|
}
|
|
6944
7038
|
|
|
6945
7039
|
// ../api-routes/src/runs.ts
|
|
6946
|
-
import
|
|
6947
|
-
import { and as and4, eq as
|
|
7040
|
+
import crypto9 from "crypto";
|
|
7041
|
+
import { and as and4, eq as eq8, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
|
|
6948
7042
|
import { gte } from "drizzle-orm";
|
|
6949
7043
|
|
|
6950
7044
|
// ../api-routes/src/run-queue.ts
|
|
6951
|
-
import
|
|
6952
|
-
import { and as and3, eq as
|
|
7045
|
+
import crypto8 from "crypto";
|
|
7046
|
+
import { and as and3, eq as eq7, or } from "drizzle-orm";
|
|
6953
7047
|
function queueRunIfProjectIdle(db, params) {
|
|
6954
7048
|
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6955
7049
|
const kind = params.kind ?? "answer-visibility";
|
|
6956
7050
|
const trigger = params.trigger ?? "manual";
|
|
6957
|
-
const runId =
|
|
7051
|
+
const runId = crypto8.randomUUID();
|
|
6958
7052
|
return db.transaction((tx) => {
|
|
6959
7053
|
const activeRun = tx.select().from(runs).where(
|
|
6960
7054
|
and3(
|
|
6961
|
-
|
|
6962
|
-
or(
|
|
7055
|
+
eq7(runs.projectId, params.projectId),
|
|
7056
|
+
or(eq7(runs.status, "queued"), eq7(runs.status, "running"))
|
|
6963
7057
|
)
|
|
6964
7058
|
).get();
|
|
6965
7059
|
if (activeRun) {
|
|
@@ -7005,7 +7099,7 @@ async function runRoutes(app, opts) {
|
|
|
7005
7099
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
7006
7100
|
let scopedQueries = null;
|
|
7007
7101
|
if (body.queries?.length) {
|
|
7008
|
-
const trackedRows = app.db.select({ query: queries.query }).from(queries).where(
|
|
7102
|
+
const trackedRows = app.db.select({ query: queries.query }).from(queries).where(eq8(queries.projectId, project.id)).all();
|
|
7009
7103
|
const tracked = new Set(trackedRows.map((r) => r.query));
|
|
7010
7104
|
const missing = body.queries.filter((q) => !tracked.has(q));
|
|
7011
7105
|
if (missing.length) {
|
|
@@ -7041,15 +7135,15 @@ async function runRoutes(app, opts) {
|
|
|
7041
7135
|
}
|
|
7042
7136
|
const result = app.db.transaction((tx) => {
|
|
7043
7137
|
const activeRun = tx.select({ id: runs.id }).from(runs).where(and4(
|
|
7044
|
-
|
|
7045
|
-
or2(
|
|
7138
|
+
eq8(runs.projectId, project.id),
|
|
7139
|
+
or2(eq8(runs.status, "queued"), eq8(runs.status, "running"))
|
|
7046
7140
|
)).get();
|
|
7047
7141
|
if (activeRun) {
|
|
7048
7142
|
return { conflict: true };
|
|
7049
7143
|
}
|
|
7050
7144
|
const inserted = [];
|
|
7051
7145
|
for (const loc of projectLocations) {
|
|
7052
|
-
const runId2 =
|
|
7146
|
+
const runId2 = crypto9.randomUUID();
|
|
7053
7147
|
tx.insert(runs).values({
|
|
7054
7148
|
id: runId2,
|
|
7055
7149
|
projectId: project.id,
|
|
@@ -7076,7 +7170,7 @@ async function runRoutes(app, opts) {
|
|
|
7076
7170
|
entityType: "run",
|
|
7077
7171
|
entityId: runId2
|
|
7078
7172
|
});
|
|
7079
|
-
const r = app.db.select().from(runs).where(
|
|
7173
|
+
const r = app.db.select().from(runs).where(eq8(runs.id, runId2)).get();
|
|
7080
7174
|
if (opts.onRunCreated) {
|
|
7081
7175
|
opts.onRunCreated(runId2, project.id, providers, loc);
|
|
7082
7176
|
}
|
|
@@ -7102,7 +7196,7 @@ async function runRoutes(app, opts) {
|
|
|
7102
7196
|
entityType: "run",
|
|
7103
7197
|
entityId: runId
|
|
7104
7198
|
});
|
|
7105
|
-
const run = app.db.select().from(runs).where(
|
|
7199
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, runId)).get();
|
|
7106
7200
|
if (opts.onRunCreated) {
|
|
7107
7201
|
opts.onRunCreated(runId, project.id, providers, resolvedLocation);
|
|
7108
7202
|
}
|
|
@@ -7113,15 +7207,15 @@ async function runRoutes(app, opts) {
|
|
|
7113
7207
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
7114
7208
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
7115
7209
|
const kind = parseListKind(request.query.kind);
|
|
7116
|
-
const where = kind ? and4(
|
|
7210
|
+
const where = kind ? and4(eq8(runs.projectId, project.id), eq8(runs.kind, kind)) : eq8(runs.projectId, project.id);
|
|
7117
7211
|
const rows = limit == null ? app.db.select().from(runs).where(where).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(where).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
7118
7212
|
return reply.send(rows.map(formatRun));
|
|
7119
7213
|
});
|
|
7120
7214
|
app.get("/projects/:name/runs/latest", async (request, reply) => {
|
|
7121
7215
|
const project = resolveProject(app.db, request.params.name);
|
|
7122
|
-
const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(
|
|
7216
|
+
const countRow = app.db.select({ count: sql5`count(*)` }).from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).get();
|
|
7123
7217
|
const totalRuns = countRow?.count ?? 0;
|
|
7124
|
-
const latestRun = app.db.select().from(runs).where(and4(
|
|
7218
|
+
const latestRun = app.db.select().from(runs).where(and4(eq8(runs.projectId, project.id), notProbeRun())).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
|
|
7125
7219
|
if (!latestRun) {
|
|
7126
7220
|
return reply.send({ totalRuns: 0, run: null });
|
|
7127
7221
|
}
|
|
@@ -7137,15 +7231,15 @@ async function runRoutes(app, opts) {
|
|
|
7137
7231
|
const kind = parseListKind(request.query.kind);
|
|
7138
7232
|
const filters = [gte(runs.createdAt, since)];
|
|
7139
7233
|
if (!includeProbe) filters.push(notProbeRun());
|
|
7140
|
-
if (kind) filters.push(
|
|
7234
|
+
if (kind) filters.push(eq8(runs.kind, kind));
|
|
7141
7235
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7142
|
-
if (scopedProjectId) filters.push(
|
|
7236
|
+
if (scopedProjectId) filters.push(eq8(runs.projectId, scopedProjectId));
|
|
7143
7237
|
const rows = app.db.select().from(runs).where(and4(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
|
|
7144
7238
|
return reply.send(rows.map(formatRun));
|
|
7145
7239
|
});
|
|
7146
7240
|
app.post("/runs", async (request, reply) => {
|
|
7147
7241
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7148
|
-
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(
|
|
7242
|
+
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(eq8(projects.id, scopedProjectId)) : app.db.select().from(projects)).all();
|
|
7149
7243
|
if (allProjects.length === 0) {
|
|
7150
7244
|
return reply.status(207).send([]);
|
|
7151
7245
|
}
|
|
@@ -7200,7 +7294,7 @@ async function runRoutes(app, opts) {
|
|
|
7200
7294
|
entityType: "run",
|
|
7201
7295
|
entityId: runId
|
|
7202
7296
|
});
|
|
7203
|
-
const run = app.db.select().from(runs).where(
|
|
7297
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, runId)).get();
|
|
7204
7298
|
if (opts.onRunCreated) {
|
|
7205
7299
|
opts.onRunCreated(runId, project.id, providers, resolvedLocation);
|
|
7206
7300
|
}
|
|
@@ -7209,13 +7303,13 @@ async function runRoutes(app, opts) {
|
|
|
7209
7303
|
return reply.status(207).send(results);
|
|
7210
7304
|
});
|
|
7211
7305
|
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
7212
|
-
const run = app.db.select().from(runs).where(
|
|
7306
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, request.params.id)).get();
|
|
7213
7307
|
if (!run) throw notFound("Run", request.params.id);
|
|
7214
7308
|
assertProjectScope(request, run.projectId);
|
|
7215
7309
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
7216
7310
|
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
7217
7311
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7218
|
-
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(
|
|
7312
|
+
app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(eq8(runs.id, run.id)).run();
|
|
7219
7313
|
writeAuditLog(app.db, {
|
|
7220
7314
|
projectId: run.projectId,
|
|
7221
7315
|
actor: "api",
|
|
@@ -7223,11 +7317,11 @@ async function runRoutes(app, opts) {
|
|
|
7223
7317
|
entityType: "run",
|
|
7224
7318
|
entityId: run.id
|
|
7225
7319
|
});
|
|
7226
|
-
const updated = app.db.select().from(runs).where(
|
|
7320
|
+
const updated = app.db.select().from(runs).where(eq8(runs.id, run.id)).get();
|
|
7227
7321
|
return reply.send(formatRun(updated));
|
|
7228
7322
|
});
|
|
7229
7323
|
app.get("/runs/:id", async (request, reply) => {
|
|
7230
|
-
const run = app.db.select().from(runs).where(
|
|
7324
|
+
const run = app.db.select().from(runs).where(eq8(runs.id, request.params.id)).get();
|
|
7231
7325
|
if (!run) throw notFound("Run", request.params.id);
|
|
7232
7326
|
assertProjectScope(request, run.projectId);
|
|
7233
7327
|
return reply.send(loadRunDetail(app, run));
|
|
@@ -7299,7 +7393,7 @@ function loadRunDetail(app, run) {
|
|
|
7299
7393
|
canonicalDomain: projects.canonicalDomain,
|
|
7300
7394
|
ownedDomains: projects.ownedDomains,
|
|
7301
7395
|
aliases: projects.aliases
|
|
7302
|
-
}).from(projects).where(
|
|
7396
|
+
}).from(projects).where(eq8(projects.id, run.projectId)).get();
|
|
7303
7397
|
const snapshots = app.db.select({
|
|
7304
7398
|
id: querySnapshots.id,
|
|
7305
7399
|
runId: querySnapshots.runId,
|
|
@@ -7316,7 +7410,7 @@ function loadRunDetail(app, run) {
|
|
|
7316
7410
|
location: querySnapshots.location,
|
|
7317
7411
|
rawResponse: querySnapshots.rawResponse,
|
|
7318
7412
|
createdAt: querySnapshots.createdAt
|
|
7319
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
7413
|
+
}).from(querySnapshots).leftJoin(queries, eq8(querySnapshots.queryId, queries.id)).where(eq8(querySnapshots.runId, run.id)).all();
|
|
7320
7414
|
return {
|
|
7321
7415
|
...formatRun(run),
|
|
7322
7416
|
snapshots: snapshots.map((s) => {
|
|
@@ -7350,8 +7444,8 @@ function loadRunDetail(app, run) {
|
|
|
7350
7444
|
}
|
|
7351
7445
|
|
|
7352
7446
|
// ../api-routes/src/apply.ts
|
|
7353
|
-
import
|
|
7354
|
-
import { and as and5, eq as
|
|
7447
|
+
import crypto11 from "crypto";
|
|
7448
|
+
import { and as and5, eq as eq9 } from "drizzle-orm";
|
|
7355
7449
|
|
|
7356
7450
|
// ../api-routes/src/schedule-utils.ts
|
|
7357
7451
|
import { CronExpressionParser } from "cron-parser";
|
|
@@ -7454,7 +7548,7 @@ function nextRunFromCron(cronExpr, timezone, from = /* @__PURE__ */ new Date())
|
|
|
7454
7548
|
}
|
|
7455
7549
|
|
|
7456
7550
|
// ../api-routes/src/webhooks.ts
|
|
7457
|
-
import
|
|
7551
|
+
import crypto10 from "crypto";
|
|
7458
7552
|
import dns from "dns/promises";
|
|
7459
7553
|
import http from "http";
|
|
7460
7554
|
import https from "https";
|
|
@@ -7506,7 +7600,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
7506
7600
|
"User-Agent": "Canonry/0.1.0"
|
|
7507
7601
|
};
|
|
7508
7602
|
if (webhookSecret) {
|
|
7509
|
-
headers["X-Canonry-Signature"] = "sha256=" +
|
|
7603
|
+
headers["X-Canonry-Signature"] = "sha256=" + crypto10.createHmac("sha256", webhookSecret).update(body).digest("hex");
|
|
7510
7604
|
}
|
|
7511
7605
|
return await new Promise((resolve) => {
|
|
7512
7606
|
const requestOptions = {
|
|
@@ -7687,7 +7781,7 @@ async function applyRoutes(app, opts) {
|
|
|
7687
7781
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
7688
7782
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7689
7783
|
if (scopedProjectId) {
|
|
7690
|
-
const target = app.db.select({ id: projects.id }).from(projects).where(
|
|
7784
|
+
const target = app.db.select({ id: projects.id }).from(projects).where(eq9(projects.name, name)).get();
|
|
7691
7785
|
if (!target || target.id !== scopedProjectId) {
|
|
7692
7786
|
throw forbidden("This API key is scoped to a single project and cannot apply this config.");
|
|
7693
7787
|
}
|
|
@@ -7696,7 +7790,7 @@ async function applyRoutes(app, opts) {
|
|
|
7696
7790
|
let scheduleAction = null;
|
|
7697
7791
|
let aliasesChanged = false;
|
|
7698
7792
|
app.db.transaction((tx) => {
|
|
7699
|
-
const existing = tx.select().from(projects).where(
|
|
7793
|
+
const existing = tx.select().from(projects).where(eq9(projects.name, name)).get();
|
|
7700
7794
|
const nextAliases = normalizeProjectAliases(config.spec.displayName, config.spec.aliases ?? []);
|
|
7701
7795
|
if (existing) {
|
|
7702
7796
|
const prevAliases = existing.aliases;
|
|
@@ -7719,7 +7813,7 @@ async function applyRoutes(app, opts) {
|
|
|
7719
7813
|
configSource: "config-file",
|
|
7720
7814
|
configRevision: existing.configRevision + 1,
|
|
7721
7815
|
updatedAt: now
|
|
7722
|
-
}).where(
|
|
7816
|
+
}).where(eq9(projects.id, existing.id)).run();
|
|
7723
7817
|
writeAuditLog(tx, {
|
|
7724
7818
|
projectId,
|
|
7725
7819
|
actor: "api",
|
|
@@ -7728,7 +7822,7 @@ async function applyRoutes(app, opts) {
|
|
|
7728
7822
|
entityId: projectId
|
|
7729
7823
|
});
|
|
7730
7824
|
} else {
|
|
7731
|
-
projectId =
|
|
7825
|
+
projectId = crypto11.randomUUID();
|
|
7732
7826
|
tx.insert(projects).values({
|
|
7733
7827
|
id: projectId,
|
|
7734
7828
|
name,
|
|
@@ -7757,16 +7851,7 @@ async function applyRoutes(app, opts) {
|
|
|
7757
7851
|
entityId: projectId
|
|
7758
7852
|
});
|
|
7759
7853
|
}
|
|
7760
|
-
tx
|
|
7761
|
-
for (const q of configQueries) {
|
|
7762
|
-
tx.insert(queries).values({
|
|
7763
|
-
id: crypto10.randomUUID(),
|
|
7764
|
-
projectId,
|
|
7765
|
-
query: q,
|
|
7766
|
-
provenance: "cli",
|
|
7767
|
-
createdAt: now
|
|
7768
|
-
}).run();
|
|
7769
|
-
}
|
|
7854
|
+
replaceProjectQueries(tx, projectId, configQueries, now);
|
|
7770
7855
|
writeAuditLog(tx, {
|
|
7771
7856
|
projectId,
|
|
7772
7857
|
actor: "api",
|
|
@@ -7774,11 +7859,11 @@ async function applyRoutes(app, opts) {
|
|
|
7774
7859
|
entityType: "query",
|
|
7775
7860
|
diff: { queries: configQueries }
|
|
7776
7861
|
});
|
|
7777
|
-
tx.delete(competitors).where(
|
|
7862
|
+
tx.delete(competitors).where(eq9(competitors.projectId, projectId)).run();
|
|
7778
7863
|
const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
|
|
7779
7864
|
for (const domain of normalizedCompetitors) {
|
|
7780
7865
|
tx.insert(competitors).values({
|
|
7781
|
-
id:
|
|
7866
|
+
id: crypto11.randomUUID(),
|
|
7782
7867
|
projectId,
|
|
7783
7868
|
domain,
|
|
7784
7869
|
provenance: "cli",
|
|
@@ -7794,7 +7879,7 @@ async function applyRoutes(app, opts) {
|
|
|
7794
7879
|
});
|
|
7795
7880
|
const AV_KIND = SchedulableRunKinds["answer-visibility"];
|
|
7796
7881
|
if (resolvedSchedule) {
|
|
7797
|
-
const existingSched = tx.select().from(schedules).where(and5(
|
|
7882
|
+
const existingSched = tx.select().from(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).get();
|
|
7798
7883
|
if (existingSched) {
|
|
7799
7884
|
tx.update(schedules).set({
|
|
7800
7885
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -7803,10 +7888,10 @@ async function applyRoutes(app, opts) {
|
|
|
7803
7888
|
providers: config.spec.schedule?.providers ?? [],
|
|
7804
7889
|
enabled: true,
|
|
7805
7890
|
updatedAt: now
|
|
7806
|
-
}).where(
|
|
7891
|
+
}).where(eq9(schedules.id, existingSched.id)).run();
|
|
7807
7892
|
} else {
|
|
7808
7893
|
tx.insert(schedules).values({
|
|
7809
|
-
id:
|
|
7894
|
+
id: crypto11.randomUUID(),
|
|
7810
7895
|
projectId,
|
|
7811
7896
|
kind: AV_KIND,
|
|
7812
7897
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -7820,21 +7905,21 @@ async function applyRoutes(app, opts) {
|
|
|
7820
7905
|
}
|
|
7821
7906
|
scheduleAction = "upsert";
|
|
7822
7907
|
} else if (deleteSchedule) {
|
|
7823
|
-
const existingSched = tx.select().from(schedules).where(and5(
|
|
7908
|
+
const existingSched = tx.select().from(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).get();
|
|
7824
7909
|
if (existingSched) {
|
|
7825
|
-
tx.delete(schedules).where(and5(
|
|
7910
|
+
tx.delete(schedules).where(and5(eq9(schedules.projectId, projectId), eq9(schedules.kind, AV_KIND))).run();
|
|
7826
7911
|
scheduleAction = "delete";
|
|
7827
7912
|
}
|
|
7828
7913
|
}
|
|
7829
7914
|
if (hasNotifications) {
|
|
7830
|
-
tx.delete(notifications).where(
|
|
7915
|
+
tx.delete(notifications).where(eq9(notifications.projectId, projectId)).run();
|
|
7831
7916
|
for (const notif of config.spec.notifications) {
|
|
7832
7917
|
tx.insert(notifications).values({
|
|
7833
|
-
id:
|
|
7918
|
+
id: crypto11.randomUUID(),
|
|
7834
7919
|
projectId,
|
|
7835
7920
|
channel: notif.channel,
|
|
7836
7921
|
config: { url: notif.url, events: notif.events },
|
|
7837
|
-
webhookSecret:
|
|
7922
|
+
webhookSecret: crypto11.randomBytes(32).toString("hex"),
|
|
7838
7923
|
enabled: true,
|
|
7839
7924
|
createdAt: now,
|
|
7840
7925
|
updatedAt: now
|
|
@@ -7861,7 +7946,7 @@ async function applyRoutes(app, opts) {
|
|
|
7861
7946
|
if ("google" in rawSpec && config.spec.google?.gsc?.propertyUrl) {
|
|
7862
7947
|
opts?.onGoogleConnectionPropertyUpdated?.(config.spec.canonicalDomain, "gsc", config.spec.google.gsc.propertyUrl);
|
|
7863
7948
|
}
|
|
7864
|
-
const project = app.db.select().from(projects).where(
|
|
7949
|
+
const project = app.db.select().from(projects).where(eq9(projects.id, projectId)).get();
|
|
7865
7950
|
return reply.status(200).send({
|
|
7866
7951
|
id: project.id,
|
|
7867
7952
|
name: project.name,
|
|
@@ -7906,7 +7991,7 @@ function normalizeCompetitorList2(domains) {
|
|
|
7906
7991
|
}
|
|
7907
7992
|
|
|
7908
7993
|
// ../api-routes/src/history.ts
|
|
7909
|
-
import { and as and6, eq as
|
|
7994
|
+
import { and as and6, eq as eq10, desc as desc2, inArray as inArray3 } from "drizzle-orm";
|
|
7910
7995
|
|
|
7911
7996
|
// ../api-routes/src/notification-redaction.ts
|
|
7912
7997
|
var REDACTED_URL = {
|
|
@@ -7952,19 +8037,19 @@ function redactNotificationDiff(value) {
|
|
|
7952
8037
|
async function historyRoutes(app) {
|
|
7953
8038
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
7954
8039
|
const project = resolveProject(app.db, request.params.name);
|
|
7955
|
-
const rows = app.db.select().from(auditLog).where(
|
|
8040
|
+
const rows = app.db.select().from(auditLog).where(eq10(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
7956
8041
|
return reply.send(rows.map(formatAuditEntry));
|
|
7957
8042
|
});
|
|
7958
8043
|
app.get("/history", async (request, reply) => {
|
|
7959
8044
|
const scopedProjectId = request.apiKey?.projectId;
|
|
7960
|
-
const rows = app.db.select().from(auditLog).where(scopedProjectId ?
|
|
8045
|
+
const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq10(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
|
|
7961
8046
|
return reply.send(rows.map(formatAuditEntry));
|
|
7962
8047
|
});
|
|
7963
8048
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
7964
8049
|
const project = resolveProject(app.db, request.params.name);
|
|
7965
8050
|
const limit = parseInt(request.query.limit ?? "50", 10);
|
|
7966
8051
|
const offset = parseInt(request.query.offset ?? "0", 10);
|
|
7967
|
-
const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and6(
|
|
8052
|
+
const projectRuns = app.db.select({ id: runs.id }).from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).all();
|
|
7968
8053
|
if (projectRuns.length === 0) {
|
|
7969
8054
|
return reply.send({ snapshots: [], total: 0 });
|
|
7970
8055
|
}
|
|
@@ -7983,7 +8068,7 @@ async function historyRoutes(app) {
|
|
|
7983
8068
|
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
7984
8069
|
location: querySnapshots.location,
|
|
7985
8070
|
createdAt: querySnapshots.createdAt
|
|
7986
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8071
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray3(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
|
|
7987
8072
|
const locationFilter = request.query.location;
|
|
7988
8073
|
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
7989
8074
|
const total = filtered.length;
|
|
@@ -8012,13 +8097,13 @@ async function historyRoutes(app) {
|
|
|
8012
8097
|
});
|
|
8013
8098
|
app.get("/projects/:name/timeline", async (request, reply) => {
|
|
8014
8099
|
const project = resolveProject(app.db, request.params.name);
|
|
8015
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
8016
|
-
const projectRuns = app.db.select().from(runs).where(and6(
|
|
8100
|
+
const projectQueries = app.db.select().from(queries).where(eq10(queries.projectId, project.id)).all();
|
|
8101
|
+
const projectRuns = app.db.select().from(runs).where(and6(eq10(runs.projectId, project.id), notProbeRun())).orderBy(runs.createdAt).all();
|
|
8017
8102
|
if (projectRuns.length === 0 || projectQueries.length === 0) {
|
|
8018
8103
|
return reply.send([]);
|
|
8019
8104
|
}
|
|
8020
8105
|
const runIds = new Set(projectRuns.map((r) => r.id));
|
|
8021
|
-
const rawSnapshots = app.db.select().from(querySnapshots).where(
|
|
8106
|
+
const rawSnapshots = app.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, [...runIds])).all();
|
|
8022
8107
|
const timelineLocationFilter = request.query.location;
|
|
8023
8108
|
const filteredSnapshots = timelineLocationFilter !== void 0 ? rawSnapshots.filter((s) => s.location === (timelineLocationFilter || null)) : rawSnapshots;
|
|
8024
8109
|
const allSnapshots = filteredSnapshots.map((snapshot) => ({
|
|
@@ -8139,7 +8224,7 @@ async function historyRoutes(app) {
|
|
|
8139
8224
|
throw validationError("Both run1 and run2 query params are required");
|
|
8140
8225
|
}
|
|
8141
8226
|
const requestedRunIds = [.../* @__PURE__ */ new Set([run1, run2])];
|
|
8142
|
-
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
8227
|
+
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray3(runs.id, requestedRunIds)).all();
|
|
8143
8228
|
const runsById = new Map(runRows.map((row) => [row.id, row]));
|
|
8144
8229
|
for (const runId of requestedRunIds) {
|
|
8145
8230
|
const run = runsById.get(runId);
|
|
@@ -8153,14 +8238,14 @@ async function historyRoutes(app) {
|
|
|
8153
8238
|
citationState: querySnapshots.citationState,
|
|
8154
8239
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8155
8240
|
answerText: querySnapshots.answerText
|
|
8156
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8241
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run1)).all();
|
|
8157
8242
|
const snaps2 = app.db.select({
|
|
8158
8243
|
queryId: querySnapshots.queryId,
|
|
8159
8244
|
query: queries.query,
|
|
8160
8245
|
citationState: querySnapshots.citationState,
|
|
8161
8246
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8162
8247
|
answerText: querySnapshots.answerText
|
|
8163
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8248
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, run2)).all();
|
|
8164
8249
|
const map1 = /* @__PURE__ */ new Map();
|
|
8165
8250
|
for (const s of snaps1) {
|
|
8166
8251
|
const resolved = {
|
|
@@ -8224,13 +8309,13 @@ function formatAuditEntry(row) {
|
|
|
8224
8309
|
}
|
|
8225
8310
|
|
|
8226
8311
|
// ../api-routes/src/analytics.ts
|
|
8227
|
-
import { and as and7, eq as
|
|
8312
|
+
import { and as and7, eq as eq11, desc as desc3, inArray as inArray4 } from "drizzle-orm";
|
|
8228
8313
|
async function analyticsRoutes(app) {
|
|
8229
8314
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
8230
8315
|
const project = resolveProject(app.db, request.params.name);
|
|
8231
8316
|
const window = parseWindow(request.query.window);
|
|
8232
8317
|
const cutoff = windowCutoff(window);
|
|
8233
|
-
const projectRuns = app.db.select().from(runs).where(and7(
|
|
8318
|
+
const projectRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8234
8319
|
if (projectRuns.length === 0) {
|
|
8235
8320
|
return reply.send({
|
|
8236
8321
|
window,
|
|
@@ -8251,14 +8336,14 @@ async function analyticsRoutes(app) {
|
|
|
8251
8336
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8252
8337
|
answerText: querySnapshots.answerText,
|
|
8253
8338
|
createdAt: querySnapshots.createdAt
|
|
8254
|
-
}).from(querySnapshots).where(
|
|
8339
|
+
}).from(querySnapshots).where(inArray4(querySnapshots.runId, runIds)).all());
|
|
8255
8340
|
const allSnapshots = rawSnapshots.map((s) => ({
|
|
8256
8341
|
...s,
|
|
8257
8342
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
8258
8343
|
}));
|
|
8259
|
-
const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(
|
|
8344
|
+
const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(eq11(queries.projectId, project.id)).all();
|
|
8260
8345
|
const queryCreatedAt = new Map(projectQueries.map((q) => [q.id, q.createdAt]));
|
|
8261
|
-
const mentionShareCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
8346
|
+
const mentionShareCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, project.id)).all().map((c) => ({
|
|
8262
8347
|
domain: c.domain,
|
|
8263
8348
|
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
8264
8349
|
}));
|
|
@@ -8282,14 +8367,14 @@ async function analyticsRoutes(app) {
|
|
|
8282
8367
|
const project = resolveProject(app.db, request.params.name);
|
|
8283
8368
|
const window = parseWindow(request.query.window);
|
|
8284
8369
|
const cutoff = windowCutoff(window);
|
|
8285
|
-
const completedRuns = app.db.select().from(runs).where(and7(
|
|
8370
|
+
const completedRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
|
|
8286
8371
|
const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
|
|
8287
8372
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
8288
8373
|
const latestRun = pickGroupRepresentative(latestGroup);
|
|
8289
8374
|
if (!latestRun) {
|
|
8290
8375
|
return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
|
|
8291
8376
|
}
|
|
8292
|
-
const windowRuns = app.db.select().from(runs).where(and7(
|
|
8377
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8293
8378
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
8294
8379
|
const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
|
|
8295
8380
|
const consistencyMap = /* @__PURE__ */ new Map();
|
|
@@ -8300,7 +8385,7 @@ async function analyticsRoutes(app) {
|
|
|
8300
8385
|
citationState: querySnapshots.citationState,
|
|
8301
8386
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8302
8387
|
answerText: querySnapshots.answerText
|
|
8303
|
-
}).from(querySnapshots).where(
|
|
8388
|
+
}).from(querySnapshots).where(inArray4(querySnapshots.runId, windowRunIds)).all());
|
|
8304
8389
|
for (const s of allWindowSnaps) {
|
|
8305
8390
|
const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
|
|
8306
8391
|
let entry = consistencyMap.get(s.queryId);
|
|
@@ -8321,7 +8406,7 @@ async function analyticsRoutes(app) {
|
|
|
8321
8406
|
answerMentioned: querySnapshots.answerMentioned,
|
|
8322
8407
|
answerText: querySnapshots.answerText,
|
|
8323
8408
|
competitorOverlap: querySnapshots.competitorOverlap
|
|
8324
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8409
|
+
}).from(querySnapshots).leftJoin(queries, eq11(querySnapshots.queryId, queries.id)).where(inArray4(querySnapshots.runId, latestGroupRunIds)).all());
|
|
8325
8410
|
const snapshots = rawSnapshots.map((s) => ({
|
|
8326
8411
|
...s,
|
|
8327
8412
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
@@ -8413,14 +8498,14 @@ async function analyticsRoutes(app) {
|
|
|
8413
8498
|
}
|
|
8414
8499
|
const classifyCtx = {
|
|
8415
8500
|
projectDomains: effectiveDomains(project),
|
|
8416
|
-
competitorDomains: app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
8501
|
+
competitorDomains: app.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, project.id)).all().map((r) => r.domain)
|
|
8417
8502
|
};
|
|
8418
8503
|
const storedSurfaceClasses = /* @__PURE__ */ new Map();
|
|
8419
|
-
for (const row of app.db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(
|
|
8504
|
+
for (const row of app.db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq11(domainClassifications.projectId, project.id)).all()) {
|
|
8420
8505
|
const mapped = surfaceClassFromCompetitorType(row.competitorType);
|
|
8421
8506
|
if (mapped) storedSurfaceClasses.set(normalizeProjectDomain(row.domain), mapped);
|
|
8422
8507
|
}
|
|
8423
|
-
const windowRuns = app.db.select().from(runs).where(and7(
|
|
8508
|
+
const windowRuns = app.db.select().from(runs).where(and7(eq11(runs.projectId, project.id), eq11(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
8424
8509
|
if (windowRuns.length === 0) {
|
|
8425
8510
|
return reply.send({
|
|
8426
8511
|
overall: [],
|
|
@@ -8440,7 +8525,7 @@ async function analyticsRoutes(app) {
|
|
|
8440
8525
|
query: queries.query,
|
|
8441
8526
|
provider: querySnapshots.provider,
|
|
8442
8527
|
rawResponse: querySnapshots.rawResponse
|
|
8443
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
8528
|
+
}).from(querySnapshots).leftJoin(queries, eq11(querySnapshots.queryId, queries.id)).where(inArray4(querySnapshots.runId, windowRunIds)).all();
|
|
8444
8529
|
const overallCounts = /* @__PURE__ */ new Map();
|
|
8445
8530
|
const byQuery = {};
|
|
8446
8531
|
const overallDomains = /* @__PURE__ */ new Map();
|
|
@@ -8541,7 +8626,7 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt, ment
|
|
|
8541
8626
|
});
|
|
8542
8627
|
if (eligible.length > 0) usable = eligible;
|
|
8543
8628
|
}
|
|
8544
|
-
const
|
|
8629
|
+
const metric2 = computeProviderMetric(usable);
|
|
8545
8630
|
const queryCount = new Set(usable.map((s) => s.queryId)).size;
|
|
8546
8631
|
const byProvider = {};
|
|
8547
8632
|
for (const provider of new Set(usable.map((s) => s.provider))) {
|
|
@@ -8550,12 +8635,12 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt, ment
|
|
|
8550
8635
|
buckets.push({
|
|
8551
8636
|
startDate: startISO,
|
|
8552
8637
|
endDate: endISO,
|
|
8553
|
-
citationRate:
|
|
8554
|
-
cited:
|
|
8555
|
-
total:
|
|
8638
|
+
citationRate: metric2.citationRate,
|
|
8639
|
+
cited: metric2.cited,
|
|
8640
|
+
total: metric2.total,
|
|
8556
8641
|
queryCount,
|
|
8557
|
-
mentionRate:
|
|
8558
|
-
mentionedCount:
|
|
8642
|
+
mentionRate: metric2.mentionRate,
|
|
8643
|
+
mentionedCount: metric2.mentionedCount,
|
|
8559
8644
|
mentionShare: computeMentionShareBucketMetric(usable, mentionShareCompetitors),
|
|
8560
8645
|
byProvider
|
|
8561
8646
|
});
|
|
@@ -8685,7 +8770,7 @@ function buildCategoryCounts(counts2) {
|
|
|
8685
8770
|
}
|
|
8686
8771
|
|
|
8687
8772
|
// ../api-routes/src/intelligence.ts
|
|
8688
|
-
import { eq as
|
|
8773
|
+
import { eq as eq12, desc as desc4, and as and8, inArray as inArray5, like } from "drizzle-orm";
|
|
8689
8774
|
var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
8690
8775
|
function severitiesAtOrAbove(min) {
|
|
8691
8776
|
const floor = SEVERITY_RANK[min];
|
|
@@ -8803,19 +8888,19 @@ function aggregateHealthSnapshots(projectId, rows) {
|
|
|
8803
8888
|
async function intelligenceRoutes(app) {
|
|
8804
8889
|
app.get("/projects/:name/insights", async (request, reply) => {
|
|
8805
8890
|
const project = resolveProject(app.db, request.params.name);
|
|
8806
|
-
const conditions = [
|
|
8891
|
+
const conditions = [eq12(insights.projectId, project.id)];
|
|
8807
8892
|
if (request.query.runId) {
|
|
8808
|
-
conditions.push(
|
|
8893
|
+
conditions.push(eq12(insights.runId, request.query.runId));
|
|
8809
8894
|
}
|
|
8810
8895
|
const typeFilter = request.query.type?.trim();
|
|
8811
8896
|
if (typeFilter) {
|
|
8812
8897
|
conditions.push(
|
|
8813
|
-
typeFilter.endsWith("*") ? like(insights.type, `${typeFilter.slice(0, -1)}%`) :
|
|
8898
|
+
typeFilter.endsWith("*") ? like(insights.type, `${typeFilter.slice(0, -1)}%`) : eq12(insights.type, typeFilter)
|
|
8814
8899
|
);
|
|
8815
8900
|
}
|
|
8816
8901
|
const severityFilter = request.query.severity?.trim();
|
|
8817
8902
|
if (severityFilter) {
|
|
8818
|
-
conditions.push(
|
|
8903
|
+
conditions.push(inArray5(insights.severity, severitiesAtOrAbove(severityFilter)));
|
|
8819
8904
|
}
|
|
8820
8905
|
let limit;
|
|
8821
8906
|
if (request.query.limit !== void 0) {
|
|
@@ -8833,7 +8918,7 @@ async function intelligenceRoutes(app) {
|
|
|
8833
8918
|
});
|
|
8834
8919
|
app.get("/projects/:name/insights/:id", async (request, reply) => {
|
|
8835
8920
|
const project = resolveProject(app.db, request.params.name);
|
|
8836
|
-
const row = app.db.select().from(insights).where(
|
|
8921
|
+
const row = app.db.select().from(insights).where(eq12(insights.id, request.params.id)).get();
|
|
8837
8922
|
if (!row || row.projectId !== project.id) {
|
|
8838
8923
|
throw notFound("Insight", request.params.id);
|
|
8839
8924
|
}
|
|
@@ -8841,19 +8926,19 @@ async function intelligenceRoutes(app) {
|
|
|
8841
8926
|
});
|
|
8842
8927
|
app.post("/projects/:name/insights/:id/dismiss", async (request, reply) => {
|
|
8843
8928
|
const project = resolveProject(app.db, request.params.name);
|
|
8844
|
-
const row = app.db.select().from(insights).where(
|
|
8929
|
+
const row = app.db.select().from(insights).where(eq12(insights.id, request.params.id)).get();
|
|
8845
8930
|
if (!row || row.projectId !== project.id) {
|
|
8846
8931
|
throw notFound("Insight", request.params.id);
|
|
8847
8932
|
}
|
|
8848
|
-
app.db.update(insights).set({ dismissed: true }).where(
|
|
8933
|
+
app.db.update(insights).set({ dismissed: true }).where(eq12(insights.id, request.params.id)).run();
|
|
8849
8934
|
return reply.send({ ok: true });
|
|
8850
8935
|
});
|
|
8851
8936
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
8852
8937
|
const project = resolveProject(app.db, request.params.name);
|
|
8853
8938
|
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and8(
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8939
|
+
eq12(runs.projectId, project.id),
|
|
8940
|
+
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
8941
|
+
inArray5(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
8857
8942
|
// Health-latest is the dashboard headline; probe runs must not
|
|
8858
8943
|
// displace the most recent real visibility sweep.
|
|
8859
8944
|
notProbeRun()
|
|
@@ -8862,14 +8947,14 @@ async function intelligenceRoutes(app) {
|
|
|
8862
8947
|
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
8863
8948
|
if (latestGroupRunIds.length > 0) {
|
|
8864
8949
|
const groupRows = app.db.select().from(healthSnapshots).where(and8(
|
|
8865
|
-
|
|
8866
|
-
|
|
8950
|
+
eq12(healthSnapshots.projectId, project.id),
|
|
8951
|
+
inArray5(healthSnapshots.runId, latestGroupRunIds)
|
|
8867
8952
|
)).all();
|
|
8868
8953
|
if (groupRows.length > 0) {
|
|
8869
8954
|
return reply.send(aggregateHealthSnapshots(project.id, groupRows));
|
|
8870
8955
|
}
|
|
8871
8956
|
}
|
|
8872
|
-
const fallback = app.db.select().from(healthSnapshots).where(
|
|
8957
|
+
const fallback = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
8873
8958
|
if (!fallback) {
|
|
8874
8959
|
return reply.send(emptyHealthSnapshot(project.id));
|
|
8875
8960
|
}
|
|
@@ -8879,20 +8964,20 @@ async function intelligenceRoutes(app) {
|
|
|
8879
8964
|
const project = resolveProject(app.db, request.params.name);
|
|
8880
8965
|
const parsed = request.query.limit ? parseInt(request.query.limit, 10) : NaN;
|
|
8881
8966
|
const limit = Number.isNaN(parsed) ? 30 : Math.min(Math.max(parsed, 1), 100);
|
|
8882
|
-
const rows = app.db.select().from(healthSnapshots).where(
|
|
8967
|
+
const rows = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(limit).all();
|
|
8883
8968
|
return reply.send(rows.map(mapHealthRow));
|
|
8884
8969
|
});
|
|
8885
8970
|
}
|
|
8886
8971
|
|
|
8887
8972
|
// ../api-routes/src/report.ts
|
|
8888
|
-
import { and as and12, desc as desc7, eq as
|
|
8973
|
+
import { and as and12, desc as desc7, eq as eq16, gte as gte2, inArray as inArray7, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
|
|
8889
8974
|
|
|
8890
8975
|
// ../api-routes/src/content.ts
|
|
8891
|
-
import
|
|
8892
|
-
import { and as and10, desc as desc6, eq as
|
|
8976
|
+
import crypto12 from "crypto";
|
|
8977
|
+
import { and as and10, desc as desc6, eq as eq14 } from "drizzle-orm";
|
|
8893
8978
|
|
|
8894
8979
|
// ../api-routes/src/content-data.ts
|
|
8895
|
-
import { and as and9, eq as
|
|
8980
|
+
import { and as and9, eq as eq13, desc as desc5, inArray as inArray6 } from "drizzle-orm";
|
|
8896
8981
|
var RECENT_RUNS_WINDOW = 5;
|
|
8897
8982
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
8898
8983
|
const projectId = project.id;
|
|
@@ -8941,7 +9026,7 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
|
8941
9026
|
};
|
|
8942
9027
|
}
|
|
8943
9028
|
function loadDomainClasses(db, projectId) {
|
|
8944
|
-
const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(
|
|
9029
|
+
const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq13(domainClassifications.projectId, projectId)).all();
|
|
8945
9030
|
return new Map(rows.map((r) => [normalizeDomain(r.domain), r.competitorType]));
|
|
8946
9031
|
}
|
|
8947
9032
|
function buildQueryIntentModifiers(project, locationFilter) {
|
|
@@ -9013,22 +9098,22 @@ var US_REGION_ABBREVIATIONS = {
|
|
|
9013
9098
|
wyoming: "wy"
|
|
9014
9099
|
};
|
|
9015
9100
|
function listQueries(db, projectId) {
|
|
9016
|
-
const rows = db.select({ text: queries.query }).from(queries).where(
|
|
9101
|
+
const rows = db.select({ text: queries.query }).from(queries).where(eq13(queries.projectId, projectId)).all();
|
|
9017
9102
|
return rows.map((r) => r.text);
|
|
9018
9103
|
}
|
|
9019
9104
|
function listCompetitorDomains(db, projectId) {
|
|
9020
|
-
const rows = db.select({ domain: competitors.domain }).from(competitors).where(
|
|
9105
|
+
const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
|
|
9021
9106
|
return rows.map((r) => r.domain);
|
|
9022
9107
|
}
|
|
9023
9108
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
|
|
9024
9109
|
const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
9025
9110
|
and9(
|
|
9026
|
-
|
|
9027
|
-
|
|
9111
|
+
eq13(runs.projectId, projectId),
|
|
9112
|
+
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
9028
9113
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
9029
9114
|
// snapshots; including them risks pointing latestRunId at a run with
|
|
9030
9115
|
// no usable evidence.
|
|
9031
|
-
|
|
9116
|
+
inArray6(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
9032
9117
|
// Probe runs are operator/agent test runs; they must not poison the
|
|
9033
9118
|
// recent-runs window the content engine uses to recommend actions.
|
|
9034
9119
|
notProbeRun()
|
|
@@ -9038,22 +9123,22 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
|
|
|
9038
9123
|
return filtered.slice(0, limit).map((r) => r.id);
|
|
9039
9124
|
}
|
|
9040
9125
|
function lookupRunTimestamp(db, runId) {
|
|
9041
|
-
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(
|
|
9126
|
+
const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
|
|
9042
9127
|
return row?.createdAt ?? "";
|
|
9043
9128
|
}
|
|
9044
9129
|
function listGscPagesForProject(db, projectId) {
|
|
9045
|
-
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(
|
|
9130
|
+
const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
9046
9131
|
return rows.map((r) => r.page);
|
|
9047
9132
|
}
|
|
9048
9133
|
function listGa4LandingPagesForProject(db, projectId) {
|
|
9049
|
-
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(
|
|
9134
|
+
const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
9050
9135
|
return rows.map((r) => r.landingPage);
|
|
9051
9136
|
}
|
|
9052
9137
|
function buildGaTrafficByPage(db, projectId) {
|
|
9053
9138
|
const rows = db.select({
|
|
9054
9139
|
landingPage: gaTrafficSnapshots.landingPage,
|
|
9055
9140
|
sessions: gaTrafficSnapshots.sessions
|
|
9056
|
-
}).from(gaTrafficSnapshots).where(
|
|
9141
|
+
}).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
|
|
9057
9142
|
const map = /* @__PURE__ */ new Map();
|
|
9058
9143
|
for (const row of rows) {
|
|
9059
9144
|
const path8 = extractPath2(row.landingPage);
|
|
@@ -9063,24 +9148,24 @@ function buildGaTrafficByPage(db, projectId) {
|
|
|
9063
9148
|
return map;
|
|
9064
9149
|
}
|
|
9065
9150
|
function sumAiReferralSessions(db, projectId) {
|
|
9066
|
-
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(
|
|
9151
|
+
const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
|
|
9067
9152
|
return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
|
|
9068
9153
|
}
|
|
9069
9154
|
function buildCandidateQueries(opts) {
|
|
9070
9155
|
if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
|
|
9071
9156
|
return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
|
|
9072
9157
|
}
|
|
9073
|
-
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(
|
|
9158
|
+
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq13(queries.projectId, opts.projectId)).all();
|
|
9074
9159
|
const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
|
|
9075
9160
|
const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
|
|
9076
|
-
const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(
|
|
9161
|
+
const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray6(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
|
|
9077
9162
|
const snapshotsByQuery = /* @__PURE__ */ new Map();
|
|
9078
9163
|
for (const row of snapshotRows) {
|
|
9079
9164
|
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
9080
9165
|
list.push(row);
|
|
9081
9166
|
snapshotsByQuery.set(row.queryId, list);
|
|
9082
9167
|
}
|
|
9083
|
-
const gscRows = opts.db.select().from(gscSearchData).where(
|
|
9168
|
+
const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
|
|
9084
9169
|
const gscByQuery = aggregateGscByQuery(gscRows);
|
|
9085
9170
|
return opts.candidateQueryStrings.map((query) => {
|
|
9086
9171
|
const queryId = queryIdByText.get(query);
|
|
@@ -9269,7 +9354,7 @@ function extractPath2(url) {
|
|
|
9269
9354
|
|
|
9270
9355
|
// ../api-routes/src/content.ts
|
|
9271
9356
|
function loadDismissedTargetRefs(db, projectId) {
|
|
9272
|
-
const rows = db.select({ targetRef: contentTargetDismissals.targetRef }).from(contentTargetDismissals).where(
|
|
9357
|
+
const rows = db.select({ targetRef: contentTargetDismissals.targetRef }).from(contentTargetDismissals).where(eq14(contentTargetDismissals.projectId, projectId)).all();
|
|
9273
9358
|
return new Set(rows.map((r) => r.targetRef));
|
|
9274
9359
|
}
|
|
9275
9360
|
function formatDismissalRow(row) {
|
|
@@ -9364,7 +9449,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9364
9449
|
});
|
|
9365
9450
|
app.get("/projects/:name/content/dismissals", async (request) => {
|
|
9366
9451
|
const project = resolveProject(app.db, request.params.name);
|
|
9367
|
-
const rows = app.db.select().from(contentTargetDismissals).where(
|
|
9452
|
+
const rows = app.db.select().from(contentTargetDismissals).where(eq14(contentTargetDismissals.projectId, project.id)).orderBy(contentTargetDismissals.dismissedAt).all();
|
|
9368
9453
|
const response = {
|
|
9369
9454
|
dismissals: rows.map(formatDismissalRow)
|
|
9370
9455
|
};
|
|
@@ -9379,7 +9464,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9379
9464
|
const { targetRef, addressedUrl, note } = parsed.data;
|
|
9380
9465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9381
9466
|
app.db.insert(contentTargetDismissals).values({
|
|
9382
|
-
id:
|
|
9467
|
+
id: crypto12.randomUUID(),
|
|
9383
9468
|
projectId: project.id,
|
|
9384
9469
|
targetRef,
|
|
9385
9470
|
addressedUrl: addressedUrl ?? null,
|
|
@@ -9394,8 +9479,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9394
9479
|
}
|
|
9395
9480
|
}).run();
|
|
9396
9481
|
const row = app.db.select().from(contentTargetDismissals).where(and10(
|
|
9397
|
-
|
|
9398
|
-
|
|
9482
|
+
eq14(contentTargetDismissals.projectId, project.id),
|
|
9483
|
+
eq14(contentTargetDismissals.targetRef, targetRef)
|
|
9399
9484
|
)).get();
|
|
9400
9485
|
if (!row) throw notFound("contentTargetDismissal", targetRef);
|
|
9401
9486
|
return formatDismissalRow(row);
|
|
@@ -9404,8 +9489,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9404
9489
|
const project = resolveProject(app.db, request.params.name);
|
|
9405
9490
|
const { targetRef } = request.params;
|
|
9406
9491
|
const result = app.db.delete(contentTargetDismissals).where(and10(
|
|
9407
|
-
|
|
9408
|
-
|
|
9492
|
+
eq14(contentTargetDismissals.projectId, project.id),
|
|
9493
|
+
eq14(contentTargetDismissals.targetRef, targetRef)
|
|
9409
9494
|
)).run();
|
|
9410
9495
|
if (result.changes === 0) {
|
|
9411
9496
|
throw notFound("contentTargetDismissal", targetRef);
|
|
@@ -9415,8 +9500,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9415
9500
|
app.get("/projects/:name/content/recommendations/:targetRef/analysis", async (request, reply) => {
|
|
9416
9501
|
const project = resolveProject(app.db, request.params.name);
|
|
9417
9502
|
const row = app.db.select().from(recommendationExplanations).where(and10(
|
|
9418
|
-
|
|
9419
|
-
|
|
9503
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9504
|
+
eq14(recommendationExplanations.targetRef, request.params.targetRef)
|
|
9420
9505
|
)).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
|
|
9421
9506
|
if (!row) {
|
|
9422
9507
|
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "No cached explanation for this targetRef." } });
|
|
@@ -9443,8 +9528,8 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9443
9528
|
}
|
|
9444
9529
|
if (!body.forceRefresh) {
|
|
9445
9530
|
const cached = app.db.select().from(recommendationExplanations).where(and10(
|
|
9446
|
-
|
|
9447
|
-
|
|
9531
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9532
|
+
eq14(recommendationExplanations.targetRef, targetRef)
|
|
9448
9533
|
)).orderBy(desc6(recommendationExplanations.generatedAt)).limit(1).get();
|
|
9449
9534
|
if (cached) return reply.send(formatExplanationRow(cached));
|
|
9450
9535
|
}
|
|
@@ -9458,7 +9543,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9458
9543
|
});
|
|
9459
9544
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9460
9545
|
app.db.insert(recommendationExplanations).values({
|
|
9461
|
-
id:
|
|
9546
|
+
id: crypto12.randomUUID(),
|
|
9462
9547
|
projectId: project.id,
|
|
9463
9548
|
targetRef,
|
|
9464
9549
|
promptVersion: result.promptVersion,
|
|
@@ -9482,9 +9567,9 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9482
9567
|
}
|
|
9483
9568
|
}).run();
|
|
9484
9569
|
const row = app.db.select().from(recommendationExplanations).where(and10(
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9570
|
+
eq14(recommendationExplanations.projectId, project.id),
|
|
9571
|
+
eq14(recommendationExplanations.targetRef, targetRef),
|
|
9572
|
+
eq14(recommendationExplanations.promptVersion, result.promptVersion)
|
|
9488
9573
|
)).get();
|
|
9489
9574
|
if (!row) throw notFound("recommendationExplanation", targetRef);
|
|
9490
9575
|
return reply.send(formatExplanationRow(row));
|
|
@@ -9542,7 +9627,7 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9542
9627
|
});
|
|
9543
9628
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9544
9629
|
app.db.insert(recommendationBriefs).values({
|
|
9545
|
-
id:
|
|
9630
|
+
id: crypto12.randomUUID(),
|
|
9546
9631
|
projectId: project.id,
|
|
9547
9632
|
targetRef,
|
|
9548
9633
|
promptVersion: result.promptVersion,
|
|
@@ -9566,16 +9651,16 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9566
9651
|
}
|
|
9567
9652
|
}).run();
|
|
9568
9653
|
const row = app.db.select().from(recommendationBriefs).where(and10(
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9654
|
+
eq14(recommendationBriefs.projectId, project.id),
|
|
9655
|
+
eq14(recommendationBriefs.targetRef, targetRef),
|
|
9656
|
+
eq14(recommendationBriefs.promptVersion, result.promptVersion)
|
|
9572
9657
|
)).get();
|
|
9573
9658
|
if (!row) throw notFound("recommendationBrief", targetRef);
|
|
9574
9659
|
return reply.send(formatBriefRow(row));
|
|
9575
9660
|
});
|
|
9576
9661
|
app.get("/projects/:name/content/domain-classifications", async (request) => {
|
|
9577
9662
|
const project = resolveProject(app.db, request.params.name);
|
|
9578
|
-
const rows = app.db.select().from(domainClassifications).where(
|
|
9663
|
+
const rows = app.db.select().from(domainClassifications).where(eq14(domainClassifications.projectId, project.id)).orderBy(desc6(domainClassifications.hits)).all();
|
|
9579
9664
|
const response = {
|
|
9580
9665
|
classifications: rows.map((r) => ({
|
|
9581
9666
|
domain: r.domain,
|
|
@@ -9589,11 +9674,11 @@ async function contentRoutes(app, opts = {}) {
|
|
|
9589
9674
|
}
|
|
9590
9675
|
function lookupCachedBrief(db, projectId, targetRef, promptVersion) {
|
|
9591
9676
|
const conditions = [
|
|
9592
|
-
|
|
9593
|
-
|
|
9677
|
+
eq14(recommendationBriefs.projectId, projectId),
|
|
9678
|
+
eq14(recommendationBriefs.targetRef, targetRef)
|
|
9594
9679
|
];
|
|
9595
9680
|
if (promptVersion !== void 0) {
|
|
9596
|
-
conditions.push(
|
|
9681
|
+
conditions.push(eq14(recommendationBriefs.promptVersion, promptVersion));
|
|
9597
9682
|
}
|
|
9598
9683
|
return db.select().from(recommendationBriefs).where(and10(...conditions)).orderBy(desc6(recommendationBriefs.generatedAt)).limit(1).get();
|
|
9599
9684
|
}
|
|
@@ -9621,7 +9706,7 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9621
9706
|
}
|
|
9622
9707
|
|
|
9623
9708
|
// ../api-routes/src/gsc-totals.ts
|
|
9624
|
-
import { and as and11, asc as asc2, eq as
|
|
9709
|
+
import { and as and11, asc as asc2, eq as eq15, sql as sql6 } from "drizzle-orm";
|
|
9625
9710
|
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9626
9711
|
const rows = db.select({
|
|
9627
9712
|
date: gscDailyTotals.date,
|
|
@@ -9630,7 +9715,7 @@ function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
|
9630
9715
|
position: gscDailyTotals.position
|
|
9631
9716
|
}).from(gscDailyTotals).where(
|
|
9632
9717
|
and11(
|
|
9633
|
-
|
|
9718
|
+
eq15(gscDailyTotals.projectId, projectId),
|
|
9634
9719
|
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9635
9720
|
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9636
9721
|
)
|
|
@@ -11950,7 +12035,7 @@ function loadSnapshotsForRun(db, runId) {
|
|
|
11950
12035
|
}
|
|
11951
12036
|
function loadSnapshotsForRunIds(db, runIds) {
|
|
11952
12037
|
if (runIds.length === 0) return [];
|
|
11953
|
-
const rows = db.select().from(querySnapshots).where(
|
|
12038
|
+
const rows = db.select().from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
|
|
11954
12039
|
return rows.filter((r) => r.queryId !== null).map((r) => ({
|
|
11955
12040
|
id: r.id,
|
|
11956
12041
|
runId: r.runId,
|
|
@@ -11967,13 +12052,13 @@ function loadSnapshotsForRunIds(db, runIds) {
|
|
|
11967
12052
|
}));
|
|
11968
12053
|
}
|
|
11969
12054
|
function loadQueryLookup(db, projectId) {
|
|
11970
|
-
const rows = db.select().from(queries).where(
|
|
12055
|
+
const rows = db.select().from(queries).where(eq16(queries.projectId, projectId)).all();
|
|
11971
12056
|
const byId = /* @__PURE__ */ new Map();
|
|
11972
12057
|
for (const row of rows) byId.set(row.id, row.query);
|
|
11973
12058
|
return { byId };
|
|
11974
12059
|
}
|
|
11975
12060
|
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
|
|
11976
|
-
const allRows = db.select().from(gscSearchData).where(
|
|
12061
|
+
const allRows = db.select().from(gscSearchData).where(eq16(gscSearchData.projectId, projectId)).all();
|
|
11977
12062
|
const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
|
|
11978
12063
|
let maxDate = "";
|
|
11979
12064
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
@@ -12065,12 +12150,12 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12065
12150
|
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
|
|
12066
12151
|
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
12067
12152
|
and12(
|
|
12068
|
-
|
|
12069
|
-
|
|
12153
|
+
eq16(gaTrafficWindowSummaries.projectId, projectId),
|
|
12154
|
+
eq16(gaTrafficWindowSummaries.windowKey, gaWindowKey)
|
|
12070
12155
|
)
|
|
12071
12156
|
).limit(1).get() : void 0;
|
|
12072
|
-
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(
|
|
12073
|
-
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(
|
|
12157
|
+
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
12158
|
+
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, projectId)).all();
|
|
12074
12159
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
12075
12160
|
let snapshotMaxDate = "";
|
|
12076
12161
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
@@ -12097,8 +12182,8 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12097
12182
|
organicSessions: data.organic
|
|
12098
12183
|
})).sort((a, b) => b.sessions - a.sessions).slice(0, TOP_LANDING_PAGES_LIMIT);
|
|
12099
12184
|
const aiConditions = [
|
|
12100
|
-
|
|
12101
|
-
|
|
12185
|
+
eq16(gaAiReferrals.projectId, projectId),
|
|
12186
|
+
eq16(gaAiReferrals.sourceDimension, "session")
|
|
12102
12187
|
];
|
|
12103
12188
|
if (snapshotStartDate && snapshotMaxDate) {
|
|
12104
12189
|
aiConditions.push(gte2(gaAiReferrals.date, snapshotStartDate));
|
|
@@ -12155,7 +12240,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
12155
12240
|
};
|
|
12156
12241
|
}
|
|
12157
12242
|
function buildSocialReferrals(db, projectId) {
|
|
12158
|
-
const rows = db.select().from(gaSocialReferrals).where(
|
|
12243
|
+
const rows = db.select().from(gaSocialReferrals).where(eq16(gaSocialReferrals.projectId, projectId)).all();
|
|
12159
12244
|
if (rows.length === 0) return null;
|
|
12160
12245
|
let total = 0;
|
|
12161
12246
|
let organic = 0;
|
|
@@ -12187,7 +12272,7 @@ function buildSocialReferrals(db, projectId) {
|
|
|
12187
12272
|
};
|
|
12188
12273
|
}
|
|
12189
12274
|
function buildAiReferrals(db, projectId) {
|
|
12190
|
-
const rows = db.select().from(gaAiReferrals).where(
|
|
12275
|
+
const rows = db.select().from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, projectId)).all();
|
|
12191
12276
|
if (rows.length === 0) return null;
|
|
12192
12277
|
const dimSessionsByTuple = /* @__PURE__ */ new Map();
|
|
12193
12278
|
for (const r of rows) {
|
|
@@ -12300,7 +12385,7 @@ function nonSubresourceReferralPathCondition() {
|
|
|
12300
12385
|
function buildServerActivity(db, projectId, windowDays) {
|
|
12301
12386
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
12302
12387
|
and12(
|
|
12303
|
-
|
|
12388
|
+
eq16(trafficSources.projectId, projectId),
|
|
12304
12389
|
ne2(trafficSources.status, TrafficSourceStatuses.archived)
|
|
12305
12390
|
)
|
|
12306
12391
|
).all();
|
|
@@ -12316,8 +12401,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12316
12401
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12317
12402
|
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
12318
12403
|
and12(
|
|
12319
|
-
|
|
12320
|
-
|
|
12404
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12405
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12321
12406
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12322
12407
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
12323
12408
|
)
|
|
@@ -12326,7 +12411,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12326
12411
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12327
12412
|
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
12328
12413
|
and12(
|
|
12329
|
-
|
|
12414
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12330
12415
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12331
12416
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
12332
12417
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
@@ -12340,7 +12425,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12340
12425
|
organic: sql7`COALESCE(SUM(${aiReferralEventsHourly.organicSessionsOrHits}), 0)`
|
|
12341
12426
|
}).from(aiReferralEventsHourly).where(
|
|
12342
12427
|
and12(
|
|
12343
|
-
|
|
12428
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12344
12429
|
nonSubresourceReferralPathCondition(),
|
|
12345
12430
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
12346
12431
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
@@ -12351,7 +12436,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12351
12436
|
const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
12352
12437
|
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
12353
12438
|
and12(
|
|
12354
|
-
|
|
12439
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12355
12440
|
gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
12356
12441
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
12357
12442
|
)
|
|
@@ -12371,7 +12456,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12371
12456
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12372
12457
|
}).from(crawlerEventsHourly).where(
|
|
12373
12458
|
and12(
|
|
12374
|
-
|
|
12459
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12375
12460
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
12376
12461
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12377
12462
|
)
|
|
@@ -12381,8 +12466,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12381
12466
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12382
12467
|
}).from(crawlerEventsHourly).where(
|
|
12383
12468
|
and12(
|
|
12384
|
-
|
|
12385
|
-
|
|
12469
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12470
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12386
12471
|
gte2(crawlerEventsHourly.tsHour, priorStart),
|
|
12387
12472
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
12388
12473
|
)
|
|
@@ -12392,7 +12477,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12392
12477
|
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
12393
12478
|
}).from(aiReferralEventsHourly).where(
|
|
12394
12479
|
and12(
|
|
12395
|
-
|
|
12480
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12396
12481
|
nonSubresourceReferralPathCondition(),
|
|
12397
12482
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12398
12483
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12403,7 +12488,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12403
12488
|
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
12404
12489
|
}).from(aiUserFetchEventsHourly).where(
|
|
12405
12490
|
and12(
|
|
12406
|
-
|
|
12491
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12407
12492
|
gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
12408
12493
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12409
12494
|
)
|
|
@@ -12447,8 +12532,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12447
12532
|
operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
12448
12533
|
}).from(crawlerEventsHourly).where(
|
|
12449
12534
|
and12(
|
|
12450
|
-
|
|
12451
|
-
|
|
12535
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12536
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12452
12537
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
12453
12538
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12454
12539
|
)
|
|
@@ -12464,7 +12549,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12464
12549
|
landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
12465
12550
|
}).from(aiReferralEventsHourly).where(
|
|
12466
12551
|
and12(
|
|
12467
|
-
|
|
12552
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12468
12553
|
nonSubresourceReferralPathCondition(),
|
|
12469
12554
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12470
12555
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12481,7 +12566,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12481
12566
|
products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
12482
12567
|
}).from(aiReferralEventsHourly).where(
|
|
12483
12568
|
and12(
|
|
12484
|
-
|
|
12569
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12485
12570
|
nonSubresourceReferralPathCondition(),
|
|
12486
12571
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
12487
12572
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12497,8 +12582,8 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12497
12582
|
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
12498
12583
|
}).from(crawlerEventsHourly).where(
|
|
12499
12584
|
and12(
|
|
12500
|
-
|
|
12501
|
-
|
|
12585
|
+
eq16(crawlerEventsHourly.projectId, projectId),
|
|
12586
|
+
eq16(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
12502
12587
|
gte2(crawlerEventsHourly.tsHour, trendStart),
|
|
12503
12588
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
12504
12589
|
)
|
|
@@ -12508,7 +12593,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12508
12593
|
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
12509
12594
|
}).from(aiReferralEventsHourly).where(
|
|
12510
12595
|
and12(
|
|
12511
|
-
|
|
12596
|
+
eq16(aiReferralEventsHourly.projectId, projectId),
|
|
12512
12597
|
nonSubresourceReferralPathCondition(),
|
|
12513
12598
|
gte2(aiReferralEventsHourly.tsHour, trendStart),
|
|
12514
12599
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -12519,7 +12604,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12519
12604
|
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
12520
12605
|
}).from(aiUserFetchEventsHourly).where(
|
|
12521
12606
|
and12(
|
|
12522
|
-
|
|
12607
|
+
eq16(aiUserFetchEventsHourly.projectId, projectId),
|
|
12523
12608
|
gte2(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
12524
12609
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
12525
12610
|
)
|
|
@@ -12592,7 +12677,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
12592
12677
|
};
|
|
12593
12678
|
}
|
|
12594
12679
|
function buildIndexingHealth(db, projectId) {
|
|
12595
|
-
const gsc = db.select().from(gscCoverageSnapshots).where(
|
|
12680
|
+
const gsc = db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, projectId)).orderBy(desc7(gscCoverageSnapshots.date)).limit(1).get();
|
|
12596
12681
|
if (gsc) {
|
|
12597
12682
|
const total = gsc.indexed + gsc.notIndexed;
|
|
12598
12683
|
return {
|
|
@@ -12605,7 +12690,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12605
12690
|
indexedPct: total > 0 ? Math.round(gsc.indexed / total * 100) : 0
|
|
12606
12691
|
};
|
|
12607
12692
|
}
|
|
12608
|
-
const bing = db.select().from(bingCoverageSnapshots).where(
|
|
12693
|
+
const bing = db.select().from(bingCoverageSnapshots).where(eq16(bingCoverageSnapshots.projectId, projectId)).orderBy(desc7(bingCoverageSnapshots.date)).limit(1).get();
|
|
12609
12694
|
if (bing) {
|
|
12610
12695
|
const total = bing.indexed + bing.notIndexed + bing.unknown;
|
|
12611
12696
|
return {
|
|
@@ -12621,7 +12706,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
12621
12706
|
return null;
|
|
12622
12707
|
}
|
|
12623
12708
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
12624
|
-
const visibilityRuns = db.select().from(runs).where(and12(
|
|
12709
|
+
const visibilityRuns = db.select().from(runs).where(and12(eq16(runs.projectId, projectId), eq16(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
12625
12710
|
const totalQueries = queryLookup.byId.size;
|
|
12626
12711
|
const points = [];
|
|
12627
12712
|
for (const run of visibilityRuns) {
|
|
@@ -12670,14 +12755,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
12670
12755
|
function buildInsightList(db, projectId, locationFilter) {
|
|
12671
12756
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
12672
12757
|
and12(
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
or3(
|
|
12758
|
+
eq16(runs.projectId, projectId),
|
|
12759
|
+
eq16(runs.kind, RunKinds["answer-visibility"]),
|
|
12760
|
+
or3(eq16(runs.status, RunStatuses.completed), eq16(runs.status, RunStatuses.partial)),
|
|
12676
12761
|
notProbeRun()
|
|
12677
12762
|
)
|
|
12678
12763
|
).orderBy(desc7(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
12679
12764
|
if (recentRunIds.length === 0) return [];
|
|
12680
|
-
const rows = db.select().from(insights).where(and12(
|
|
12765
|
+
const rows = db.select().from(insights).where(and12(eq16(insights.projectId, projectId), inArray7(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
|
|
12681
12766
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
12682
12767
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
12683
12768
|
const recommendation = r.recommendation;
|
|
@@ -13229,7 +13314,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
13229
13314
|
const project = resolveProject(db, projectName);
|
|
13230
13315
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
13231
13316
|
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
13232
|
-
const allRuns = db.select().from(runs).where(and12(
|
|
13317
|
+
const allRuns = db.select().from(runs).where(and12(eq16(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
13233
13318
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
13234
13319
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
13235
13320
|
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
@@ -13238,7 +13323,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
13238
13323
|
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
13239
13324
|
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
13240
13325
|
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
13241
|
-
const competitorRows = db.select().from(competitors).where(
|
|
13326
|
+
const competitorRows = db.select().from(competitors).where(eq16(competitors.projectId, project.id)).all();
|
|
13242
13327
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
13243
13328
|
const ownedDomains = project.ownedDomains;
|
|
13244
13329
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
@@ -13465,16 +13550,16 @@ async function reportRoutes(app) {
|
|
|
13465
13550
|
}
|
|
13466
13551
|
|
|
13467
13552
|
// ../api-routes/src/citations.ts
|
|
13468
|
-
import { and as and13, eq as
|
|
13553
|
+
import { and as and13, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
13469
13554
|
async function citationRoutes(app) {
|
|
13470
13555
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
13471
13556
|
const project = resolveProject(app.db, request.params.name);
|
|
13472
13557
|
const configuredProviders = project.providers;
|
|
13473
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
13558
|
+
const projectQueries = app.db.select().from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
13474
13559
|
if (projectQueries.length === 0) {
|
|
13475
13560
|
return reply.send(emptyCitationVisibility("no-queries"));
|
|
13476
13561
|
}
|
|
13477
|
-
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and13(
|
|
13562
|
+
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and13(eq17(runs.projectId, project.id), notProbeRun())).all();
|
|
13478
13563
|
if (projectRuns.length === 0) {
|
|
13479
13564
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
13480
13565
|
}
|
|
@@ -13489,7 +13574,7 @@ async function citationRoutes(app) {
|
|
|
13489
13574
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
13490
13575
|
answerMentioned: querySnapshots.answerMentioned,
|
|
13491
13576
|
createdAt: querySnapshots.createdAt
|
|
13492
|
-
}).from(querySnapshots).where(
|
|
13577
|
+
}).from(querySnapshots).where(inArray8(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
|
|
13493
13578
|
if (rawSnapshots.length === 0) {
|
|
13494
13579
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
13495
13580
|
}
|
|
@@ -13498,7 +13583,7 @@ async function citationRoutes(app) {
|
|
|
13498
13583
|
queryId: s.queryId,
|
|
13499
13584
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
13500
13585
|
}));
|
|
13501
|
-
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
13586
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
|
|
13502
13587
|
const response = computeCitationVisibility({
|
|
13503
13588
|
queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
|
|
13504
13589
|
snapshots,
|
|
@@ -13630,7 +13715,254 @@ function normalizeDomain2(domain) {
|
|
|
13630
13715
|
}
|
|
13631
13716
|
|
|
13632
13717
|
// ../api-routes/src/visibility-stats.ts
|
|
13633
|
-
import { and as and14, desc as desc8, eq as
|
|
13718
|
+
import { and as and14, desc as desc8, eq as eq18, inArray as inArray9 } from "drizzle-orm";
|
|
13719
|
+
|
|
13720
|
+
// ../api-routes/src/visibility-compare.ts
|
|
13721
|
+
var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
13722
|
+
function basketPairKey(queryId, provider) {
|
|
13723
|
+
return JSON.stringify([queryId, provider]);
|
|
13724
|
+
}
|
|
13725
|
+
function citedHostMatches(citedHost, competitorHost) {
|
|
13726
|
+
return citedHost === competitorHost || citedHost.endsWith(`.${competitorHost}`);
|
|
13727
|
+
}
|
|
13728
|
+
function restrict(snapshots, attribution, pairs) {
|
|
13729
|
+
const out = [];
|
|
13730
|
+
for (const snap of snapshots) {
|
|
13731
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13732
|
+
if (!resolved) continue;
|
|
13733
|
+
if (!pairs.has(basketPairKey(resolved.id, snap.provider))) continue;
|
|
13734
|
+
out.push({ ...snap, queryId: resolved.id });
|
|
13735
|
+
}
|
|
13736
|
+
return out;
|
|
13737
|
+
}
|
|
13738
|
+
function observed(snapshots, attribution) {
|
|
13739
|
+
const queryIds = /* @__PURE__ */ new Set();
|
|
13740
|
+
const providers = /* @__PURE__ */ new Set();
|
|
13741
|
+
for (const snap of snapshots) {
|
|
13742
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13743
|
+
if (!resolved) continue;
|
|
13744
|
+
queryIds.add(resolved.id);
|
|
13745
|
+
providers.add(snap.provider);
|
|
13746
|
+
}
|
|
13747
|
+
return { queryIds, providers };
|
|
13748
|
+
}
|
|
13749
|
+
function observedPairs(snapshots, attribution, queryIds) {
|
|
13750
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
13751
|
+
for (const snap of snapshots) {
|
|
13752
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13753
|
+
if (!resolved || !queryIds.has(resolved.id)) continue;
|
|
13754
|
+
const pair = { queryId: resolved.id, provider: snap.provider };
|
|
13755
|
+
pairs.set(basketPairKey(pair.queryId, pair.provider), pair);
|
|
13756
|
+
}
|
|
13757
|
+
return pairs;
|
|
13758
|
+
}
|
|
13759
|
+
function period(numerator, denominator) {
|
|
13760
|
+
const ci = wilsonInterval(numerator, denominator);
|
|
13761
|
+
return {
|
|
13762
|
+
point: denominator > 0 ? Math.round(numerator / denominator * 1e4) / 1e4 : null,
|
|
13763
|
+
ciLow: ci ? ci.low : null,
|
|
13764
|
+
ciHigh: ci ? ci.high : null,
|
|
13765
|
+
numerator,
|
|
13766
|
+
denominator
|
|
13767
|
+
};
|
|
13768
|
+
}
|
|
13769
|
+
function ciOverlap(a, b) {
|
|
13770
|
+
if (a.ciLow === null || a.ciHigh === null || b.ciLow === null || b.ciHigh === null) return true;
|
|
13771
|
+
return a.ciLow <= b.ciHigh && b.ciLow <= a.ciHigh;
|
|
13772
|
+
}
|
|
13773
|
+
function metric(key, label, driftRobust, from, to, continuityBlock) {
|
|
13774
|
+
const verdict = continuityBlock ?? (from.denominator === 0 || to.denominator === 0 ? "insufficient-data" : ciOverlap(from, to) ? "within-noise" : "moved");
|
|
13775
|
+
const direction = from.point === null || to.point === null ? null : to.point > from.point ? "up" : to.point < from.point ? "down" : "flat";
|
|
13776
|
+
const rateRatio = from.point === null || from.point === 0 || to.point === null ? null : Math.round(to.point / from.point * 100) / 100;
|
|
13777
|
+
return { key, label, driftRobust, from, to, rateRatio, direction, verdict };
|
|
13778
|
+
}
|
|
13779
|
+
function countPeriod(snaps, competitors2) {
|
|
13780
|
+
let checked = 0;
|
|
13781
|
+
let mentioned = 0;
|
|
13782
|
+
let cited = 0;
|
|
13783
|
+
let competitorCited = 0;
|
|
13784
|
+
const perProvider = /* @__PURE__ */ new Map();
|
|
13785
|
+
const models = /* @__PURE__ */ new Map();
|
|
13786
|
+
const modelUnknownProviders = /* @__PURE__ */ new Set();
|
|
13787
|
+
const mentionedQueries = /* @__PURE__ */ new Set();
|
|
13788
|
+
const competitorHosts = competitors2.map((c) => hostOf(c.domain)).filter((h) => h !== null && h.length > 0);
|
|
13789
|
+
for (const snap of snaps) {
|
|
13790
|
+
const isMentioned = snap.answerMentioned === true;
|
|
13791
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) checked += 1;
|
|
13792
|
+
if (isMentioned) {
|
|
13793
|
+
mentioned += 1;
|
|
13794
|
+
mentionedQueries.add(snap.queryId);
|
|
13795
|
+
}
|
|
13796
|
+
const isCited = snap.citationState === CitationStates.cited;
|
|
13797
|
+
if (isCited) cited += 1;
|
|
13798
|
+
const pp = perProvider.get(snap.provider) ?? { checked: 0, mentioned: 0, cited: 0 };
|
|
13799
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) pp.checked += 1;
|
|
13800
|
+
if (isMentioned) pp.mentioned += 1;
|
|
13801
|
+
if (isCited) pp.cited += 1;
|
|
13802
|
+
perProvider.set(snap.provider, pp);
|
|
13803
|
+
if (snap.model) {
|
|
13804
|
+
const set = models.get(snap.provider) ?? /* @__PURE__ */ new Set();
|
|
13805
|
+
set.add(snap.model);
|
|
13806
|
+
models.set(snap.provider, set);
|
|
13807
|
+
} else {
|
|
13808
|
+
modelUnknownProviders.add(snap.provider);
|
|
13809
|
+
}
|
|
13810
|
+
if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
13811
|
+
const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
|
|
13812
|
+
for (const compHost of competitorHosts) {
|
|
13813
|
+
if (citedHosts.some((ch) => citedHostMatches(ch, compHost))) competitorCited += 1;
|
|
13814
|
+
}
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
const mentionShare = buildMentionShare(
|
|
13818
|
+
snaps.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
|
|
13819
|
+
{ competitors: competitors2 }
|
|
13820
|
+
);
|
|
13821
|
+
return {
|
|
13822
|
+
checked,
|
|
13823
|
+
mentioned,
|
|
13824
|
+
total: snaps.length,
|
|
13825
|
+
cited,
|
|
13826
|
+
projectCited: cited,
|
|
13827
|
+
competitorCited,
|
|
13828
|
+
queriesMentioned: mentionedQueries.size,
|
|
13829
|
+
perProvider,
|
|
13830
|
+
models,
|
|
13831
|
+
modelUnknownProviders,
|
|
13832
|
+
mentionShare,
|
|
13833
|
+
competitors: mentionShare.breakdown.perCompetitor.map((c) => ({ domain: c.domain, mentions: c.mentionSnapshots }))
|
|
13834
|
+
};
|
|
13835
|
+
}
|
|
13836
|
+
function computeVisibilityCompare(input) {
|
|
13837
|
+
const attribution = buildQueryAttribution(input.queries);
|
|
13838
|
+
const fromObs = observed(input.from.snapshots, attribution);
|
|
13839
|
+
const toObs = observed(input.to.snapshots, attribution);
|
|
13840
|
+
const queriesObservedBoth = new Set([...fromObs.queryIds].filter((q) => toObs.queryIds.has(q)));
|
|
13841
|
+
const fromPairs = observedPairs(input.from.snapshots, attribution, queriesObservedBoth);
|
|
13842
|
+
const toPairs = observedPairs(input.to.snapshots, attribution, queriesObservedBoth);
|
|
13843
|
+
const pairsBoth = new Map([...fromPairs].filter(([key]) => toPairs.has(key)));
|
|
13844
|
+
const candidateProviders = new Set([...pairsBoth.values()].map((pair) => pair.provider));
|
|
13845
|
+
const fromCandidateSnaps = restrict(input.from.snapshots, attribution, pairsBoth);
|
|
13846
|
+
const toCandidateSnaps = restrict(input.to.snapshots, attribution, pairsBoth);
|
|
13847
|
+
const fromCandidateCounts = countPeriod(fromCandidateSnaps, input.competitors);
|
|
13848
|
+
const toCandidateCounts = countPeriod(toCandidateSnaps, input.competitors);
|
|
13849
|
+
const continuityProviders = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
13850
|
+
const fromModels = [...fromCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13851
|
+
const toModels = [...toCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13852
|
+
const fromModelUnknown = fromCandidateCounts.modelUnknownProviders.has(provider) || fromModels.length === 0;
|
|
13853
|
+
const toModelUnknown = toCandidateCounts.modelUnknownProviders.has(provider) || toModels.length === 0;
|
|
13854
|
+
const status = fromModelUnknown || toModelUnknown ? "model-unknown" : fromModels.length === 1 && toModels.length === 1 && fromModels[0] === toModels[0] ? "included" : "model-discontinuous";
|
|
13855
|
+
return { provider, status, fromModels, toModels };
|
|
13856
|
+
});
|
|
13857
|
+
const providersBoth = new Set(
|
|
13858
|
+
continuityProviders.filter((provider) => provider.status === "included").map((provider) => provider.provider)
|
|
13859
|
+
);
|
|
13860
|
+
const continuityStatus = candidateProviders.size === 0 ? "insufficient-data" : providersBoth.size > 0 ? "comparable" : continuityProviders.every((provider) => provider.status === "model-unknown") ? "model-unknown" : "model-discontinuous";
|
|
13861
|
+
const continuityBlock = continuityStatus === "model-discontinuous" || continuityStatus === "model-unknown" ? continuityStatus : null;
|
|
13862
|
+
const comparedPairs = new Map([...pairsBoth].filter(([, pair]) => providersBoth.has(pair.provider)));
|
|
13863
|
+
const queriesBoth = new Set([...comparedPairs.values()].map((pair) => pair.queryId));
|
|
13864
|
+
const excludedProviders = [.../* @__PURE__ */ new Set([...fromObs.providers, ...toObs.providers])].filter((p) => !providersBoth.has(p)).sort((a, b) => a.localeCompare(b));
|
|
13865
|
+
const fromSnaps = restrict(input.from.snapshots, attribution, comparedPairs);
|
|
13866
|
+
const toSnaps = restrict(input.to.snapshots, attribution, comparedPairs);
|
|
13867
|
+
const fromCounts = countPeriod(fromSnaps, input.competitors);
|
|
13868
|
+
const toCounts = countPeriod(toSnaps, input.competitors);
|
|
13869
|
+
const shareCounts = (c) => ({
|
|
13870
|
+
proj: c.mentionShare.breakdown.projectMentionSnapshots,
|
|
13871
|
+
comp: c.mentionShare.breakdown.competitorMentionSnapshots
|
|
13872
|
+
});
|
|
13873
|
+
const fromShare = shareCounts(fromCounts);
|
|
13874
|
+
const toShare = shareCounts(toCounts);
|
|
13875
|
+
const metrics = [
|
|
13876
|
+
metric(
|
|
13877
|
+
"mention-share-of-voice",
|
|
13878
|
+
"Named share of voice",
|
|
13879
|
+
true,
|
|
13880
|
+
period(fromShare.proj, fromShare.proj + fromShare.comp),
|
|
13881
|
+
period(toShare.proj, toShare.proj + toShare.comp),
|
|
13882
|
+
continuityBlock
|
|
13883
|
+
),
|
|
13884
|
+
// Share of voice is undefined without a competitive frame: with zero
|
|
13885
|
+
// configured competitors the denominator degenerates to the project's own
|
|
13886
|
+
// count and the metric would fabricate a 100%. buildMentionShare already
|
|
13887
|
+
// refuses that on the mention side ("reporting 100% would mislead");
|
|
13888
|
+
// degrade the cited side identically to a 0/0 period -> insufficient-data.
|
|
13889
|
+
metric(
|
|
13890
|
+
"cited-share-of-voice",
|
|
13891
|
+
"Cited share of voice",
|
|
13892
|
+
true,
|
|
13893
|
+
input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : period(0, 0),
|
|
13894
|
+
input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : period(0, 0),
|
|
13895
|
+
continuityBlock
|
|
13896
|
+
),
|
|
13897
|
+
metric(
|
|
13898
|
+
"mention-rate",
|
|
13899
|
+
"Named rate",
|
|
13900
|
+
false,
|
|
13901
|
+
period(fromCounts.mentioned, fromCounts.checked),
|
|
13902
|
+
period(toCounts.mentioned, toCounts.checked),
|
|
13903
|
+
continuityBlock
|
|
13904
|
+
),
|
|
13905
|
+
metric(
|
|
13906
|
+
"cited-rate",
|
|
13907
|
+
"Cited rate",
|
|
13908
|
+
false,
|
|
13909
|
+
period(fromCounts.cited, fromCounts.total),
|
|
13910
|
+
period(toCounts.cited, toCounts.total),
|
|
13911
|
+
continuityBlock
|
|
13912
|
+
)
|
|
13913
|
+
];
|
|
13914
|
+
const modelChanges = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
13915
|
+
const fromModels = [...fromCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13916
|
+
const toModels = [...toCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13917
|
+
return { provider, fromModels, toModels };
|
|
13918
|
+
}).filter(
|
|
13919
|
+
(c) => c.fromModels.length > 0 && c.toModels.length > 0 && JSON.stringify(c.fromModels) !== JSON.stringify(c.toModels)
|
|
13920
|
+
);
|
|
13921
|
+
const byProvider = [...providersBoth].sort((a, b) => a.localeCompare(b)).map((provider) => ({
|
|
13922
|
+
provider,
|
|
13923
|
+
from: fromCounts.perProvider.get(provider) ?? { checked: 0, mentioned: 0, cited: 0 },
|
|
13924
|
+
to: toCounts.perProvider.get(provider) ?? { checked: 0, mentioned: 0, cited: 0 }
|
|
13925
|
+
}));
|
|
13926
|
+
return {
|
|
13927
|
+
project: input.project,
|
|
13928
|
+
from: {
|
|
13929
|
+
month: input.from.month,
|
|
13930
|
+
since: input.from.since,
|
|
13931
|
+
until: input.from.until,
|
|
13932
|
+
runCount: input.from.runCount,
|
|
13933
|
+
lowRunCount: input.from.runCount < VISIBILITY_COMPARE_MIN_RUNS
|
|
13934
|
+
},
|
|
13935
|
+
to: {
|
|
13936
|
+
month: input.to.month,
|
|
13937
|
+
since: input.to.since,
|
|
13938
|
+
until: input.to.until,
|
|
13939
|
+
runCount: input.to.runCount,
|
|
13940
|
+
lowRunCount: input.to.runCount < VISIBILITY_COMPARE_MIN_RUNS
|
|
13941
|
+
},
|
|
13942
|
+
basket: {
|
|
13943
|
+
queryCount: queriesBoth.size,
|
|
13944
|
+
excludedFromOnly: [...fromObs.queryIds].filter((q) => !queriesBoth.has(q)).length,
|
|
13945
|
+
excludedToOnly: [...toObs.queryIds].filter((q) => !queriesBoth.has(q)).length,
|
|
13946
|
+
providers: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
13947
|
+
excludedProviders
|
|
13948
|
+
},
|
|
13949
|
+
metrics,
|
|
13950
|
+
queriesMentioned: {
|
|
13951
|
+
from: { count: fromCounts.queriesMentioned, of: queriesBoth.size },
|
|
13952
|
+
to: { count: toCounts.queriesMentioned, of: queriesBoth.size }
|
|
13953
|
+
},
|
|
13954
|
+
byProvider,
|
|
13955
|
+
modelChanges,
|
|
13956
|
+
continuity: {
|
|
13957
|
+
status: continuityStatus,
|
|
13958
|
+
comparedProviders: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
13959
|
+
providers: continuityProviders
|
|
13960
|
+
},
|
|
13961
|
+
competitors: { from: fromCounts.competitors, to: toCounts.competitors }
|
|
13962
|
+
};
|
|
13963
|
+
}
|
|
13964
|
+
|
|
13965
|
+
// ../api-routes/src/visibility-stats.ts
|
|
13634
13966
|
function round42(value) {
|
|
13635
13967
|
return Math.round(value * 1e4) / 1e4;
|
|
13636
13968
|
}
|
|
@@ -13781,8 +14113,8 @@ async function visibilityStatsRoutes(app) {
|
|
|
13781
14113
|
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
13782
14114
|
lastRuns = n;
|
|
13783
14115
|
}
|
|
13784
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
13785
|
-
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(
|
|
14116
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
14117
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq18(runs.projectId, project.id), eq18(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
13786
14118
|
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
13787
14119
|
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
13788
14120
|
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
@@ -13795,12 +14127,12 @@ async function visibilityStatsRoutes(app) {
|
|
|
13795
14127
|
citationState: querySnapshots.citationState,
|
|
13796
14128
|
answerMentioned: querySnapshots.answerMentioned,
|
|
13797
14129
|
createdAt: querySnapshots.createdAt
|
|
13798
|
-
}).from(querySnapshots).where(
|
|
14130
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
13799
14131
|
const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
|
|
13800
14132
|
const queryAttribution = buildQueryAttribution(projectQueries);
|
|
13801
14133
|
let shareOfVoice;
|
|
13802
14134
|
if (wantShareOfVoice) {
|
|
13803
|
-
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
14135
|
+
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
13804
14136
|
const mentionShareCompetitors = competitorRows.map((c) => ({
|
|
13805
14137
|
domain: c.domain,
|
|
13806
14138
|
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
@@ -13810,7 +14142,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
13810
14142
|
queryText: querySnapshots.queryText,
|
|
13811
14143
|
answerMentioned: querySnapshots.answerMentioned,
|
|
13812
14144
|
answerText: querySnapshots.answerText
|
|
13813
|
-
}).from(querySnapshots).where(
|
|
14145
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
13814
14146
|
const attributedSovSnapshots = sovSnapshots.filter((s) => resolveCurrentQuery(queryAttribution, s) !== void 0);
|
|
13815
14147
|
const result = buildMentionShare(
|
|
13816
14148
|
attributedSovSnapshots.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
|
|
@@ -13849,10 +14181,66 @@ async function visibilityStatsRoutes(app) {
|
|
|
13849
14181
|
};
|
|
13850
14182
|
return reply.send(response);
|
|
13851
14183
|
});
|
|
14184
|
+
app.get("/projects/:name/visibility-compare", async (request, reply) => {
|
|
14185
|
+
const project = resolveProject(app.db, request.params.name);
|
|
14186
|
+
const { from: fromRaw, to: toRaw } = request.query;
|
|
14187
|
+
if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
|
|
14188
|
+
if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
|
|
14189
|
+
let fromBounds;
|
|
14190
|
+
let toBounds;
|
|
14191
|
+
try {
|
|
14192
|
+
fromBounds = calendarMonthBounds(fromRaw);
|
|
14193
|
+
} catch (err) {
|
|
14194
|
+
throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
|
|
14195
|
+
}
|
|
14196
|
+
try {
|
|
14197
|
+
toBounds = calendarMonthBounds(toRaw);
|
|
14198
|
+
} catch (err) {
|
|
14199
|
+
throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
|
|
14200
|
+
}
|
|
14201
|
+
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
14202
|
+
throw validationError('"from" must be a month strictly before "to"');
|
|
14203
|
+
}
|
|
14204
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
14205
|
+
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
14206
|
+
const competitorInputs = competitorRows.map((c) => ({
|
|
14207
|
+
domain: c.domain,
|
|
14208
|
+
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
14209
|
+
}));
|
|
14210
|
+
const loadMonth = (bounds) => {
|
|
14211
|
+
const sinceMs = Date.parse(bounds.since);
|
|
14212
|
+
const untilMs = Date.parse(bounds.until);
|
|
14213
|
+
const monthRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq18(runs.projectId, project.id), eq18(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
14214
|
+
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
14215
|
+
);
|
|
14216
|
+
const runIds = monthRuns.map((r) => r.id);
|
|
14217
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? app.db.select({
|
|
14218
|
+
queryId: querySnapshots.queryId,
|
|
14219
|
+
queryText: querySnapshots.queryText,
|
|
14220
|
+
provider: querySnapshots.provider,
|
|
14221
|
+
model: querySnapshots.model,
|
|
14222
|
+
citationState: querySnapshots.citationState,
|
|
14223
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
14224
|
+
answerText: querySnapshots.answerText,
|
|
14225
|
+
citedDomains: querySnapshots.citedDomains
|
|
14226
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all() : [];
|
|
14227
|
+
return { runCount: monthRuns.length, snapshots };
|
|
14228
|
+
};
|
|
14229
|
+
const fromMonth = loadMonth(fromBounds);
|
|
14230
|
+
const toMonth = loadMonth(toBounds);
|
|
14231
|
+
const dto = computeVisibilityCompare({
|
|
14232
|
+
project: project.name,
|
|
14233
|
+
queries: projectQueries,
|
|
14234
|
+
competitors: competitorInputs,
|
|
14235
|
+
from: { month: fromRaw, since: fromBounds.since, until: fromBounds.until, runCount: fromMonth.runCount, snapshots: fromMonth.snapshots },
|
|
14236
|
+
to: { month: toRaw, since: toBounds.since, until: toBounds.until, runCount: toMonth.runCount, snapshots: toMonth.snapshots }
|
|
14237
|
+
});
|
|
14238
|
+
return reply.send(dto);
|
|
14239
|
+
});
|
|
13852
14240
|
}
|
|
13853
14241
|
|
|
13854
14242
|
// ../api-routes/src/composites.ts
|
|
13855
|
-
import { eq as
|
|
14243
|
+
import { eq as eq19, and as and15, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray10 } from "drizzle-orm";
|
|
13856
14244
|
var TOP_INSIGHT_LIMIT = 5;
|
|
13857
14245
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
13858
14246
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -13870,7 +14258,7 @@ async function compositeRoutes(app) {
|
|
|
13870
14258
|
const project = resolveProject(app.db, request.params.name);
|
|
13871
14259
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
13872
14260
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
13873
|
-
const allRunsRaw = app.db.select().from(runs).where(and15(
|
|
14261
|
+
const allRunsRaw = app.db.select().from(runs).where(and15(eq19(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
13874
14262
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
13875
14263
|
const totalRuns = allRuns.length;
|
|
13876
14264
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -13883,15 +14271,15 @@ async function compositeRoutes(app) {
|
|
|
13883
14271
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
13884
14272
|
const latestRunRow = allRuns[0] ?? null;
|
|
13885
14273
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
13886
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
14274
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq19(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
13887
14275
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
13888
|
-
const insightRows = app.db.select().from(insights).where(
|
|
14276
|
+
const insightRows = app.db.select().from(insights).where(eq19(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
13889
14277
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
13890
14278
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
13891
14279
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
13892
14280
|
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
13893
14281
|
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
13894
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
14282
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq19(queries.projectId, project.id)).all();
|
|
13895
14283
|
const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
|
|
13896
14284
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
|
|
13897
14285
|
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
@@ -13907,7 +14295,7 @@ async function compositeRoutes(app) {
|
|
|
13907
14295
|
trackedPrevious,
|
|
13908
14296
|
previousVisibilityRun?.createdAt ?? null
|
|
13909
14297
|
);
|
|
13910
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
14298
|
+
const competitorRows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
|
|
13911
14299
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
13912
14300
|
for (const snapshots of snapshotsByRun.values()) {
|
|
13913
14301
|
for (const snapshot of snapshots) {
|
|
@@ -14025,9 +14413,9 @@ async function compositeRoutes(app) {
|
|
|
14025
14413
|
citedDomains: querySnapshots.citedDomains,
|
|
14026
14414
|
rawResponse: querySnapshots.rawResponse,
|
|
14027
14415
|
createdAt: querySnapshots.createdAt
|
|
14028
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
14416
|
+
}).from(querySnapshots).innerJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(
|
|
14029
14417
|
and15(
|
|
14030
|
-
|
|
14418
|
+
eq19(queries.projectId, project.id),
|
|
14031
14419
|
or4(
|
|
14032
14420
|
sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
14033
14421
|
sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -14038,7 +14426,7 @@ async function compositeRoutes(app) {
|
|
|
14038
14426
|
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
14039
14427
|
const insightMatches = app.db.select().from(insights).where(
|
|
14040
14428
|
and15(
|
|
14041
|
-
|
|
14429
|
+
eq19(insights.projectId, project.id),
|
|
14042
14430
|
or4(
|
|
14043
14431
|
like2(insights.title, pattern),
|
|
14044
14432
|
like2(insights.query, pattern),
|
|
@@ -14117,7 +14505,7 @@ function loadSnapshotsByRunIds(app, runIds, queryIdByText) {
|
|
|
14117
14505
|
answerText: querySnapshots.answerText,
|
|
14118
14506
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
14119
14507
|
citedDomains: querySnapshots.citedDomains
|
|
14120
|
-
}).from(querySnapshots).where(
|
|
14508
|
+
}).from(querySnapshots).where(inArray10(querySnapshots.runId, [...runIds])).all();
|
|
14121
14509
|
for (const row of rows) {
|
|
14122
14510
|
const queryText = row.queryText?.trim() || null;
|
|
14123
14511
|
let queryId;
|
|
@@ -14255,7 +14643,7 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
14255
14643
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
14256
14644
|
avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
14257
14645
|
}).from(gscSearchData).where(and15(
|
|
14258
|
-
|
|
14646
|
+
eq19(gscSearchData.projectId, projectId),
|
|
14259
14647
|
sql8`${gscSearchData.date} >= ${cutoff}`,
|
|
14260
14648
|
sql8`${gscSearchData.impressions} > 0`
|
|
14261
14649
|
)).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
@@ -14278,8 +14666,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
14278
14666
|
tooltip,
|
|
14279
14667
|
trend: []
|
|
14280
14668
|
};
|
|
14281
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
14282
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
14669
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq19(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
14670
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq19(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
14283
14671
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
14284
14672
|
if (!chosen) return empty;
|
|
14285
14673
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -14305,7 +14693,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
14305
14693
|
url: gscUrlInspections.url,
|
|
14306
14694
|
indexingState: gscUrlInspections.indexingState,
|
|
14307
14695
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
14308
|
-
}).from(gscUrlInspections).where(
|
|
14696
|
+
}).from(gscUrlInspections).where(eq19(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
14309
14697
|
if (rows.length === 0) return 0;
|
|
14310
14698
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
14311
14699
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -14654,6 +15042,7 @@ var SCHEMA_TABLE = {
|
|
|
14654
15042
|
TrafficSourceListResponse: trafficSourceListResponseSchema,
|
|
14655
15043
|
TrafficStatusResponse: trafficStatusResponseSchema,
|
|
14656
15044
|
TrafficSyncResponse: trafficSyncResponseSchema,
|
|
15045
|
+
VisibilityCompareDto: visibilityCompareDtoSchema,
|
|
14657
15046
|
VisibilityStatsDto: visibilityStatsDtoSchema,
|
|
14658
15047
|
WordpressAuditPageDto: wordpressAuditPageDtoSchema,
|
|
14659
15048
|
WordpressBulkMetaResultDto: wordpressBulkMetaResultDtoSchema,
|
|
@@ -14930,6 +15319,20 @@ var monthQueryParameter = {
|
|
|
14930
15319
|
description: `Aggregate a single calendar month (YYYY-MM), expanded to that month's inclusive UTC bounds. Mutually exclusive with "since"/"until"/"lastRuns".`,
|
|
14931
15320
|
schema: stringSchema
|
|
14932
15321
|
};
|
|
15322
|
+
var compareFromQueryParameter = {
|
|
15323
|
+
name: "from",
|
|
15324
|
+
in: "query",
|
|
15325
|
+
required: true,
|
|
15326
|
+
description: 'Earlier calendar month (YYYY-MM) \u2014 the baseline period. Must be strictly before "to".',
|
|
15327
|
+
schema: stringSchema
|
|
15328
|
+
};
|
|
15329
|
+
var compareToQueryParameter = {
|
|
15330
|
+
name: "to",
|
|
15331
|
+
in: "query",
|
|
15332
|
+
required: true,
|
|
15333
|
+
description: 'Later calendar month (YYYY-MM) \u2014 the reporting period compared against "from".',
|
|
15334
|
+
schema: stringSchema
|
|
15335
|
+
};
|
|
14933
15336
|
var shareOfVoiceQueryParameter = {
|
|
14934
15337
|
name: "shareOfVoice",
|
|
14935
15338
|
in: "query",
|
|
@@ -15705,6 +16108,19 @@ var routeCatalog = [
|
|
|
15705
16108
|
404: errorResponse("Project not found.")
|
|
15706
16109
|
}
|
|
15707
16110
|
},
|
|
16111
|
+
{
|
|
16112
|
+
method: "get",
|
|
16113
|
+
path: "/api/v1/projects/{name}/visibility-compare",
|
|
16114
|
+
summary: "Compare AEO visibility month over month",
|
|
16115
|
+
description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. A silent upstream version bump under an unchanged configured id remains undetectable.",
|
|
16116
|
+
tags: ["analytics"],
|
|
16117
|
+
parameters: [nameParameter, compareFromQueryParameter, compareToQueryParameter],
|
|
16118
|
+
responses: {
|
|
16119
|
+
200: jsonResponse("Month-over-month visibility comparison returned.", "VisibilityCompareDto"),
|
|
16120
|
+
400: errorResponse("Invalid or missing from/to months."),
|
|
16121
|
+
404: errorResponse("Project not found.")
|
|
16122
|
+
}
|
|
16123
|
+
},
|
|
15708
16124
|
{
|
|
15709
16125
|
method: "get",
|
|
15710
16126
|
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
@@ -19015,8 +19431,8 @@ async function settingsRoutes(app, opts) {
|
|
|
19015
19431
|
}
|
|
19016
19432
|
|
|
19017
19433
|
// ../api-routes/src/keys.ts
|
|
19018
|
-
import
|
|
19019
|
-
import { desc as desc10, eq as
|
|
19434
|
+
import crypto13 from "crypto";
|
|
19435
|
+
import { desc as desc10, eq as eq20 } from "drizzle-orm";
|
|
19020
19436
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
19021
19437
|
function toApiKeyDto(row) {
|
|
19022
19438
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -19035,7 +19451,7 @@ function toApiKeyDto(row) {
|
|
|
19035
19451
|
async function keysRoutes(app) {
|
|
19036
19452
|
app.get("/keys", async (request) => {
|
|
19037
19453
|
const scopedProjectId = request.apiKey?.projectId;
|
|
19038
|
-
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(
|
|
19454
|
+
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq20(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
19039
19455
|
return { keys: rows.map(toApiKeyDto) };
|
|
19040
19456
|
});
|
|
19041
19457
|
app.get("/keys/self", async (request) => {
|
|
@@ -19043,7 +19459,7 @@ async function keysRoutes(app) {
|
|
|
19043
19459
|
if (!id) {
|
|
19044
19460
|
throw notFound("API key", "self");
|
|
19045
19461
|
}
|
|
19046
|
-
const row = app.db.select().from(apiKeys).where(
|
|
19462
|
+
const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
|
|
19047
19463
|
if (!row) {
|
|
19048
19464
|
throw notFound("API key", id);
|
|
19049
19465
|
}
|
|
@@ -19061,15 +19477,15 @@ async function keysRoutes(app) {
|
|
|
19061
19477
|
throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
|
|
19062
19478
|
}
|
|
19063
19479
|
if (projectId) {
|
|
19064
|
-
const proj = app.db.select({ id: projects.id }).from(projects).where(
|
|
19480
|
+
const proj = app.db.select({ id: projects.id }).from(projects).where(eq20(projects.id, projectId)).get();
|
|
19065
19481
|
if (!proj) {
|
|
19066
19482
|
throw notFound("Project", projectId);
|
|
19067
19483
|
}
|
|
19068
19484
|
}
|
|
19069
|
-
const raw = `cnry_${
|
|
19485
|
+
const raw = `cnry_${crypto13.randomBytes(16).toString("hex")}`;
|
|
19070
19486
|
const keyHash = hashApiKey(raw);
|
|
19071
19487
|
const keyPrefix = raw.slice(0, 9);
|
|
19072
|
-
const id =
|
|
19488
|
+
const id = crypto13.randomUUID();
|
|
19073
19489
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19074
19490
|
const effectiveScopes = scopes ?? ["*"];
|
|
19075
19491
|
app.db.transaction((tx) => {
|
|
@@ -19107,7 +19523,7 @@ async function keysRoutes(app) {
|
|
|
19107
19523
|
app.post("/keys/:id/revoke", async (request) => {
|
|
19108
19524
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
19109
19525
|
const { id } = request.params;
|
|
19110
|
-
const row = app.db.select().from(apiKeys).where(
|
|
19526
|
+
const row = app.db.select().from(apiKeys).where(eq20(apiKeys.id, id)).get();
|
|
19111
19527
|
if (!row) {
|
|
19112
19528
|
throw notFound("API key", id);
|
|
19113
19529
|
}
|
|
@@ -19123,7 +19539,7 @@ async function keysRoutes(app) {
|
|
|
19123
19539
|
}
|
|
19124
19540
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19125
19541
|
app.db.transaction((tx) => {
|
|
19126
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
19542
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq20(apiKeys.id, id)).run();
|
|
19127
19543
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
19128
19544
|
actor: "api",
|
|
19129
19545
|
action: "api-key.revoked",
|
|
@@ -19195,8 +19611,8 @@ async function telemetryRoutes(app, opts) {
|
|
|
19195
19611
|
}
|
|
19196
19612
|
|
|
19197
19613
|
// ../api-routes/src/schedules.ts
|
|
19198
|
-
import
|
|
19199
|
-
import { and as and16, eq as
|
|
19614
|
+
import crypto14 from "crypto";
|
|
19615
|
+
import { and as and16, eq as eq21 } from "drizzle-orm";
|
|
19200
19616
|
function parseKindParam(raw) {
|
|
19201
19617
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
19202
19618
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -19223,7 +19639,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19223
19639
|
if (!sourceId) {
|
|
19224
19640
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
19225
19641
|
}
|
|
19226
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
19642
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq21(trafficSources.id, sourceId)).get();
|
|
19227
19643
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
19228
19644
|
throw notFound("Traffic source", sourceId);
|
|
19229
19645
|
}
|
|
@@ -19268,7 +19684,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
19268
19684
|
}
|
|
19269
19685
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19270
19686
|
const enabledBool = enabled !== false;
|
|
19271
|
-
const existing = app.db.select().from(schedules).where(and16(
|
|
19687
|
+
const existing = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19272
19688
|
if (existing) {
|
|
19273
19689
|
app.db.update(schedules).set({
|
|
19274
19690
|
cronExpr,
|
|
@@ -19278,10 +19694,10 @@ async function scheduleRoutes(app, opts) {
|
|
|
19278
19694
|
sourceId: sourceId ?? null,
|
|
19279
19695
|
enabled: enabledBool,
|
|
19280
19696
|
updatedAt: now
|
|
19281
|
-
}).where(
|
|
19697
|
+
}).where(eq21(schedules.id, existing.id)).run();
|
|
19282
19698
|
} else {
|
|
19283
19699
|
app.db.insert(schedules).values({
|
|
19284
|
-
id:
|
|
19700
|
+
id: crypto14.randomUUID(),
|
|
19285
19701
|
projectId: project.id,
|
|
19286
19702
|
kind,
|
|
19287
19703
|
cronExpr,
|
|
@@ -19302,13 +19718,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
19302
19718
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
19303
19719
|
});
|
|
19304
19720
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
19305
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19721
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19306
19722
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
19307
19723
|
});
|
|
19308
19724
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
19309
19725
|
const project = resolveProject(app.db, request.params.name);
|
|
19310
19726
|
const kind = parseKindParam(request.query?.kind);
|
|
19311
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19727
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19312
19728
|
if (!schedule) {
|
|
19313
19729
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19314
19730
|
}
|
|
@@ -19317,11 +19733,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
19317
19733
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
19318
19734
|
const project = resolveProject(app.db, request.params.name);
|
|
19319
19735
|
const kind = parseKindParam(request.query?.kind);
|
|
19320
|
-
const schedule = app.db.select().from(schedules).where(and16(
|
|
19736
|
+
const schedule = app.db.select().from(schedules).where(and16(eq21(schedules.projectId, project.id), eq21(schedules.kind, kind))).get();
|
|
19321
19737
|
if (!schedule) {
|
|
19322
19738
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
19323
19739
|
}
|
|
19324
|
-
app.db.delete(schedules).where(
|
|
19740
|
+
app.db.delete(schedules).where(eq21(schedules.id, schedule.id)).run();
|
|
19325
19741
|
writeAuditLog(app.db, {
|
|
19326
19742
|
projectId: project.id,
|
|
19327
19743
|
actor: "api",
|
|
@@ -19353,8 +19769,8 @@ function formatSchedule(row) {
|
|
|
19353
19769
|
}
|
|
19354
19770
|
|
|
19355
19771
|
// ../api-routes/src/notifications.ts
|
|
19356
|
-
import
|
|
19357
|
-
import { eq as
|
|
19772
|
+
import crypto15 from "crypto";
|
|
19773
|
+
import { eq as eq22 } from "drizzle-orm";
|
|
19358
19774
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
19359
19775
|
async function notificationRoutes(app, opts = {}) {
|
|
19360
19776
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -19373,8 +19789,8 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19373
19789
|
throw validationError(`Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}`);
|
|
19374
19790
|
}
|
|
19375
19791
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19376
|
-
const id =
|
|
19377
|
-
const webhookSecret =
|
|
19792
|
+
const id = crypto15.randomUUID();
|
|
19793
|
+
const webhookSecret = crypto15.randomBytes(32).toString("hex");
|
|
19378
19794
|
app.db.insert(notifications).values({
|
|
19379
19795
|
id,
|
|
19380
19796
|
projectId: project.id,
|
|
@@ -19394,22 +19810,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19394
19810
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
19395
19811
|
});
|
|
19396
19812
|
return reply.status(201).send({
|
|
19397
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
19813
|
+
...formatNotification(app.db.select().from(notifications).where(eq22(notifications.id, id)).get()),
|
|
19398
19814
|
webhookSecret
|
|
19399
19815
|
});
|
|
19400
19816
|
});
|
|
19401
19817
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
19402
19818
|
const project = resolveProject(app.db, request.params.name);
|
|
19403
|
-
const rows = app.db.select().from(notifications).where(
|
|
19819
|
+
const rows = app.db.select().from(notifications).where(eq22(notifications.projectId, project.id)).all();
|
|
19404
19820
|
return reply.send(rows.map(formatNotification));
|
|
19405
19821
|
});
|
|
19406
19822
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
19407
19823
|
const project = resolveProject(app.db, request.params.name);
|
|
19408
|
-
const notification = app.db.select().from(notifications).where(
|
|
19824
|
+
const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
|
|
19409
19825
|
if (!notification || notification.projectId !== project.id) {
|
|
19410
19826
|
throw notFound("Notification", request.params.id);
|
|
19411
19827
|
}
|
|
19412
|
-
app.db.delete(notifications).where(
|
|
19828
|
+
app.db.delete(notifications).where(eq22(notifications.id, notification.id)).run();
|
|
19413
19829
|
writeAuditLog(app.db, {
|
|
19414
19830
|
projectId: project.id,
|
|
19415
19831
|
actor: "api",
|
|
@@ -19421,7 +19837,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
19421
19837
|
});
|
|
19422
19838
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
19423
19839
|
const project = resolveProject(app.db, request.params.name);
|
|
19424
|
-
const notification = app.db.select().from(notifications).where(
|
|
19840
|
+
const notification = app.db.select().from(notifications).where(eq22(notifications.id, request.params.id)).get();
|
|
19425
19841
|
if (!notification || notification.projectId !== project.id) {
|
|
19426
19842
|
throw notFound("Notification", request.params.id);
|
|
19427
19843
|
}
|
|
@@ -19473,8 +19889,8 @@ function formatNotification(row) {
|
|
|
19473
19889
|
}
|
|
19474
19890
|
|
|
19475
19891
|
// ../api-routes/src/google.ts
|
|
19476
|
-
import
|
|
19477
|
-
import { eq as
|
|
19892
|
+
import crypto19 from "crypto";
|
|
19893
|
+
import { eq as eq23, and as and17, desc as desc11, sql as sql9, inArray as inArray11 } from "drizzle-orm";
|
|
19478
19894
|
|
|
19479
19895
|
// ../api-routes/src/gbp-summary.ts
|
|
19480
19896
|
function computeMetricTotals(rows) {
|
|
@@ -19516,12 +19932,12 @@ function computeWindowDelta(rows, referenceDate) {
|
|
|
19516
19932
|
}
|
|
19517
19933
|
const deltaPct = {};
|
|
19518
19934
|
const metrics = /* @__PURE__ */ new Set([...Object.keys(recent7d), ...Object.keys(prior7d)]);
|
|
19519
|
-
for (const
|
|
19520
|
-
const recent = recent7d[
|
|
19521
|
-
const prior = prior7d[
|
|
19522
|
-
recent7d[
|
|
19523
|
-
prior7d[
|
|
19524
|
-
deltaPct[
|
|
19935
|
+
for (const metric2 of metrics) {
|
|
19936
|
+
const recent = recent7d[metric2] ?? 0;
|
|
19937
|
+
const prior = prior7d[metric2] ?? 0;
|
|
19938
|
+
recent7d[metric2] = recent;
|
|
19939
|
+
prior7d[metric2] = prior;
|
|
19940
|
+
deltaPct[metric2] = prior === 0 ? null : Math.round((recent - prior) / prior * 100);
|
|
19525
19941
|
}
|
|
19526
19942
|
return { recent7d, prior7d, deltaPct };
|
|
19527
19943
|
}
|
|
@@ -20009,7 +20425,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
|
20009
20425
|
}
|
|
20010
20426
|
|
|
20011
20427
|
// ../integration-google-analytics/src/ga4-client.ts
|
|
20012
|
-
import
|
|
20428
|
+
import crypto16 from "crypto";
|
|
20013
20429
|
|
|
20014
20430
|
// ../integration-google-analytics/src/constants.ts
|
|
20015
20431
|
var GA4_DATA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
|
|
@@ -20116,7 +20532,7 @@ function createServiceAccountJwt(clientEmail, privateKey, scope) {
|
|
|
20116
20532
|
const headerB64 = encode(header);
|
|
20117
20533
|
const payloadB64 = encode(payload);
|
|
20118
20534
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
20119
|
-
const sign =
|
|
20535
|
+
const sign = crypto16.createSign("RSA-SHA256");
|
|
20120
20536
|
sign.update(signingInput);
|
|
20121
20537
|
const signature = sign.sign(privateKey, "base64url");
|
|
20122
20538
|
return `${signingInput}.${signature}`;
|
|
@@ -20996,7 +21412,7 @@ async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
|
|
|
20996
21412
|
}
|
|
20997
21413
|
|
|
20998
21414
|
// ../integration-google-business-profile/src/lodging-client.ts
|
|
20999
|
-
import
|
|
21415
|
+
import crypto17 from "crypto";
|
|
21000
21416
|
async function getLodging(accessToken, locationName, opts = {}) {
|
|
21001
21417
|
const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
|
|
21002
21418
|
try {
|
|
@@ -21023,7 +21439,7 @@ function isPopulated(value) {
|
|
|
21023
21439
|
return true;
|
|
21024
21440
|
}
|
|
21025
21441
|
function hashLodging(lodging) {
|
|
21026
|
-
return
|
|
21442
|
+
return crypto17.createHash("sha256").update(stableStringify2(lodging)).digest("hex");
|
|
21027
21443
|
}
|
|
21028
21444
|
function stableStringify2(value) {
|
|
21029
21445
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -21034,7 +21450,7 @@ function stableStringify2(value) {
|
|
|
21034
21450
|
}
|
|
21035
21451
|
|
|
21036
21452
|
// ../integration-google-business-profile/src/attributes-client.ts
|
|
21037
|
-
import
|
|
21453
|
+
import crypto18 from "crypto";
|
|
21038
21454
|
async function getAttributes(accessToken, locationName, opts = {}) {
|
|
21039
21455
|
const url = `${GBP_BUSINESS_INFO_BASE}/${locationName}/attributes`;
|
|
21040
21456
|
let res;
|
|
@@ -21064,7 +21480,7 @@ function countAttributes(attrs) {
|
|
|
21064
21480
|
}
|
|
21065
21481
|
function hashAttributes(attrs) {
|
|
21066
21482
|
const sorted = [...attrs].sort((a, b) => a.name.localeCompare(b.name));
|
|
21067
|
-
return
|
|
21483
|
+
return crypto18.createHash("sha256").update(stableStringify3(sorted)).digest("hex");
|
|
21068
21484
|
}
|
|
21069
21485
|
function stableStringify3(value) {
|
|
21070
21486
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
@@ -21086,7 +21502,7 @@ function scopesForConnectionType(type) {
|
|
|
21086
21502
|
}
|
|
21087
21503
|
}
|
|
21088
21504
|
function signState(payload, secret) {
|
|
21089
|
-
return
|
|
21505
|
+
return crypto19.createHmac("sha256", secret).update(payload).digest("hex");
|
|
21090
21506
|
}
|
|
21091
21507
|
function buildSignedState(data, secret) {
|
|
21092
21508
|
const payload = JSON.stringify(data);
|
|
@@ -21097,7 +21513,7 @@ function verifySignedState(encoded, secret) {
|
|
|
21097
21513
|
try {
|
|
21098
21514
|
const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
|
|
21099
21515
|
const expected = signState(payload, secret);
|
|
21100
|
-
if (!
|
|
21516
|
+
if (!crypto19.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
|
|
21101
21517
|
return JSON.parse(payload);
|
|
21102
21518
|
} catch {
|
|
21103
21519
|
return null;
|
|
@@ -21314,7 +21730,7 @@ async function googleRoutes(app, opts) {
|
|
|
21314
21730
|
if (!projectId) {
|
|
21315
21731
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
21316
21732
|
}
|
|
21317
|
-
const project = app.db.select().from(projects).where(
|
|
21733
|
+
const project = app.db.select().from(projects).where(eq23(projects.id, projectId)).get();
|
|
21318
21734
|
if (!project) {
|
|
21319
21735
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
21320
21736
|
}
|
|
@@ -21433,7 +21849,7 @@ async function googleRoutes(app, opts) {
|
|
|
21433
21849
|
throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
21434
21850
|
}
|
|
21435
21851
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21436
|
-
const runId =
|
|
21852
|
+
const runId = crypto19.randomUUID();
|
|
21437
21853
|
app.db.insert(runs).values({
|
|
21438
21854
|
id: runId,
|
|
21439
21855
|
projectId: project.id,
|
|
@@ -21446,14 +21862,14 @@ async function googleRoutes(app, opts) {
|
|
|
21446
21862
|
if (opts.onGscSyncRequested) {
|
|
21447
21863
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
21448
21864
|
}
|
|
21449
|
-
const run = app.db.select().from(runs).where(
|
|
21865
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21450
21866
|
return run;
|
|
21451
21867
|
});
|
|
21452
21868
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
21453
21869
|
const project = resolveProject(app.db, request.params.name);
|
|
21454
21870
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
21455
21871
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
21456
|
-
const conditions = [
|
|
21872
|
+
const conditions = [eq23(gscSearchData.projectId, project.id)];
|
|
21457
21873
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21458
21874
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21459
21875
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21481,7 +21897,7 @@ async function googleRoutes(app, opts) {
|
|
|
21481
21897
|
const windowStart = startDate ?? cutoffDate ?? "";
|
|
21482
21898
|
const windowEnd = endDate ?? "9999-12-31";
|
|
21483
21899
|
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
21484
|
-
const conditions = [
|
|
21900
|
+
const conditions = [eq23(gscSearchData.projectId, project.id)];
|
|
21485
21901
|
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
21486
21902
|
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
21487
21903
|
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -21542,7 +21958,7 @@ async function googleRoutes(app, opts) {
|
|
|
21542
21958
|
const mob = ir.mobileUsabilityResult;
|
|
21543
21959
|
const rich = ir.richResultsResult;
|
|
21544
21960
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21545
|
-
const id =
|
|
21961
|
+
const id = crypto19.randomUUID();
|
|
21546
21962
|
app.db.insert(gscUrlInspections).values({
|
|
21547
21963
|
id,
|
|
21548
21964
|
projectId: project.id,
|
|
@@ -21580,8 +21996,8 @@ async function googleRoutes(app, opts) {
|
|
|
21580
21996
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
21581
21997
|
const project = resolveProject(app.db, request.params.name);
|
|
21582
21998
|
const { url, limit } = request.query;
|
|
21583
|
-
const conditions = [
|
|
21584
|
-
if (url) conditions.push(
|
|
21999
|
+
const conditions = [eq23(gscUrlInspections.projectId, project.id)];
|
|
22000
|
+
if (url) conditions.push(eq23(gscUrlInspections.url, url));
|
|
21585
22001
|
const rows = app.db.select().from(gscUrlInspections).where(and17(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
21586
22002
|
return rows.map((r) => ({
|
|
21587
22003
|
id: r.id,
|
|
@@ -21601,7 +22017,7 @@ async function googleRoutes(app, opts) {
|
|
|
21601
22017
|
});
|
|
21602
22018
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
21603
22019
|
const project = resolveProject(app.db, request.params.name);
|
|
21604
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22020
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21605
22021
|
const byUrl = /* @__PURE__ */ new Map();
|
|
21606
22022
|
for (const row of allInspections) {
|
|
21607
22023
|
const existing = byUrl.get(row.url);
|
|
@@ -21629,7 +22045,7 @@ async function googleRoutes(app, opts) {
|
|
|
21629
22045
|
});
|
|
21630
22046
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
21631
22047
|
const project = resolveProject(app.db, request.params.name);
|
|
21632
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22048
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21633
22049
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
21634
22050
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21635
22051
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -21678,7 +22094,7 @@ async function googleRoutes(app, opts) {
|
|
|
21678
22094
|
const total = latestByUrl.size;
|
|
21679
22095
|
const indexed = indexedUrls.length;
|
|
21680
22096
|
const notIndexed = notIndexedUrls.length;
|
|
21681
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
22097
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
21682
22098
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
21683
22099
|
const formatRow = (r) => ({
|
|
21684
22100
|
id: r.id,
|
|
@@ -21729,7 +22145,7 @@ async function googleRoutes(app, opts) {
|
|
|
21729
22145
|
const project = resolveProject(app.db, request.params.name);
|
|
21730
22146
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
21731
22147
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
21732
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
22148
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq23(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
21733
22149
|
return rows.map((r) => ({
|
|
21734
22150
|
date: r.date,
|
|
21735
22151
|
indexed: r.indexed,
|
|
@@ -21786,7 +22202,7 @@ async function googleRoutes(app, opts) {
|
|
|
21786
22202
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21787
22203
|
});
|
|
21788
22204
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21789
|
-
const runId =
|
|
22205
|
+
const runId = crypto19.randomUUID();
|
|
21790
22206
|
app.db.insert(runs).values({
|
|
21791
22207
|
id: runId,
|
|
21792
22208
|
projectId: project.id,
|
|
@@ -21798,7 +22214,7 @@ async function googleRoutes(app, opts) {
|
|
|
21798
22214
|
if (opts.onInspectSitemapRequested) {
|
|
21799
22215
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
21800
22216
|
}
|
|
21801
|
-
const run = app.db.select().from(runs).where(
|
|
22217
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21802
22218
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
21803
22219
|
});
|
|
21804
22220
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -21812,7 +22228,7 @@ async function googleRoutes(app, opts) {
|
|
|
21812
22228
|
throw validationError("No GSC property configured for this connection");
|
|
21813
22229
|
}
|
|
21814
22230
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21815
|
-
const runId =
|
|
22231
|
+
const runId = crypto19.randomUUID();
|
|
21816
22232
|
app.db.insert(runs).values({
|
|
21817
22233
|
id: runId,
|
|
21818
22234
|
projectId: project.id,
|
|
@@ -21825,7 +22241,7 @@ async function googleRoutes(app, opts) {
|
|
|
21825
22241
|
if (opts.onInspectSitemapRequested) {
|
|
21826
22242
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
21827
22243
|
}
|
|
21828
|
-
const run = app.db.select().from(runs).where(
|
|
22244
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21829
22245
|
return run;
|
|
21830
22246
|
});
|
|
21831
22247
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -21872,7 +22288,7 @@ async function googleRoutes(app, opts) {
|
|
|
21872
22288
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
21873
22289
|
let urlsToNotify = request.body?.urls ?? [];
|
|
21874
22290
|
if (request.body?.allUnindexed) {
|
|
21875
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
22291
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21876
22292
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21877
22293
|
for (const row of allInspections) {
|
|
21878
22294
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21990,7 +22406,7 @@ async function googleRoutes(app, opts) {
|
|
|
21990
22406
|
};
|
|
21991
22407
|
}
|
|
21992
22408
|
function listSelectionResponse(projectId) {
|
|
21993
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
22409
|
+
const rows = app.db.select().from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).all();
|
|
21994
22410
|
const dtos = rows.map(rowToDto2);
|
|
21995
22411
|
return {
|
|
21996
22412
|
locations: dtos,
|
|
@@ -21999,15 +22415,15 @@ async function googleRoutes(app, opts) {
|
|
|
21999
22415
|
};
|
|
22000
22416
|
}
|
|
22001
22417
|
function clearGbpProjectData(tx, projectId) {
|
|
22002
|
-
tx.delete(gbpDailyMetrics).where(
|
|
22003
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
22004
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
22005
|
-
tx.delete(gbpPlaceActions).where(
|
|
22006
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
22007
|
-
tx.delete(gbpLocations).where(
|
|
22418
|
+
tx.delete(gbpDailyMetrics).where(eq23(gbpDailyMetrics.projectId, projectId)).run();
|
|
22419
|
+
tx.delete(gbpKeywordImpressions).where(eq23(gbpKeywordImpressions.projectId, projectId)).run();
|
|
22420
|
+
tx.delete(gbpKeywordMonthly).where(eq23(gbpKeywordMonthly.projectId, projectId)).run();
|
|
22421
|
+
tx.delete(gbpPlaceActions).where(eq23(gbpPlaceActions.projectId, projectId)).run();
|
|
22422
|
+
tx.delete(gbpLodgingSnapshots).where(eq23(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
22423
|
+
tx.delete(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).run();
|
|
22008
22424
|
}
|
|
22009
22425
|
function currentProjectAccount(projectId) {
|
|
22010
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
22426
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq23(gbpLocations.projectId, projectId)).limit(1).get();
|
|
22011
22427
|
return row?.accountName ?? null;
|
|
22012
22428
|
}
|
|
22013
22429
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -22077,7 +22493,7 @@ async function googleRoutes(app, opts) {
|
|
|
22077
22493
|
app.db.transaction((tx) => {
|
|
22078
22494
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
22079
22495
|
for (const remote of remoteLocations) {
|
|
22080
|
-
const existing = tx.select().from(gbpLocations).where(and17(
|
|
22496
|
+
const existing = tx.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, remote.name))).get();
|
|
22081
22497
|
const profile = buildLocationProfileFields(remote);
|
|
22082
22498
|
if (existing) {
|
|
22083
22499
|
tx.update(gbpLocations).set({
|
|
@@ -22090,10 +22506,10 @@ async function googleRoutes(app, opts) {
|
|
|
22090
22506
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
22091
22507
|
...profile,
|
|
22092
22508
|
updatedAt: now
|
|
22093
|
-
}).where(
|
|
22509
|
+
}).where(eq23(gbpLocations.id, existing.id)).run();
|
|
22094
22510
|
} else {
|
|
22095
22511
|
tx.insert(gbpLocations).values({
|
|
22096
|
-
id:
|
|
22512
|
+
id: crypto19.randomUUID(),
|
|
22097
22513
|
projectId: project.id,
|
|
22098
22514
|
accountName,
|
|
22099
22515
|
locationName: remote.name,
|
|
@@ -22174,11 +22590,11 @@ async function googleRoutes(app, opts) {
|
|
|
22174
22590
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
22175
22591
|
}
|
|
22176
22592
|
const { selected } = parsed.data;
|
|
22177
|
-
const existing = app.db.select().from(gbpLocations).where(and17(
|
|
22593
|
+
const existing = app.db.select().from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.locationName, locationName))).get();
|
|
22178
22594
|
if (!existing) throw notFound("GBP location", locationName);
|
|
22179
22595
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22180
22596
|
app.db.transaction((tx) => {
|
|
22181
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
22597
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq23(gbpLocations.id, existing.id)).run();
|
|
22182
22598
|
writeAuditLog(tx, {
|
|
22183
22599
|
projectId: project.id,
|
|
22184
22600
|
actor: "api",
|
|
@@ -22187,7 +22603,7 @@ async function googleRoutes(app, opts) {
|
|
|
22187
22603
|
entityId: locationName
|
|
22188
22604
|
});
|
|
22189
22605
|
});
|
|
22190
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
22606
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq23(gbpLocations.id, existing.id)).get();
|
|
22191
22607
|
return rowToDto2(refreshed);
|
|
22192
22608
|
});
|
|
22193
22609
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -22217,7 +22633,7 @@ async function googleRoutes(app, opts) {
|
|
|
22217
22633
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
|
|
22218
22634
|
}
|
|
22219
22635
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22220
|
-
const runId =
|
|
22636
|
+
const runId = crypto19.randomUUID();
|
|
22221
22637
|
app.db.insert(runs).values({
|
|
22222
22638
|
id: runId,
|
|
22223
22639
|
projectId: project.id,
|
|
@@ -22231,9 +22647,9 @@ async function googleRoutes(app, opts) {
|
|
|
22231
22647
|
});
|
|
22232
22648
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
22233
22649
|
const project = resolveProject(app.db, request.params.name);
|
|
22234
|
-
const conditions = [
|
|
22235
|
-
if (request.query.locationName) conditions.push(
|
|
22236
|
-
if (request.query.metric) conditions.push(
|
|
22650
|
+
const conditions = [eq23(gbpDailyMetrics.projectId, project.id)];
|
|
22651
|
+
if (request.query.locationName) conditions.push(eq23(gbpDailyMetrics.locationName, request.query.locationName));
|
|
22652
|
+
if (request.query.metric) conditions.push(eq23(gbpDailyMetrics.metric, request.query.metric));
|
|
22237
22653
|
const rows = app.db.select().from(gbpDailyMetrics).where(and17(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
22238
22654
|
return {
|
|
22239
22655
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
@@ -22242,8 +22658,8 @@ async function googleRoutes(app, opts) {
|
|
|
22242
22658
|
});
|
|
22243
22659
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
22244
22660
|
const project = resolveProject(app.db, request.params.name);
|
|
22245
|
-
const conditions = [
|
|
22246
|
-
if (request.query.locationName) conditions.push(
|
|
22661
|
+
const conditions = [eq23(gbpKeywordImpressions.projectId, project.id)];
|
|
22662
|
+
if (request.query.locationName) conditions.push(eq23(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
22247
22663
|
const rows = app.db.select().from(gbpKeywordImpressions).where(and17(...conditions)).all();
|
|
22248
22664
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
22249
22665
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
@@ -22262,8 +22678,8 @@ async function googleRoutes(app, opts) {
|
|
|
22262
22678
|
});
|
|
22263
22679
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
22264
22680
|
const project = resolveProject(app.db, request.params.name);
|
|
22265
|
-
const conditions = [
|
|
22266
|
-
if (request.query.locationName) conditions.push(
|
|
22681
|
+
const conditions = [eq23(gbpPlaceActions.projectId, project.id)];
|
|
22682
|
+
if (request.query.locationName) conditions.push(eq23(gbpPlaceActions.locationName, request.query.locationName));
|
|
22267
22683
|
const rows = app.db.select().from(gbpPlaceActions).where(and17(...conditions)).all();
|
|
22268
22684
|
return {
|
|
22269
22685
|
placeActions: rows.map((r) => ({
|
|
@@ -22279,8 +22695,8 @@ async function googleRoutes(app, opts) {
|
|
|
22279
22695
|
});
|
|
22280
22696
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
22281
22697
|
const project = resolveProject(app.db, request.params.name);
|
|
22282
|
-
const conditions = [
|
|
22283
|
-
if (request.query.locationName) conditions.push(
|
|
22698
|
+
const conditions = [eq23(gbpLodgingSnapshots.projectId, project.id)];
|
|
22699
|
+
if (request.query.locationName) conditions.push(eq23(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
22284
22700
|
const rows = app.db.select().from(gbpLodgingSnapshots).where(and17(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22285
22701
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22286
22702
|
for (const row of rows) {
|
|
@@ -22296,8 +22712,8 @@ async function googleRoutes(app, opts) {
|
|
|
22296
22712
|
});
|
|
22297
22713
|
app.get("/projects/:name/gbp/attributes", async (request) => {
|
|
22298
22714
|
const project = resolveProject(app.db, request.params.name);
|
|
22299
|
-
const conditions = [
|
|
22300
|
-
if (request.query.locationName) conditions.push(
|
|
22715
|
+
const conditions = [eq23(gbpAttributesSnapshots.projectId, project.id)];
|
|
22716
|
+
if (request.query.locationName) conditions.push(eq23(gbpAttributesSnapshots.locationName, request.query.locationName));
|
|
22301
22717
|
const rows = app.db.select().from(gbpAttributesSnapshots).where(and17(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
|
|
22302
22718
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22303
22719
|
for (const row of rows) {
|
|
@@ -22316,8 +22732,8 @@ async function googleRoutes(app, opts) {
|
|
|
22316
22732
|
});
|
|
22317
22733
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
22318
22734
|
const project = resolveProject(app.db, request.params.name);
|
|
22319
|
-
const conditions = [
|
|
22320
|
-
if (request.query.locationName) conditions.push(
|
|
22735
|
+
const conditions = [eq23(gbpPlaceDetails.projectId, project.id)];
|
|
22736
|
+
if (request.query.locationName) conditions.push(eq23(gbpPlaceDetails.locationName, request.query.locationName));
|
|
22321
22737
|
const rows = app.db.select().from(gbpPlaceDetails).where(and17(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
22322
22738
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
22323
22739
|
for (const row of rows) {
|
|
@@ -22337,7 +22753,7 @@ async function googleRoutes(app, opts) {
|
|
|
22337
22753
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
22338
22754
|
const project = resolveProject(app.db, request.params.name);
|
|
22339
22755
|
const locationName = request.query.locationName ?? null;
|
|
22340
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(
|
|
22756
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), eq23(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
22341
22757
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
22342
22758
|
if (locationNames.length === 0) {
|
|
22343
22759
|
return buildGbpSummary({
|
|
@@ -22351,10 +22767,10 @@ async function googleRoutes(app, opts) {
|
|
|
22351
22767
|
locationProfiles: []
|
|
22352
22768
|
});
|
|
22353
22769
|
}
|
|
22354
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(
|
|
22355
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(
|
|
22356
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(
|
|
22357
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(
|
|
22770
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and17(eq23(gbpDailyMetrics.projectId, project.id), inArray11(gbpDailyMetrics.locationName, locationNames))).all();
|
|
22771
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and17(eq23(gbpKeywordImpressions.projectId, project.id), inArray11(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
22772
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and17(eq23(gbpPlaceActions.projectId, project.id), inArray11(gbpPlaceActions.locationName, locationNames))).all();
|
|
22773
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and17(eq23(gbpLodgingSnapshots.projectId, project.id), inArray11(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
22358
22774
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
22359
22775
|
for (const row of lodgingRows) {
|
|
22360
22776
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -22368,7 +22784,7 @@ async function googleRoutes(app, opts) {
|
|
|
22368
22784
|
regularHours: gbpLocations.regularHours,
|
|
22369
22785
|
primaryPhone: gbpLocations.primaryPhone,
|
|
22370
22786
|
openStatus: gbpLocations.openStatus
|
|
22371
|
-
}).from(gbpLocations).where(and17(
|
|
22787
|
+
}).from(gbpLocations).where(and17(eq23(gbpLocations.projectId, project.id), inArray11(gbpLocations.locationName, locationNames))).all();
|
|
22372
22788
|
return buildGbpSummary({
|
|
22373
22789
|
locationName,
|
|
22374
22790
|
locationCount: locationNames.length,
|
|
@@ -22383,8 +22799,8 @@ async function googleRoutes(app, opts) {
|
|
|
22383
22799
|
}
|
|
22384
22800
|
|
|
22385
22801
|
// ../api-routes/src/ads.ts
|
|
22386
|
-
import
|
|
22387
|
-
import { eq as
|
|
22802
|
+
import crypto20 from "crypto";
|
|
22803
|
+
import { eq as eq24, and as and18, asc as asc3, gte as gte3, lte as lte2, inArray as inArray12 } from "drizzle-orm";
|
|
22388
22804
|
function statusDto(row) {
|
|
22389
22805
|
if (!row) return { connected: false };
|
|
22390
22806
|
return {
|
|
@@ -22434,7 +22850,7 @@ async function adsRoutes(app, opts) {
|
|
|
22434
22850
|
createdAt: existingCfg?.createdAt ?? now,
|
|
22435
22851
|
updatedAt: now
|
|
22436
22852
|
});
|
|
22437
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
22853
|
+
const existingRow = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22438
22854
|
app.db.transaction((tx) => {
|
|
22439
22855
|
if (existingRow) {
|
|
22440
22856
|
tx.update(adsConnections).set({
|
|
@@ -22444,10 +22860,10 @@ async function adsRoutes(app, opts) {
|
|
|
22444
22860
|
timezone: account.timezone,
|
|
22445
22861
|
status: account.status,
|
|
22446
22862
|
updatedAt: now
|
|
22447
|
-
}).where(
|
|
22863
|
+
}).where(eq24(adsConnections.id, existingRow.id)).run();
|
|
22448
22864
|
} else {
|
|
22449
22865
|
tx.insert(adsConnections).values({
|
|
22450
|
-
id:
|
|
22866
|
+
id: crypto20.randomUUID(),
|
|
22451
22867
|
projectId: project.id,
|
|
22452
22868
|
adAccountId: account.id,
|
|
22453
22869
|
displayName: account.name,
|
|
@@ -22466,16 +22882,16 @@ async function adsRoutes(app, opts) {
|
|
|
22466
22882
|
entityId: account.id
|
|
22467
22883
|
}));
|
|
22468
22884
|
});
|
|
22469
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22885
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22470
22886
|
return statusDto(row);
|
|
22471
22887
|
}
|
|
22472
22888
|
);
|
|
22473
22889
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
22474
22890
|
const project = resolveProject(app.db, request.params.name);
|
|
22475
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22891
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22476
22892
|
if (row) {
|
|
22477
22893
|
app.db.transaction((tx) => {
|
|
22478
|
-
tx.delete(adsConnections).where(
|
|
22894
|
+
tx.delete(adsConnections).where(eq24(adsConnections.id, row.id)).run();
|
|
22479
22895
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
22480
22896
|
projectId: project.id,
|
|
22481
22897
|
actor: "api",
|
|
@@ -22491,25 +22907,25 @@ async function adsRoutes(app, opts) {
|
|
|
22491
22907
|
});
|
|
22492
22908
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
22493
22909
|
const project = resolveProject(app.db, request.params.name);
|
|
22494
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22910
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22495
22911
|
return statusDto(row);
|
|
22496
22912
|
});
|
|
22497
22913
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
22498
22914
|
const project = resolveProject(app.db, request.params.name);
|
|
22499
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22915
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22500
22916
|
if (!row) {
|
|
22501
22917
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
22502
22918
|
}
|
|
22503
22919
|
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and18(
|
|
22504
|
-
|
|
22505
|
-
|
|
22506
|
-
|
|
22920
|
+
eq24(runs.projectId, project.id),
|
|
22921
|
+
eq24(runs.kind, RunKinds["ads-sync"]),
|
|
22922
|
+
inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
22507
22923
|
)).get();
|
|
22508
22924
|
if (inFlight) {
|
|
22509
22925
|
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
22510
22926
|
return existing;
|
|
22511
22927
|
}
|
|
22512
|
-
const runId =
|
|
22928
|
+
const runId = crypto20.randomUUID();
|
|
22513
22929
|
app.db.insert(runs).values({
|
|
22514
22930
|
id: runId,
|
|
22515
22931
|
projectId: project.id,
|
|
@@ -22524,9 +22940,9 @@ async function adsRoutes(app, opts) {
|
|
|
22524
22940
|
});
|
|
22525
22941
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
22526
22942
|
const project = resolveProject(app.db, request.params.name);
|
|
22527
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
22528
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
22529
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
22943
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all();
|
|
22944
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all();
|
|
22945
|
+
const adRows = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all();
|
|
22530
22946
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
22531
22947
|
for (const ad of adRows) {
|
|
22532
22948
|
const dto = {
|
|
@@ -22580,9 +22996,9 @@ async function adsRoutes(app, opts) {
|
|
|
22580
22996
|
}
|
|
22581
22997
|
parsedLevel = result.data;
|
|
22582
22998
|
}
|
|
22583
|
-
const conditions = [
|
|
22584
|
-
if (parsedLevel) conditions.push(
|
|
22585
|
-
if (entityId) conditions.push(
|
|
22999
|
+
const conditions = [eq24(adsInsightsDaily.projectId, project.id)];
|
|
23000
|
+
if (parsedLevel) conditions.push(eq24(adsInsightsDaily.level, parsedLevel));
|
|
23001
|
+
if (entityId) conditions.push(eq24(adsInsightsDaily.entityId, entityId));
|
|
22586
23002
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
22587
23003
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
22588
23004
|
const rows = app.db.select().from(adsInsightsDaily).where(and18(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
|
|
@@ -22597,19 +23013,19 @@ async function adsRoutes(app, opts) {
|
|
|
22597
23013
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
22598
23014
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
22599
23015
|
}));
|
|
22600
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
23016
|
+
const conn = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
22601
23017
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
22602
23018
|
return response;
|
|
22603
23019
|
});
|
|
22604
23020
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
22605
23021
|
const project = resolveProject(app.db, request.params.name);
|
|
22606
|
-
const row = app.db.select().from(adsConnections).where(
|
|
22607
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
22608
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
22609
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
23022
|
+
const row = app.db.select().from(adsConnections).where(eq24(adsConnections.projectId, project.id)).get();
|
|
23023
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq24(adsCampaigns.projectId, project.id)).all().length;
|
|
23024
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq24(adsAdGroups.projectId, project.id)).all().length;
|
|
23025
|
+
const adCount = app.db.select().from(adsAds).where(eq24(adsAds.projectId, project.id)).all().length;
|
|
22610
23026
|
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and18(
|
|
22611
|
-
|
|
22612
|
-
|
|
23027
|
+
eq24(adsInsightsDaily.projectId, project.id),
|
|
23028
|
+
eq24(adsInsightsDaily.level, "campaign")
|
|
22613
23029
|
)).all();
|
|
22614
23030
|
let impressions = 0;
|
|
22615
23031
|
let clicks = 0;
|
|
@@ -22648,8 +23064,8 @@ async function adsRoutes(app, opts) {
|
|
|
22648
23064
|
}
|
|
22649
23065
|
|
|
22650
23066
|
// ../api-routes/src/bing.ts
|
|
22651
|
-
import
|
|
22652
|
-
import { eq as
|
|
23067
|
+
import crypto21 from "crypto";
|
|
23068
|
+
import { eq as eq25, and as and19, desc as desc12 } from "drizzle-orm";
|
|
22653
23069
|
|
|
22654
23070
|
// ../integration-bing/src/constants.ts
|
|
22655
23071
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -23023,7 +23439,7 @@ async function bingRoutes(app, opts) {
|
|
|
23023
23439
|
const store = requireConnectionStore();
|
|
23024
23440
|
const project = resolveProject(app.db, request.params.name);
|
|
23025
23441
|
requireConnection(store, project.canonicalDomain);
|
|
23026
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
23442
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23027
23443
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23028
23444
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
23029
23445
|
for (const row of allInspections) {
|
|
@@ -23080,7 +23496,7 @@ async function bingRoutes(app, opts) {
|
|
|
23080
23496
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
23081
23497
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23082
23498
|
app.db.insert(bingCoverageSnapshots).values({
|
|
23083
|
-
id:
|
|
23499
|
+
id: crypto21.randomUUID(),
|
|
23084
23500
|
projectId: project.id,
|
|
23085
23501
|
syncRunId: snapshotRunId,
|
|
23086
23502
|
date: snapshotDate,
|
|
@@ -23112,7 +23528,7 @@ async function bingRoutes(app, opts) {
|
|
|
23112
23528
|
const project = resolveProject(app.db, request.params.name);
|
|
23113
23529
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
23114
23530
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
23115
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
23531
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq25(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
23116
23532
|
return rows.map((r) => ({
|
|
23117
23533
|
date: r.date,
|
|
23118
23534
|
indexed: r.indexed,
|
|
@@ -23124,7 +23540,7 @@ async function bingRoutes(app, opts) {
|
|
|
23124
23540
|
requireConnectionStore();
|
|
23125
23541
|
const project = resolveProject(app.db, request.params.name);
|
|
23126
23542
|
const { url, limit } = request.query;
|
|
23127
|
-
const whereClause = url ? and19(
|
|
23543
|
+
const whereClause = url ? and19(eq25(bingUrlInspections.projectId, project.id), eq25(bingUrlInspections.url, url)) : eq25(bingUrlInspections.projectId, project.id);
|
|
23128
23544
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc12(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
23129
23545
|
return filtered.map((r) => ({
|
|
23130
23546
|
id: r.id,
|
|
@@ -23151,7 +23567,7 @@ async function bingRoutes(app, opts) {
|
|
|
23151
23567
|
throw validationError("url is required");
|
|
23152
23568
|
}
|
|
23153
23569
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23154
|
-
const runId =
|
|
23570
|
+
const runId = crypto21.randomUUID();
|
|
23155
23571
|
app.db.insert(runs).values({
|
|
23156
23572
|
id: runId,
|
|
23157
23573
|
projectId: project.id,
|
|
@@ -23172,7 +23588,7 @@ async function bingRoutes(app, opts) {
|
|
|
23172
23588
|
discoveryDate: result.DiscoveryDate ?? null
|
|
23173
23589
|
});
|
|
23174
23590
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23175
|
-
const id =
|
|
23591
|
+
const id = crypto21.randomUUID();
|
|
23176
23592
|
const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
|
|
23177
23593
|
const lastCrawledDate = parseBingDate(result.LastCrawledDate);
|
|
23178
23594
|
const inIndexDate = parseBingDate(result.InIndexDate);
|
|
@@ -23214,7 +23630,7 @@ async function bingRoutes(app, opts) {
|
|
|
23214
23630
|
anchorCount: result.AnchorCount ?? null,
|
|
23215
23631
|
discoveryDate
|
|
23216
23632
|
}).run();
|
|
23217
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
23633
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq25(runs.id, runId)).run();
|
|
23218
23634
|
return {
|
|
23219
23635
|
id,
|
|
23220
23636
|
url,
|
|
@@ -23230,7 +23646,7 @@ async function bingRoutes(app, opts) {
|
|
|
23230
23646
|
} catch (e) {
|
|
23231
23647
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23232
23648
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
23233
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
23649
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
23234
23650
|
throw e;
|
|
23235
23651
|
}
|
|
23236
23652
|
});
|
|
@@ -23242,7 +23658,7 @@ async function bingRoutes(app, opts) {
|
|
|
23242
23658
|
throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
|
|
23243
23659
|
}
|
|
23244
23660
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23245
|
-
const runId =
|
|
23661
|
+
const runId = crypto21.randomUUID();
|
|
23246
23662
|
app.db.insert(runs).values({
|
|
23247
23663
|
id: runId,
|
|
23248
23664
|
projectId: project.id,
|
|
@@ -23257,7 +23673,7 @@ async function bingRoutes(app, opts) {
|
|
|
23257
23673
|
} else {
|
|
23258
23674
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
23259
23675
|
}
|
|
23260
|
-
const run = app.db.select().from(runs).where(
|
|
23676
|
+
const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
|
|
23261
23677
|
return run;
|
|
23262
23678
|
});
|
|
23263
23679
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -23269,7 +23685,7 @@ async function bingRoutes(app, opts) {
|
|
|
23269
23685
|
}
|
|
23270
23686
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
23271
23687
|
if (request.body?.allUnindexed) {
|
|
23272
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
23688
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
23273
23689
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
23274
23690
|
for (const row of allInspections) {
|
|
23275
23691
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -23356,14 +23772,14 @@ async function bingRoutes(app, opts) {
|
|
|
23356
23772
|
import fs from "fs";
|
|
23357
23773
|
import path from "path";
|
|
23358
23774
|
import os from "os";
|
|
23359
|
-
import { eq as
|
|
23775
|
+
import { eq as eq26, and as and20 } from "drizzle-orm";
|
|
23360
23776
|
function getScreenshotDir() {
|
|
23361
23777
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
23362
23778
|
}
|
|
23363
23779
|
async function cdpRoutes(app, opts) {
|
|
23364
23780
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
23365
23781
|
const { snapshotId } = request.params;
|
|
23366
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs,
|
|
23782
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq26(querySnapshots.runId, runs.id)).where(eq26(querySnapshots.id, snapshotId)).get();
|
|
23367
23783
|
if (!snapshot?.screenshotPath) {
|
|
23368
23784
|
const err = notFound("Screenshot", snapshotId);
|
|
23369
23785
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23431,7 +23847,7 @@ async function cdpRoutes(app, opts) {
|
|
|
23431
23847
|
async (request, reply) => {
|
|
23432
23848
|
const project = resolveProject(app.db, request.params.name);
|
|
23433
23849
|
const { runId } = request.params;
|
|
23434
|
-
const run = app.db.select().from(runs).where(and20(
|
|
23850
|
+
const run = app.db.select().from(runs).where(and20(eq26(runs.id, runId), eq26(runs.projectId, project.id))).get();
|
|
23435
23851
|
if (!run) {
|
|
23436
23852
|
const err = notFound("Run", runId);
|
|
23437
23853
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -23444,8 +23860,8 @@ async function cdpRoutes(app, opts) {
|
|
|
23444
23860
|
citedDomains: querySnapshots.citedDomains,
|
|
23445
23861
|
screenshotPath: querySnapshots.screenshotPath,
|
|
23446
23862
|
rawResponse: querySnapshots.rawResponse
|
|
23447
|
-
}).from(querySnapshots).where(
|
|
23448
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
23863
|
+
}).from(querySnapshots).where(eq26(querySnapshots.runId, runId)).all());
|
|
23864
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq26(queries.projectId, project.id)).all();
|
|
23449
23865
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
23450
23866
|
const byQuery = /* @__PURE__ */ new Map();
|
|
23451
23867
|
for (const snap of snapshots) {
|
|
@@ -23527,8 +23943,8 @@ async function cdpRoutes(app, opts) {
|
|
|
23527
23943
|
}
|
|
23528
23944
|
|
|
23529
23945
|
// ../api-routes/src/ga.ts
|
|
23530
|
-
import
|
|
23531
|
-
import { eq as
|
|
23946
|
+
import crypto22 from "crypto";
|
|
23947
|
+
import { eq as eq27, desc as desc13, and as and21, sql as sql10 } from "drizzle-orm";
|
|
23532
23948
|
function gaLog(level, action, ctx) {
|
|
23533
23949
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
23534
23950
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -23805,10 +24221,10 @@ async function ga4Routes(app, opts) {
|
|
|
23805
24221
|
if (!saConn && !oauthConn) {
|
|
23806
24222
|
throw notFound("GA4 connection", project.name);
|
|
23807
24223
|
}
|
|
23808
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
23809
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
23810
|
-
app.db.delete(gaAiReferrals).where(
|
|
23811
|
-
app.db.delete(gaSocialReferrals).where(
|
|
24224
|
+
app.db.delete(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).run();
|
|
24225
|
+
app.db.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
|
|
24226
|
+
app.db.delete(gaAiReferrals).where(eq27(gaAiReferrals.projectId, project.id)).run();
|
|
24227
|
+
app.db.delete(gaSocialReferrals).where(eq27(gaSocialReferrals.projectId, project.id)).run();
|
|
23812
24228
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
23813
24229
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
23814
24230
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -23829,7 +24245,7 @@ async function ga4Routes(app, opts) {
|
|
|
23829
24245
|
if (!connected) {
|
|
23830
24246
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
23831
24247
|
}
|
|
23832
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
24248
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
23833
24249
|
return {
|
|
23834
24250
|
connected: true,
|
|
23835
24251
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -23853,7 +24269,7 @@ async function ga4Routes(app, opts) {
|
|
|
23853
24269
|
const syncAi = !only || only === "ai";
|
|
23854
24270
|
const syncSocial = !only || only === "social";
|
|
23855
24271
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
23856
|
-
const runId =
|
|
24272
|
+
const runId = crypto22.randomUUID();
|
|
23857
24273
|
app.db.insert(runs).values({
|
|
23858
24274
|
id: runId,
|
|
23859
24275
|
projectId: project.id,
|
|
@@ -23894,14 +24310,14 @@ async function ga4Routes(app, opts) {
|
|
|
23894
24310
|
if (syncTraffic) {
|
|
23895
24311
|
tx.delete(gaTrafficSnapshots).where(
|
|
23896
24312
|
and21(
|
|
23897
|
-
|
|
24313
|
+
eq27(gaTrafficSnapshots.projectId, project.id),
|
|
23898
24314
|
sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
23899
24315
|
sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
23900
24316
|
)
|
|
23901
24317
|
).run();
|
|
23902
24318
|
for (const row of rows) {
|
|
23903
24319
|
tx.insert(gaTrafficSnapshots).values({
|
|
23904
|
-
id:
|
|
24320
|
+
id: crypto22.randomUUID(),
|
|
23905
24321
|
projectId: project.id,
|
|
23906
24322
|
date: row.date,
|
|
23907
24323
|
landingPage: row.landingPage,
|
|
@@ -23918,14 +24334,14 @@ async function ga4Routes(app, opts) {
|
|
|
23918
24334
|
if (syncAi) {
|
|
23919
24335
|
tx.delete(gaAiReferrals).where(
|
|
23920
24336
|
and21(
|
|
23921
|
-
|
|
24337
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
23922
24338
|
sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
23923
24339
|
sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
23924
24340
|
)
|
|
23925
24341
|
).run();
|
|
23926
24342
|
for (const row of aiReferrals) {
|
|
23927
24343
|
tx.insert(gaAiReferrals).values({
|
|
23928
|
-
id:
|
|
24344
|
+
id: crypto22.randomUUID(),
|
|
23929
24345
|
projectId: project.id,
|
|
23930
24346
|
date: row.date,
|
|
23931
24347
|
source: row.source,
|
|
@@ -23950,14 +24366,14 @@ async function ga4Routes(app, opts) {
|
|
|
23950
24366
|
if (syncSocial) {
|
|
23951
24367
|
tx.delete(gaSocialReferrals).where(
|
|
23952
24368
|
and21(
|
|
23953
|
-
|
|
24369
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
23954
24370
|
sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
23955
24371
|
sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
23956
24372
|
)
|
|
23957
24373
|
).run();
|
|
23958
24374
|
for (const row of socialReferrals) {
|
|
23959
24375
|
tx.insert(gaSocialReferrals).values({
|
|
23960
|
-
id:
|
|
24376
|
+
id: crypto22.randomUUID(),
|
|
23961
24377
|
projectId: project.id,
|
|
23962
24378
|
date: row.date,
|
|
23963
24379
|
source: row.source,
|
|
@@ -23971,9 +24387,9 @@ async function ga4Routes(app, opts) {
|
|
|
23971
24387
|
}
|
|
23972
24388
|
}
|
|
23973
24389
|
if (syncSummary) {
|
|
23974
|
-
tx.delete(gaTrafficSummaries).where(
|
|
24390
|
+
tx.delete(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).run();
|
|
23975
24391
|
tx.insert(gaTrafficSummaries).values({
|
|
23976
|
-
id:
|
|
24392
|
+
id: crypto22.randomUUID(),
|
|
23977
24393
|
projectId: project.id,
|
|
23978
24394
|
periodStart: summary.periodStart,
|
|
23979
24395
|
periodEnd: summary.periodEnd,
|
|
@@ -23983,10 +24399,10 @@ async function ga4Routes(app, opts) {
|
|
|
23983
24399
|
syncedAt: now,
|
|
23984
24400
|
syncRunId: runId
|
|
23985
24401
|
}).run();
|
|
23986
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
24402
|
+
tx.delete(gaTrafficWindowSummaries).where(eq27(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
23987
24403
|
for (const ws of windowSummaries) {
|
|
23988
24404
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
23989
|
-
id:
|
|
24405
|
+
id: crypto22.randomUUID(),
|
|
23990
24406
|
projectId: project.id,
|
|
23991
24407
|
windowKey: ws.windowKey,
|
|
23992
24408
|
periodStart: ws.periodStart,
|
|
@@ -24001,7 +24417,7 @@ async function ga4Routes(app, opts) {
|
|
|
24001
24417
|
}
|
|
24002
24418
|
}
|
|
24003
24419
|
});
|
|
24004
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
24420
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq27(runs.id, runId)).run();
|
|
24005
24421
|
const syncedComponents = only ? [
|
|
24006
24422
|
...syncTraffic ? ["traffic"] : [],
|
|
24007
24423
|
...syncSummary ? ["summary"] : [],
|
|
@@ -24030,7 +24446,7 @@ async function ga4Routes(app, opts) {
|
|
|
24030
24446
|
} catch (e) {
|
|
24031
24447
|
const msg = e instanceof Error ? e.message : String(e);
|
|
24032
24448
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
24033
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
24449
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(runs.id, runId)).run();
|
|
24034
24450
|
throw e;
|
|
24035
24451
|
}
|
|
24036
24452
|
});
|
|
@@ -24041,11 +24457,11 @@ async function ga4Routes(app, opts) {
|
|
|
24041
24457
|
const window = parseWindow(request.query.window);
|
|
24042
24458
|
const cutoff = windowCutoff(window);
|
|
24043
24459
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
24044
|
-
const snapshotConditions = [
|
|
24460
|
+
const snapshotConditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
|
|
24045
24461
|
if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24046
|
-
const aiConditions = [
|
|
24462
|
+
const aiConditions = [eq27(gaAiReferrals.projectId, project.id)];
|
|
24047
24463
|
if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24048
|
-
const socialConditions = [
|
|
24464
|
+
const socialConditions = [eq27(gaSocialReferrals.projectId, project.id)];
|
|
24049
24465
|
if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24050
24466
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
24051
24467
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -24054,8 +24470,8 @@ async function ga4Routes(app, opts) {
|
|
|
24054
24470
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
24055
24471
|
}).from(gaTrafficWindowSummaries).where(
|
|
24056
24472
|
and21(
|
|
24057
|
-
|
|
24058
|
-
|
|
24473
|
+
eq27(gaTrafficWindowSummaries.projectId, project.id),
|
|
24474
|
+
eq27(gaTrafficWindowSummaries.windowKey, window)
|
|
24059
24475
|
)
|
|
24060
24476
|
).get() : null;
|
|
24061
24477
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
@@ -24067,14 +24483,14 @@ async function ga4Routes(app, opts) {
|
|
|
24067
24483
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
24068
24484
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
24069
24485
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
24070
|
-
}).from(gaTrafficSummaries).where(
|
|
24486
|
+
}).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
|
|
24071
24487
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
24072
24488
|
totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
24073
24489
|
}).from(gaTrafficSnapshots).where(and21(...snapshotConditions)).get();
|
|
24074
24490
|
const summaryMeta = app.db.select({
|
|
24075
24491
|
periodStart: gaTrafficSummaries.periodStart,
|
|
24076
24492
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
24077
|
-
}).from(gaTrafficSummaries).where(
|
|
24493
|
+
}).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).get();
|
|
24078
24494
|
const rows = app.db.select({
|
|
24079
24495
|
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
24080
24496
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
@@ -24135,7 +24551,7 @@ async function ga4Routes(app, opts) {
|
|
|
24135
24551
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
24136
24552
|
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
24137
24553
|
}).from(gaSocialReferrals).where(and21(...socialConditions)).get();
|
|
24138
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
24554
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq27(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
24139
24555
|
const total = summaryRow?.totalSessions ?? 0;
|
|
24140
24556
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
24141
24557
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -24233,7 +24649,7 @@ async function ga4Routes(app, opts) {
|
|
|
24233
24649
|
const project = resolveProject(app.db, request.params.name);
|
|
24234
24650
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24235
24651
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24236
|
-
const conditions = [
|
|
24652
|
+
const conditions = [eq27(gaAiReferrals.projectId, project.id)];
|
|
24237
24653
|
if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
24238
24654
|
const rows = app.db.select({
|
|
24239
24655
|
date: gaAiReferrals.date,
|
|
@@ -24258,7 +24674,7 @@ async function ga4Routes(app, opts) {
|
|
|
24258
24674
|
const project = resolveProject(app.db, request.params.name);
|
|
24259
24675
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24260
24676
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24261
|
-
const conditions = [
|
|
24677
|
+
const conditions = [eq27(gaSocialReferrals.projectId, project.id)];
|
|
24262
24678
|
if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
24263
24679
|
const rows = app.db.select({
|
|
24264
24680
|
date: gaSocialReferrals.date,
|
|
@@ -24281,7 +24697,7 @@ async function ga4Routes(app, opts) {
|
|
|
24281
24697
|
return fmt(d);
|
|
24282
24698
|
};
|
|
24283
24699
|
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
|
|
24284
|
-
|
|
24700
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24285
24701
|
sql10`${gaSocialReferrals.date} >= ${from}`,
|
|
24286
24702
|
sql10`${gaSocialReferrals.date} < ${to}`
|
|
24287
24703
|
)).get();
|
|
@@ -24294,7 +24710,7 @@ async function ga4Routes(app, opts) {
|
|
|
24294
24710
|
source: gaSocialReferrals.source,
|
|
24295
24711
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24296
24712
|
}).from(gaSocialReferrals).where(and21(
|
|
24297
|
-
|
|
24713
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24298
24714
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
24299
24715
|
sql10`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
24300
24716
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -24302,7 +24718,7 @@ async function ga4Routes(app, opts) {
|
|
|
24302
24718
|
source: gaSocialReferrals.source,
|
|
24303
24719
|
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
24304
24720
|
}).from(gaSocialReferrals).where(and21(
|
|
24305
|
-
|
|
24721
|
+
eq27(gaSocialReferrals.projectId, project.id),
|
|
24306
24722
|
sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
24307
24723
|
sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
24308
24724
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -24343,16 +24759,16 @@ async function ga4Routes(app, opts) {
|
|
|
24343
24759
|
return fmt(d);
|
|
24344
24760
|
};
|
|
24345
24761
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
24346
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24347
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24348
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(
|
|
24762
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24763
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24764
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and21(eq27(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
24349
24765
|
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24350
|
-
|
|
24766
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24351
24767
|
sql10`${gaAiReferrals.date} >= ${from}`,
|
|
24352
24768
|
sql10`${gaAiReferrals.date} < ${to}`,
|
|
24353
|
-
|
|
24769
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24354
24770
|
)).get();
|
|
24355
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(
|
|
24771
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
|
|
24356
24772
|
const todayStr = fmt(today);
|
|
24357
24773
|
const buildTrend = (sum) => {
|
|
24358
24774
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -24362,16 +24778,16 @@ async function ga4Routes(app, opts) {
|
|
|
24362
24778
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
24363
24779
|
};
|
|
24364
24780
|
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24365
|
-
|
|
24781
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24366
24782
|
sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
24367
24783
|
sql10`${gaAiReferrals.date} < ${todayStr}`,
|
|
24368
|
-
|
|
24784
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24369
24785
|
)).groupBy(gaAiReferrals.source).all();
|
|
24370
24786
|
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and21(
|
|
24371
|
-
|
|
24787
|
+
eq27(gaAiReferrals.projectId, project.id),
|
|
24372
24788
|
sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
24373
24789
|
sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
24374
|
-
|
|
24790
|
+
eq27(gaAiReferrals.sourceDimension, "session")
|
|
24375
24791
|
)).groupBy(gaAiReferrals.source).all();
|
|
24376
24792
|
const findBiggestMover = (current, prev) => {
|
|
24377
24793
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -24387,8 +24803,8 @@ async function ga4Routes(app, opts) {
|
|
|
24387
24803
|
}
|
|
24388
24804
|
return mover;
|
|
24389
24805
|
};
|
|
24390
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(
|
|
24391
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(
|
|
24806
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
24807
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and21(eq27(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
24392
24808
|
return {
|
|
24393
24809
|
total: buildTrend(sumTotal),
|
|
24394
24810
|
organic: buildTrend(sumOrganic),
|
|
@@ -24403,7 +24819,7 @@ async function ga4Routes(app, opts) {
|
|
|
24403
24819
|
const project = resolveProject(app.db, request.params.name);
|
|
24404
24820
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
24405
24821
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
24406
|
-
const conditions = [
|
|
24822
|
+
const conditions = [eq27(gaTrafficSnapshots.projectId, project.id)];
|
|
24407
24823
|
if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
24408
24824
|
const rows = app.db.select({
|
|
24409
24825
|
date: gaTrafficSnapshots.date,
|
|
@@ -24426,7 +24842,7 @@ async function ga4Routes(app, opts) {
|
|
|
24426
24842
|
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
24427
24843
|
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
24428
24844
|
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
24429
|
-
}).from(gaTrafficSnapshots).where(
|
|
24845
|
+
}).from(gaTrafficSnapshots).where(eq27(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
24430
24846
|
return {
|
|
24431
24847
|
pages: trafficPages.map((r) => ({
|
|
24432
24848
|
landingPage: r.landingPage,
|
|
@@ -24560,7 +24976,7 @@ function parseSchemaPageEntry(entry) {
|
|
|
24560
24976
|
}
|
|
24561
24977
|
|
|
24562
24978
|
// ../integration-wordpress/src/wordpress-client.ts
|
|
24563
|
-
import
|
|
24979
|
+
import crypto23 from "crypto";
|
|
24564
24980
|
function validateUsername(username) {
|
|
24565
24981
|
if (!username || typeof username !== "string" || username.trim().length === 0) {
|
|
24566
24982
|
throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
|
|
@@ -24773,7 +25189,7 @@ function buildSnippet(content) {
|
|
|
24773
25189
|
return `${text2.slice(0, 157)}...`;
|
|
24774
25190
|
}
|
|
24775
25191
|
function contentHash(content) {
|
|
24776
|
-
return
|
|
25192
|
+
return crypto23.createHash("sha256").update(content).digest("hex");
|
|
24777
25193
|
}
|
|
24778
25194
|
function buildAmbiguousSlugMessage(slug, pages) {
|
|
24779
25195
|
const candidates = pages.map((page) => {
|
|
@@ -26073,8 +26489,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
26073
26489
|
}
|
|
26074
26490
|
|
|
26075
26491
|
// ../api-routes/src/backlinks.ts
|
|
26076
|
-
import
|
|
26077
|
-
import { and as and23, asc as asc4, desc as desc14, eq as
|
|
26492
|
+
import crypto24 from "crypto";
|
|
26493
|
+
import { and as and23, asc as asc4, desc as desc14, eq as eq28, sql as sql11 } from "drizzle-orm";
|
|
26078
26494
|
|
|
26079
26495
|
// ../integration-commoncrawl/src/constants.ts
|
|
26080
26496
|
import os2 from "os";
|
|
@@ -26571,10 +26987,10 @@ function mapRunRow(row) {
|
|
|
26571
26987
|
}
|
|
26572
26988
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
26573
26989
|
const conditions = [
|
|
26574
|
-
|
|
26575
|
-
|
|
26990
|
+
eq28(backlinkSummaries.projectId, projectId),
|
|
26991
|
+
eq28(backlinkSummaries.source, source)
|
|
26576
26992
|
];
|
|
26577
|
-
if (release) conditions.push(
|
|
26993
|
+
if (release) conditions.push(eq28(backlinkSummaries.release, release));
|
|
26578
26994
|
return db.select().from(backlinkSummaries).where(and23(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
26579
26995
|
}
|
|
26580
26996
|
function parseExcludeCrawlers(value) {
|
|
@@ -26584,9 +27000,9 @@ function parseExcludeCrawlers(value) {
|
|
|
26584
27000
|
}
|
|
26585
27001
|
function computeFilteredSummary(db, base) {
|
|
26586
27002
|
const baseDomainCondition = and23(
|
|
26587
|
-
|
|
26588
|
-
|
|
26589
|
-
|
|
27003
|
+
eq28(backlinkDomains.projectId, base.projectId),
|
|
27004
|
+
eq28(backlinkDomains.source, base.source),
|
|
27005
|
+
eq28(backlinkDomains.release, base.release)
|
|
26590
27006
|
);
|
|
26591
27007
|
const filteredCondition = and23(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
26592
27008
|
const unfilteredAgg = db.select({
|
|
@@ -26618,13 +27034,13 @@ function computeFilteredSummary(db, base) {
|
|
|
26618
27034
|
};
|
|
26619
27035
|
}
|
|
26620
27036
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
26621
|
-
const summary = db.select().from(backlinkSummaries).where(and23(
|
|
27037
|
+
const summary = db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, projectId), eq28(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
26622
27038
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
26623
27039
|
if (summary && excludeCrawlers) {
|
|
26624
27040
|
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and23(
|
|
26625
|
-
|
|
26626
|
-
|
|
26627
|
-
|
|
27041
|
+
eq28(backlinkDomains.projectId, projectId),
|
|
27042
|
+
eq28(backlinkDomains.source, source),
|
|
27043
|
+
eq28(backlinkDomains.release, summary.release),
|
|
26628
27044
|
backlinkCrawlerExclusionClause()
|
|
26629
27045
|
)).get();
|
|
26630
27046
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -26639,7 +27055,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
26639
27055
|
};
|
|
26640
27056
|
}
|
|
26641
27057
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
26642
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
27058
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
26643
27059
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
26644
27060
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
26645
27061
|
const sources = [
|
|
@@ -26695,7 +27111,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26695
27111
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
26696
27112
|
);
|
|
26697
27113
|
}
|
|
26698
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
27114
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.release, release)).get();
|
|
26699
27115
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26700
27116
|
if (existing) {
|
|
26701
27117
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -26706,12 +27122,12 @@ async function backlinksRoutes(app, opts) {
|
|
|
26706
27122
|
phaseDetail: null,
|
|
26707
27123
|
error: null,
|
|
26708
27124
|
updatedAt: now
|
|
26709
|
-
}).where(
|
|
27125
|
+
}).where(eq28(ccReleaseSyncs.id, existing.id)).run();
|
|
26710
27126
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
26711
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
27127
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, existing.id)).get();
|
|
26712
27128
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
26713
27129
|
}
|
|
26714
|
-
const id =
|
|
27130
|
+
const id = crypto24.randomUUID();
|
|
26715
27131
|
app.db.insert(ccReleaseSyncs).values({
|
|
26716
27132
|
id,
|
|
26717
27133
|
release,
|
|
@@ -26720,7 +27136,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26720
27136
|
updatedAt: now
|
|
26721
27137
|
}).run();
|
|
26722
27138
|
opts.onReleaseSyncRequested(id, release);
|
|
26723
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
27139
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.id, id)).get();
|
|
26724
27140
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
26725
27141
|
});
|
|
26726
27142
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -26768,7 +27184,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26768
27184
|
throw validationError("Invalid release id");
|
|
26769
27185
|
}
|
|
26770
27186
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26771
|
-
const runId =
|
|
27187
|
+
const runId = crypto24.randomUUID();
|
|
26772
27188
|
app.db.insert(runs).values({
|
|
26773
27189
|
id: runId,
|
|
26774
27190
|
projectId: project.id,
|
|
@@ -26778,7 +27194,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26778
27194
|
createdAt: now
|
|
26779
27195
|
}).run();
|
|
26780
27196
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
26781
|
-
const run = app.db.select().from(runs).where(
|
|
27197
|
+
const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
26782
27198
|
return reply.status(201).send(mapRunRow(run));
|
|
26783
27199
|
});
|
|
26784
27200
|
app.get(
|
|
@@ -26805,9 +27221,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
26805
27221
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
26806
27222
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
26807
27223
|
const baseDomainCondition = and23(
|
|
26808
|
-
|
|
26809
|
-
|
|
26810
|
-
|
|
27224
|
+
eq28(backlinkDomains.projectId, project.id),
|
|
27225
|
+
eq28(backlinkDomains.source, source),
|
|
27226
|
+
eq28(backlinkDomains.release, targetRelease)
|
|
26811
27227
|
);
|
|
26812
27228
|
const domainCondition = excludeCrawlers ? and23(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
26813
27229
|
const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
@@ -26833,7 +27249,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26833
27249
|
async (request, reply) => {
|
|
26834
27250
|
const project = resolveProject(app.db, request.params.name);
|
|
26835
27251
|
const source = parseSourceParam(request.query.source);
|
|
26836
|
-
const rows = app.db.select().from(backlinkSummaries).where(and23(
|
|
27252
|
+
const rows = app.db.select().from(backlinkSummaries).where(and23(eq28(backlinkSummaries.projectId, project.id), eq28(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
|
|
26837
27253
|
const response = rows.map((r) => ({
|
|
26838
27254
|
release: r.release,
|
|
26839
27255
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -26870,7 +27286,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
26870
27286
|
);
|
|
26871
27287
|
}
|
|
26872
27288
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26873
|
-
const runId =
|
|
27289
|
+
const runId = crypto24.randomUUID();
|
|
26874
27290
|
app.db.insert(runs).values({
|
|
26875
27291
|
id: runId,
|
|
26876
27292
|
projectId: project.id,
|
|
@@ -26880,19 +27296,19 @@ async function backlinksRoutes(app, opts) {
|
|
|
26880
27296
|
createdAt: now
|
|
26881
27297
|
}).run();
|
|
26882
27298
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
26883
|
-
const run = app.db.select().from(runs).where(
|
|
27299
|
+
const run = app.db.select().from(runs).where(eq28(runs.id, runId)).get();
|
|
26884
27300
|
return reply.status(201).send(mapRunRow(run));
|
|
26885
27301
|
}
|
|
26886
27302
|
);
|
|
26887
27303
|
}
|
|
26888
27304
|
|
|
26889
27305
|
// ../api-routes/src/traffic.ts
|
|
26890
|
-
import
|
|
27306
|
+
import crypto26 from "crypto";
|
|
26891
27307
|
import { Agent as UndiciAgent } from "undici";
|
|
26892
|
-
import { and as and24, desc as desc15, eq as
|
|
27308
|
+
import { and as and24, desc as desc15, eq as eq29, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
|
|
26893
27309
|
|
|
26894
27310
|
// ../integration-cloud-run/src/auth.ts
|
|
26895
|
-
import
|
|
27311
|
+
import crypto25 from "crypto";
|
|
26896
27312
|
var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
26897
27313
|
var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
|
|
26898
27314
|
var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -26923,7 +27339,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
|
|
|
26923
27339
|
const headerB64 = encode(header);
|
|
26924
27340
|
const payloadB64 = encode(payload);
|
|
26925
27341
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
26926
|
-
const sign =
|
|
27342
|
+
const sign = crypto25.createSign("RSA-SHA256");
|
|
26927
27343
|
sign.update(signingInput);
|
|
26928
27344
|
const signature = sign.sign(privateKey, "base64url");
|
|
26929
27345
|
return `${signingInput}.${signature}`;
|
|
@@ -30756,8 +31172,8 @@ async function runBackfillTask(options) {
|
|
|
30756
31172
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30757
31173
|
try {
|
|
30758
31174
|
app.db.transaction((tx) => {
|
|
30759
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
30760
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
31175
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
|
|
31176
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
30761
31177
|
});
|
|
30762
31178
|
} catch {
|
|
30763
31179
|
}
|
|
@@ -30772,7 +31188,7 @@ async function runBackfillTask(options) {
|
|
|
30772
31188
|
if (allEvents.length === 0) {
|
|
30773
31189
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
30774
31190
|
try {
|
|
30775
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
31191
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq29(runs.id, runId)).run();
|
|
30776
31192
|
} catch {
|
|
30777
31193
|
}
|
|
30778
31194
|
return;
|
|
@@ -30795,28 +31211,28 @@ async function runBackfillTask(options) {
|
|
|
30795
31211
|
app.db.transaction((tx) => {
|
|
30796
31212
|
tx.delete(crawlerEventsHourly).where(
|
|
30797
31213
|
and24(
|
|
30798
|
-
|
|
31214
|
+
eq29(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
30799
31215
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
30800
31216
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
30801
31217
|
)
|
|
30802
31218
|
).run();
|
|
30803
31219
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
30804
31220
|
and24(
|
|
30805
|
-
|
|
31221
|
+
eq29(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
30806
31222
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
30807
31223
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
30808
31224
|
)
|
|
30809
31225
|
).run();
|
|
30810
31226
|
tx.delete(aiReferralEventsHourly).where(
|
|
30811
31227
|
and24(
|
|
30812
|
-
|
|
31228
|
+
eq29(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
30813
31229
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
30814
31230
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
30815
31231
|
)
|
|
30816
31232
|
).run();
|
|
30817
31233
|
tx.delete(rawEventSamples).where(
|
|
30818
31234
|
and24(
|
|
30819
|
-
|
|
31235
|
+
eq29(rawEventSamples.sourceId, sourceRow.id),
|
|
30820
31236
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
30821
31237
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
30822
31238
|
)
|
|
@@ -30883,7 +31299,7 @@ async function runBackfillTask(options) {
|
|
|
30883
31299
|
}
|
|
30884
31300
|
})();
|
|
30885
31301
|
tx.insert(rawEventSamples).values({
|
|
30886
|
-
id:
|
|
31302
|
+
id: crypto26.randomUUID(),
|
|
30887
31303
|
projectId: project.id,
|
|
30888
31304
|
sourceId: sourceRow.id,
|
|
30889
31305
|
ts: sample.observedAt,
|
|
@@ -30907,8 +31323,8 @@ async function runBackfillTask(options) {
|
|
|
30907
31323
|
lastError: null,
|
|
30908
31324
|
lastEventIds: newRingBuffer,
|
|
30909
31325
|
updatedAt: finishedAt
|
|
30910
|
-
}).where(
|
|
30911
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
31326
|
+
}).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
31327
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
|
|
30912
31328
|
});
|
|
30913
31329
|
} catch (e) {
|
|
30914
31330
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -30990,7 +31406,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30990
31406
|
createdAt: existing?.createdAt ?? now,
|
|
30991
31407
|
updatedAt: now
|
|
30992
31408
|
});
|
|
30993
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31409
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
30994
31410
|
const config = {
|
|
30995
31411
|
gcpProjectId,
|
|
30996
31412
|
serviceName: serviceName ?? null,
|
|
@@ -31006,10 +31422,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31006
31422
|
lastError: null,
|
|
31007
31423
|
configJson: config,
|
|
31008
31424
|
updatedAt: now
|
|
31009
|
-
}).where(
|
|
31010
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31425
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31426
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31011
31427
|
} else {
|
|
31012
|
-
const newId =
|
|
31428
|
+
const newId = crypto26.randomUUID();
|
|
31013
31429
|
app.db.insert(trafficSources).values({
|
|
31014
31430
|
id: newId,
|
|
31015
31431
|
projectId: project.id,
|
|
@@ -31024,7 +31440,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31024
31440
|
createdAt: now,
|
|
31025
31441
|
updatedAt: now
|
|
31026
31442
|
}).run();
|
|
31027
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31443
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31028
31444
|
}
|
|
31029
31445
|
writeAuditLog(app.db, {
|
|
31030
31446
|
projectId: project.id,
|
|
@@ -31076,7 +31492,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31076
31492
|
createdAt: existing?.createdAt ?? now,
|
|
31077
31493
|
updatedAt: now
|
|
31078
31494
|
});
|
|
31079
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31495
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
31080
31496
|
const config = { baseUrl, username };
|
|
31081
31497
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
31082
31498
|
let sourceRow;
|
|
@@ -31087,10 +31503,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31087
31503
|
lastError: null,
|
|
31088
31504
|
configJson: config,
|
|
31089
31505
|
updatedAt: now
|
|
31090
|
-
}).where(
|
|
31091
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31506
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31507
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31092
31508
|
} else {
|
|
31093
|
-
const newId =
|
|
31509
|
+
const newId = crypto26.randomUUID();
|
|
31094
31510
|
app.db.insert(trafficSources).values({
|
|
31095
31511
|
id: newId,
|
|
31096
31512
|
projectId: project.id,
|
|
@@ -31105,7 +31521,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31105
31521
|
createdAt: now,
|
|
31106
31522
|
updatedAt: now
|
|
31107
31523
|
}).run();
|
|
31108
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
31524
|
+
sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31109
31525
|
}
|
|
31110
31526
|
writeAuditLog(app.db, {
|
|
31111
31527
|
projectId: project.id,
|
|
@@ -31159,7 +31575,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31159
31575
|
createdAt: existing?.createdAt ?? now,
|
|
31160
31576
|
updatedAt: now
|
|
31161
31577
|
});
|
|
31162
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
31578
|
+
const activeSource = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
31163
31579
|
const config = { projectId, teamId, environment };
|
|
31164
31580
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
31165
31581
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -31171,10 +31587,10 @@ async function trafficRoutes(app, opts) {
|
|
|
31171
31587
|
lastError: null,
|
|
31172
31588
|
configJson: config,
|
|
31173
31589
|
updatedAt: now
|
|
31174
|
-
}).where(
|
|
31175
|
-
row = tx.select().from(trafficSources).where(
|
|
31590
|
+
}).where(eq29(trafficSources.id, activeSource.id)).run();
|
|
31591
|
+
row = tx.select().from(trafficSources).where(eq29(trafficSources.id, activeSource.id)).get();
|
|
31176
31592
|
} else {
|
|
31177
|
-
const newId =
|
|
31593
|
+
const newId = crypto26.randomUUID();
|
|
31178
31594
|
tx.insert(trafficSources).values({
|
|
31179
31595
|
id: newId,
|
|
31180
31596
|
projectId: project.id,
|
|
@@ -31197,18 +31613,18 @@ async function trafficRoutes(app, opts) {
|
|
|
31197
31613
|
createdAt: now,
|
|
31198
31614
|
updatedAt: now
|
|
31199
31615
|
}).run();
|
|
31200
|
-
row = tx.select().from(trafficSources).where(
|
|
31616
|
+
row = tx.select().from(trafficSources).where(eq29(trafficSources.id, newId)).get();
|
|
31201
31617
|
}
|
|
31202
31618
|
const existingSchedule = tx.select().from(schedules).where(
|
|
31203
31619
|
and24(
|
|
31204
|
-
|
|
31205
|
-
|
|
31620
|
+
eq29(schedules.projectId, project.id),
|
|
31621
|
+
eq29(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
31206
31622
|
)
|
|
31207
31623
|
).get();
|
|
31208
31624
|
let created = false;
|
|
31209
31625
|
if (!existingSchedule) {
|
|
31210
31626
|
tx.insert(schedules).values({
|
|
31211
|
-
id:
|
|
31627
|
+
id: crypto26.randomUUID(),
|
|
31212
31628
|
projectId: project.id,
|
|
31213
31629
|
kind: SchedulableRunKinds["traffic-sync"],
|
|
31214
31630
|
cronExpr: DEFAULT_TRAFFIC_SYNC_CRON,
|
|
@@ -31251,7 +31667,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31251
31667
|
});
|
|
31252
31668
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
31253
31669
|
const project = resolveProject(app.db, request.params.name);
|
|
31254
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31670
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
31255
31671
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
31256
31672
|
throw notFound("Traffic source", request.params.id);
|
|
31257
31673
|
}
|
|
@@ -31263,7 +31679,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31263
31679
|
const windowEnd = /* @__PURE__ */ new Date();
|
|
31264
31680
|
const startedAt = windowEnd.toISOString();
|
|
31265
31681
|
const syncStartedAtMs = windowEnd.getTime();
|
|
31266
|
-
const runId =
|
|
31682
|
+
const runId = crypto26.randomUUID();
|
|
31267
31683
|
app.db.insert(runs).values({
|
|
31268
31684
|
id: runId,
|
|
31269
31685
|
projectId: project.id,
|
|
@@ -31277,8 +31693,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31277
31693
|
const markFailed = (msg, errorCode) => {
|
|
31278
31694
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31279
31695
|
app.db.transaction((tx) => {
|
|
31280
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
31281
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
31696
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq29(runs.id, runId)).run();
|
|
31697
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
31282
31698
|
});
|
|
31283
31699
|
try {
|
|
31284
31700
|
opts.onTrafficSynced?.({
|
|
@@ -31359,7 +31775,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31359
31775
|
}
|
|
31360
31776
|
const credential = credentialStore.getConnection(project.name);
|
|
31361
31777
|
if (!credential) {
|
|
31362
|
-
app.db.delete(runs).where(
|
|
31778
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31363
31779
|
throw validationError(
|
|
31364
31780
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
31365
31781
|
);
|
|
@@ -31408,12 +31824,12 @@ async function trafficRoutes(app, opts) {
|
|
|
31408
31824
|
auditAction = "traffic.vercel.synced";
|
|
31409
31825
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
31410
31826
|
if (!credentialStore) {
|
|
31411
|
-
app.db.delete(runs).where(
|
|
31827
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31412
31828
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
31413
31829
|
}
|
|
31414
31830
|
const credential = credentialStore.getConnection(project.name);
|
|
31415
31831
|
if (!credential) {
|
|
31416
|
-
app.db.delete(runs).where(
|
|
31832
|
+
app.db.delete(runs).where(eq29(runs.id, runId)).run();
|
|
31417
31833
|
throw validationError(
|
|
31418
31834
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
31419
31835
|
);
|
|
@@ -31513,7 +31929,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31513
31929
|
let aiReferralHitsCount = 0;
|
|
31514
31930
|
let unknownHitsCount = 0;
|
|
31515
31931
|
app.db.transaction((tx) => {
|
|
31516
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
31932
|
+
const latestRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
|
|
31517
31933
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
31518
31934
|
const seenEventIds = new Set(previousIds);
|
|
31519
31935
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -31653,7 +32069,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31653
32069
|
}
|
|
31654
32070
|
})();
|
|
31655
32071
|
tx.insert(rawEventSamples).values({
|
|
31656
|
-
id:
|
|
32072
|
+
id: crypto26.randomUUID(),
|
|
31657
32073
|
projectId: project.id,
|
|
31658
32074
|
sourceId: sourceRow.id,
|
|
31659
32075
|
ts: sample.observedAt,
|
|
@@ -31688,8 +32104,8 @@ async function trafficRoutes(app, opts) {
|
|
|
31688
32104
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
31689
32105
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
31690
32106
|
}
|
|
31691
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
31692
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
32107
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
32108
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq29(runs.id, runId)).run();
|
|
31693
32109
|
});
|
|
31694
32110
|
writeAuditLog(app.db, {
|
|
31695
32111
|
projectId: project.id,
|
|
@@ -31741,7 +32157,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31741
32157
|
});
|
|
31742
32158
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
31743
32159
|
const project = resolveProject(app.db, request.params.name);
|
|
31744
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
32160
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
31745
32161
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
31746
32162
|
throw notFound("Traffic source", request.params.id);
|
|
31747
32163
|
}
|
|
@@ -31891,7 +32307,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31891
32307
|
};
|
|
31892
32308
|
}
|
|
31893
32309
|
const startedAt = windowEnd.toISOString();
|
|
31894
|
-
const runId =
|
|
32310
|
+
const runId = crypto26.randomUUID();
|
|
31895
32311
|
app.db.insert(runs).values({
|
|
31896
32312
|
id: runId,
|
|
31897
32313
|
projectId: project.id,
|
|
@@ -31930,7 +32346,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31930
32346
|
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
31931
32347
|
}).from(crawlerEventsHourly).where(
|
|
31932
32348
|
and24(
|
|
31933
|
-
|
|
32349
|
+
eq29(crawlerEventsHourly.sourceId, row.id),
|
|
31934
32350
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
31935
32351
|
)
|
|
31936
32352
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
@@ -31940,27 +32356,27 @@ async function trafficRoutes(app, opts) {
|
|
|
31940
32356
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
31941
32357
|
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
31942
32358
|
and24(
|
|
31943
|
-
|
|
32359
|
+
eq29(aiUserFetchEventsHourly.sourceId, row.id),
|
|
31944
32360
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
31945
32361
|
)
|
|
31946
32362
|
).get();
|
|
31947
32363
|
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31948
32364
|
and24(
|
|
31949
|
-
|
|
32365
|
+
eq29(aiReferralEventsHourly.sourceId, row.id),
|
|
31950
32366
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
31951
32367
|
)
|
|
31952
32368
|
).get();
|
|
31953
32369
|
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
31954
32370
|
and24(
|
|
31955
|
-
|
|
32371
|
+
eq29(rawEventSamples.sourceId, row.id),
|
|
31956
32372
|
gte4(rawEventSamples.ts, since)
|
|
31957
32373
|
)
|
|
31958
32374
|
).get();
|
|
31959
32375
|
const latestRun = app.db.select().from(runs).where(
|
|
31960
32376
|
and24(
|
|
31961
|
-
|
|
31962
|
-
|
|
31963
|
-
|
|
32377
|
+
eq29(runs.projectId, projectId),
|
|
32378
|
+
eq29(runs.kind, RunKinds["traffic-sync"]),
|
|
32379
|
+
eq29(runs.sourceId, row.id)
|
|
31964
32380
|
)
|
|
31965
32381
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
31966
32382
|
return {
|
|
@@ -31991,7 +32407,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31991
32407
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
31992
32408
|
);
|
|
31993
32409
|
}
|
|
31994
|
-
const sourceRow = app.db.select().from(trafficSources).where(and24(
|
|
32410
|
+
const sourceRow = app.db.select().from(trafficSources).where(and24(eq29(trafficSources.projectId, project.id), eq29(trafficSources.id, request.params.id))).get();
|
|
31995
32411
|
if (!sourceRow) {
|
|
31996
32412
|
throw notFound("traffic source", request.params.id);
|
|
31997
32413
|
}
|
|
@@ -32008,7 +32424,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32008
32424
|
status: TrafficSourceStatuses.connected,
|
|
32009
32425
|
lastError: null,
|
|
32010
32426
|
updatedAt: now
|
|
32011
|
-
}).where(
|
|
32427
|
+
}).where(eq29(trafficSources.id, sourceRow.id)).run();
|
|
32012
32428
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
32013
32429
|
projectId: project.id,
|
|
32014
32430
|
actor: "api",
|
|
@@ -32016,20 +32432,20 @@ async function trafficRoutes(app, opts) {
|
|
|
32016
32432
|
entityType: "traffic_source",
|
|
32017
32433
|
entityId: sourceRow.id
|
|
32018
32434
|
}));
|
|
32019
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
32435
|
+
updatedRow = tx.select().from(trafficSources).where(eq29(trafficSources.id, sourceRow.id)).get();
|
|
32020
32436
|
});
|
|
32021
32437
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
32022
32438
|
});
|
|
32023
32439
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
32024
32440
|
const project = resolveProject(app.db, request.params.name);
|
|
32025
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
32441
|
+
const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32026
32442
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
32027
32443
|
const response = { sources };
|
|
32028
32444
|
return response;
|
|
32029
32445
|
});
|
|
32030
32446
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
32031
32447
|
const project = resolveProject(app.db, request.params.name);
|
|
32032
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
32448
|
+
const rows = app.db.select().from(trafficSources).where(eq29(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
32033
32449
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
32034
32450
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
32035
32451
|
const response = { sources };
|
|
@@ -32039,7 +32455,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32039
32455
|
"/projects/:name/traffic/sources/:id",
|
|
32040
32456
|
async (request) => {
|
|
32041
32457
|
const project = resolveProject(app.db, request.params.name);
|
|
32042
|
-
const row = app.db.select().from(trafficSources).where(
|
|
32458
|
+
const row = app.db.select().from(trafficSources).where(eq29(trafficSources.id, request.params.id)).get();
|
|
32043
32459
|
if (!row || row.projectId !== project.id) {
|
|
32044
32460
|
throw notFound("Traffic source", request.params.id);
|
|
32045
32461
|
}
|
|
@@ -32091,11 +32507,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32091
32507
|
let aiReferralCounts = aiReferralClassCounts(0, 0, 0);
|
|
32092
32508
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
32093
32509
|
const crawlerFilters = [
|
|
32094
|
-
|
|
32510
|
+
eq29(crawlerEventsHourly.projectId, project.id),
|
|
32095
32511
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
32096
32512
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
32097
32513
|
];
|
|
32098
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
32514
|
+
if (sourceIdParam) crawlerFilters.push(eq29(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
32099
32515
|
const crawlerWhere = and24(...crawlerFilters);
|
|
32100
32516
|
const pathTotals = app.db.select({
|
|
32101
32517
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
@@ -32123,11 +32539,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32123
32539
|
}
|
|
32124
32540
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
32125
32541
|
const userFetchFilters = [
|
|
32126
|
-
|
|
32542
|
+
eq29(aiUserFetchEventsHourly.projectId, project.id),
|
|
32127
32543
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
32128
32544
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
32129
32545
|
];
|
|
32130
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
32546
|
+
if (sourceIdParam) userFetchFilters.push(eq29(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
32131
32547
|
const userFetchWhere = and24(...userFetchFilters);
|
|
32132
32548
|
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
32133
32549
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
@@ -32148,11 +32564,11 @@ async function trafficRoutes(app, opts) {
|
|
|
32148
32564
|
}
|
|
32149
32565
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
32150
32566
|
const aiFilters = [
|
|
32151
|
-
|
|
32567
|
+
eq29(aiReferralEventsHourly.projectId, project.id),
|
|
32152
32568
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
32153
32569
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
32154
32570
|
];
|
|
32155
|
-
if (sourceIdParam) aiFilters.push(
|
|
32571
|
+
if (sourceIdParam) aiFilters.push(eq29(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
32156
32572
|
const aiWhere = and24(...aiFilters);
|
|
32157
32573
|
const total = app.db.select({
|
|
32158
32574
|
total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
@@ -32207,7 +32623,7 @@ async function trafficRoutes(app, opts) {
|
|
|
32207
32623
|
}
|
|
32208
32624
|
|
|
32209
32625
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
32210
|
-
import
|
|
32626
|
+
import crypto27 from "crypto";
|
|
32211
32627
|
import fs6 from "fs";
|
|
32212
32628
|
import path7 from "path";
|
|
32213
32629
|
var REQUIRED_SKILLS = ["canonry", "aero"];
|
|
@@ -32360,7 +32776,7 @@ function isInstalled(dir) {
|
|
|
32360
32776
|
}
|
|
32361
32777
|
function hashInstalledFile(filePath) {
|
|
32362
32778
|
try {
|
|
32363
|
-
return
|
|
32779
|
+
return crypto27.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
|
|
32364
32780
|
} catch {
|
|
32365
32781
|
return void 0;
|
|
32366
32782
|
}
|
|
@@ -32375,7 +32791,7 @@ function readInstalledManifest(skillDir) {
|
|
|
32375
32791
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
32376
32792
|
|
|
32377
32793
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
32378
|
-
import { and as and25, eq as
|
|
32794
|
+
import { and as and25, eq as eq30 } from "drizzle-orm";
|
|
32379
32795
|
function skippedNoProject() {
|
|
32380
32796
|
return {
|
|
32381
32797
|
status: CheckStatuses.skipped,
|
|
@@ -32391,8 +32807,8 @@ var BACKLINKS_CHECKS = [
|
|
|
32391
32807
|
title: "Backlinks source connected",
|
|
32392
32808
|
run: (ctx) => {
|
|
32393
32809
|
if (!ctx.project) return skippedNoProject();
|
|
32394
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
32395
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
32810
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq30(projects.id, ctx.project.id)).get();
|
|
32811
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
32396
32812
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
32397
32813
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
32398
32814
|
const connected = [];
|
|
@@ -32408,8 +32824,8 @@ var BACKLINKS_CHECKS = [
|
|
|
32408
32824
|
};
|
|
32409
32825
|
}
|
|
32410
32826
|
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and25(
|
|
32411
|
-
|
|
32412
|
-
|
|
32827
|
+
eq30(backlinkSummaries.projectId, ctx.project.id),
|
|
32828
|
+
eq30(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
32413
32829
|
)).limit(1).get() : false;
|
|
32414
32830
|
return {
|
|
32415
32831
|
status: CheckStatuses.ok,
|
|
@@ -32569,7 +32985,7 @@ var BING_AUTH_CHECKS = [
|
|
|
32569
32985
|
];
|
|
32570
32986
|
|
|
32571
32987
|
// ../api-routes/src/doctor/checks/content.ts
|
|
32572
|
-
import { eq as
|
|
32988
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
32573
32989
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
32574
32990
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
32575
32991
|
function skippedNoProject2() {
|
|
@@ -32582,7 +32998,7 @@ function skippedNoProject2() {
|
|
|
32582
32998
|
}
|
|
32583
32999
|
function loadProject(ctx) {
|
|
32584
33000
|
if (!ctx.project) return null;
|
|
32585
|
-
return ctx.db.select().from(projects).where(
|
|
33001
|
+
return ctx.db.select().from(projects).where(eq31(projects.id, ctx.project.id)).get() ?? null;
|
|
32586
33002
|
}
|
|
32587
33003
|
function percent(value) {
|
|
32588
33004
|
return Math.round(value * 100);
|
|
@@ -32674,7 +33090,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
32674
33090
|
);
|
|
32675
33091
|
|
|
32676
33092
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
32677
|
-
import { eq as
|
|
33093
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
32678
33094
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
32679
33095
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
32680
33096
|
var adsConnectionCheck = {
|
|
@@ -32691,7 +33107,7 @@ var adsConnectionCheck = {
|
|
|
32691
33107
|
remediation: null
|
|
32692
33108
|
};
|
|
32693
33109
|
}
|
|
32694
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
33110
|
+
const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
|
|
32695
33111
|
if (!row) {
|
|
32696
33112
|
return {
|
|
32697
33113
|
status: CheckStatuses.skipped,
|
|
@@ -32741,7 +33157,7 @@ var adsRecentSyncCheck = {
|
|
|
32741
33157
|
remediation: null
|
|
32742
33158
|
};
|
|
32743
33159
|
}
|
|
32744
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
33160
|
+
const row = ctx.db.select().from(adsConnections).where(eq32(adsConnections.projectId, ctx.project.id)).get();
|
|
32745
33161
|
if (!row) {
|
|
32746
33162
|
return {
|
|
32747
33163
|
status: CheckStatuses.skipped,
|
|
@@ -32932,7 +33348,7 @@ var ga4ConnectionCheck = {
|
|
|
32932
33348
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
32933
33349
|
|
|
32934
33350
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
32935
|
-
import { and as and26, eq as
|
|
33351
|
+
import { and as and26, eq as eq33 } from "drizzle-orm";
|
|
32936
33352
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
32937
33353
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
32938
33354
|
function skippedNoProject3() {
|
|
@@ -33165,7 +33581,7 @@ var recentSyncCheck = {
|
|
|
33165
33581
|
title: "GBP recent sync",
|
|
33166
33582
|
run: (ctx) => {
|
|
33167
33583
|
if (!ctx.project) return skippedNoProject3();
|
|
33168
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(
|
|
33584
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and26(eq33(gbpLocations.projectId, ctx.project.id), eq33(gbpLocations.selected, true))).all();
|
|
33169
33585
|
if (selected.length === 0) {
|
|
33170
33586
|
return {
|
|
33171
33587
|
status: CheckStatuses.skipped,
|
|
@@ -33225,7 +33641,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
33225
33641
|
);
|
|
33226
33642
|
|
|
33227
33643
|
// ../api-routes/src/doctor/checks/places.ts
|
|
33228
|
-
import { eq as
|
|
33644
|
+
import { eq as eq34 } from "drizzle-orm";
|
|
33229
33645
|
var apiKeyCheck = {
|
|
33230
33646
|
id: "gbp.places.api-key",
|
|
33231
33647
|
category: CheckCategories.auth,
|
|
@@ -33270,7 +33686,7 @@ var apiKeyCheck = {
|
|
|
33270
33686
|
details: { tier: cfg.tier }
|
|
33271
33687
|
};
|
|
33272
33688
|
}
|
|
33273
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
33689
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq34(gbpLocations.projectId, ctx.project.id)).all();
|
|
33274
33690
|
const selected = rows.filter((r) => r.selected);
|
|
33275
33691
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
33276
33692
|
const details = {
|
|
@@ -33767,7 +34183,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
33767
34183
|
];
|
|
33768
34184
|
|
|
33769
34185
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
33770
|
-
import { and as and27, eq as
|
|
34186
|
+
import { and as and27, eq as eq35, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
33771
34187
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
33772
34188
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
33773
34189
|
function skippedNoProject5() {
|
|
@@ -33782,7 +34198,7 @@ function loadProbes(ctx) {
|
|
|
33782
34198
|
if (!ctx.project) return [];
|
|
33783
34199
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
33784
34200
|
and27(
|
|
33785
|
-
|
|
34201
|
+
eq35(trafficSources.projectId, ctx.project.id),
|
|
33786
34202
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
33787
34203
|
)
|
|
33788
34204
|
).all();
|
|
@@ -33863,7 +34279,7 @@ var recentDataCheck = {
|
|
|
33863
34279
|
const recentCrawlers = Number(
|
|
33864
34280
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33865
34281
|
and27(
|
|
33866
|
-
|
|
34282
|
+
eq35(crawlerEventsHourly.projectId, ctx.project.id),
|
|
33867
34283
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
33868
34284
|
)
|
|
33869
34285
|
).get()?.total ?? 0
|
|
@@ -33871,7 +34287,7 @@ var recentDataCheck = {
|
|
|
33871
34287
|
const recentReferrals = Number(
|
|
33872
34288
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33873
34289
|
and27(
|
|
33874
|
-
|
|
34290
|
+
eq35(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
33875
34291
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
33876
34292
|
)
|
|
33877
34293
|
).get()?.total ?? 0
|
|
@@ -33887,7 +34303,7 @@ var recentDataCheck = {
|
|
|
33887
34303
|
const olderCrawlers = Number(
|
|
33888
34304
|
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33889
34305
|
and27(
|
|
33890
|
-
|
|
34306
|
+
eq35(crawlerEventsHourly.projectId, ctx.project.id),
|
|
33891
34307
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
33892
34308
|
)
|
|
33893
34309
|
).get()?.total ?? 0
|
|
@@ -33895,7 +34311,7 @@ var recentDataCheck = {
|
|
|
33895
34311
|
const olderReferrals = Number(
|
|
33896
34312
|
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33897
34313
|
and27(
|
|
33898
|
-
|
|
34314
|
+
eq35(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
33899
34315
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
33900
34316
|
)
|
|
33901
34317
|
).get()?.total ?? 0
|
|
@@ -34310,8 +34726,8 @@ async function doctorRoutes(app, opts) {
|
|
|
34310
34726
|
}
|
|
34311
34727
|
|
|
34312
34728
|
// ../api-routes/src/discovery/routes.ts
|
|
34313
|
-
import
|
|
34314
|
-
import { and as and28, desc as desc16, eq as
|
|
34729
|
+
import crypto28 from "crypto";
|
|
34730
|
+
import { and as and28, desc as desc16, eq as eq36, gte as gte6, inArray as inArray13, isNull, or as or5 } from "drizzle-orm";
|
|
34315
34731
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
34316
34732
|
async function discoveryRoutes(app, opts) {
|
|
34317
34733
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -34346,12 +34762,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34346
34762
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
34347
34763
|
const decision = app.db.transaction((tx) => {
|
|
34348
34764
|
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and28(
|
|
34349
|
-
|
|
34350
|
-
|
|
34765
|
+
eq36(discoverySessions.projectId, project.id),
|
|
34766
|
+
eq36(discoverySessions.icpDescription, icpDescription),
|
|
34351
34767
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
34352
34768
|
// semantics, so a request with a different (or no) buyer must start
|
|
34353
34769
|
// its own session, never adopt another buyer's probes.
|
|
34354
|
-
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) :
|
|
34770
|
+
parsed.data.buyerDescription == null ? isNull(discoverySessions.buyerDescription) : eq36(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
34355
34771
|
// Locations are identity too: a different service-area subset seeds
|
|
34356
34772
|
// and probes a different geo, so it must never reuse another geo's
|
|
34357
34773
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -34361,12 +34777,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34361
34777
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
34362
34778
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
34363
34779
|
// upgrade, never a wrong reuse.
|
|
34364
|
-
locations.length === 0 ? or5(isNull(discoverySessions.locations),
|
|
34780
|
+
locations.length === 0 ? or5(isNull(discoverySessions.locations), eq36(discoverySessions.locations, locations)) : eq36(discoverySessions.locations, locations),
|
|
34365
34781
|
// Seed provider set is identity: a different phrasing distribution
|
|
34366
34782
|
// must never reuse another set's session. Null (= Gemini-only
|
|
34367
34783
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
34368
|
-
seedProviders == null ? isNull(discoverySessions.seedProviders) :
|
|
34369
|
-
|
|
34784
|
+
seedProviders == null ? isNull(discoverySessions.seedProviders) : eq36(discoverySessions.seedProviders, seedProviders),
|
|
34785
|
+
inArray13(discoverySessions.status, [
|
|
34370
34786
|
DiscoverySessionStatuses.queued,
|
|
34371
34787
|
DiscoverySessionStatuses.seeding,
|
|
34372
34788
|
DiscoverySessionStatuses.probing
|
|
@@ -34376,8 +34792,8 @@ async function discoveryRoutes(app, opts) {
|
|
|
34376
34792
|
if (existing && existing.runId) {
|
|
34377
34793
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
34378
34794
|
}
|
|
34379
|
-
const sessionId =
|
|
34380
|
-
const runId =
|
|
34795
|
+
const sessionId = crypto28.randomUUID();
|
|
34796
|
+
const runId = crypto28.randomUUID();
|
|
34381
34797
|
tx.insert(discoverySessions).values({
|
|
34382
34798
|
id: sessionId,
|
|
34383
34799
|
projectId: project.id,
|
|
@@ -34441,7 +34857,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34441
34857
|
const project = resolveProject(app.db, request.params.name);
|
|
34442
34858
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
34443
34859
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
34444
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
34860
|
+
const rows = app.db.select().from(discoverySessions).where(eq36(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
34445
34861
|
return reply.send(rows.map(serializeSession));
|
|
34446
34862
|
}
|
|
34447
34863
|
);
|
|
@@ -34449,11 +34865,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
34449
34865
|
"/projects/:name/discover/sessions/:id",
|
|
34450
34866
|
async (request, reply) => {
|
|
34451
34867
|
const project = resolveProject(app.db, request.params.name);
|
|
34452
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34868
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34453
34869
|
if (!session || session.projectId !== project.id) {
|
|
34454
34870
|
throw notFound("Discovery session", request.params.id);
|
|
34455
34871
|
}
|
|
34456
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34872
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34457
34873
|
const detail = {
|
|
34458
34874
|
...serializeSession(session),
|
|
34459
34875
|
probes: probeRows.map(serializeProbe)
|
|
@@ -34465,7 +34881,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34465
34881
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
34466
34882
|
async (request, reply) => {
|
|
34467
34883
|
const project = resolveProject(app.db, request.params.name);
|
|
34468
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34884
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34469
34885
|
if (!session || session.projectId !== project.id) {
|
|
34470
34886
|
throw notFound("Discovery session", request.params.id);
|
|
34471
34887
|
}
|
|
@@ -34473,7 +34889,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34473
34889
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
34474
34890
|
const applyAnchor = request.query.anchor !== "false";
|
|
34475
34891
|
const provider = session.seedProvider ?? "gemini";
|
|
34476
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34892
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34477
34893
|
const extract = opts.harvestSearchQueries;
|
|
34478
34894
|
const probesWithQueries = probeRows.map((row) => {
|
|
34479
34895
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -34484,7 +34900,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34484
34900
|
return { searchQueries: [] };
|
|
34485
34901
|
}
|
|
34486
34902
|
});
|
|
34487
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
34903
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query);
|
|
34488
34904
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
34489
34905
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
34490
34906
|
effectiveDomains(project)
|
|
@@ -34531,12 +34947,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
34531
34947
|
"/projects/:name/discover/sessions/:id/promote",
|
|
34532
34948
|
async (request, reply) => {
|
|
34533
34949
|
const project = resolveProject(app.db, request.params.name);
|
|
34534
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34950
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34535
34951
|
if (!session || session.projectId !== project.id) {
|
|
34536
34952
|
throw notFound("Discovery session", request.params.id);
|
|
34537
34953
|
}
|
|
34538
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
34539
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34954
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34955
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
34540
34956
|
const seenCompetitors = new Set(existingCompetitors);
|
|
34541
34957
|
const cited = /* @__PURE__ */ new Set();
|
|
34542
34958
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -34565,7 +34981,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34565
34981
|
);
|
|
34566
34982
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
34567
34983
|
const project = resolveProject(app.db, request.params.name);
|
|
34568
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
34984
|
+
const session = app.db.select().from(discoverySessions).where(eq36(discoverySessions.id, request.params.id)).get();
|
|
34569
34985
|
if (!session || session.projectId !== project.id) {
|
|
34570
34986
|
throw notFound("Discovery session", request.params.id);
|
|
34571
34987
|
}
|
|
@@ -34588,7 +35004,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34588
35004
|
const bucketSet = new Set(buckets);
|
|
34589
35005
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
34590
35006
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
34591
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
35007
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq36(discoveryProbes.sessionId, session.id)).all();
|
|
34592
35008
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
34593
35009
|
for (const probe of probeRows) {
|
|
34594
35010
|
if (!probe.bucket) continue;
|
|
@@ -34596,7 +35012,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34596
35012
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
34597
35013
|
}
|
|
34598
35014
|
const existingQueries = new Set(
|
|
34599
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
35015
|
+
app.db.select({ query: queries.query }).from(queries).where(eq36(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
34600
35016
|
);
|
|
34601
35017
|
const promotedQueries = [];
|
|
34602
35018
|
const skippedQueries = [];
|
|
@@ -34612,7 +35028,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34612
35028
|
const skippedCompetitors = [];
|
|
34613
35029
|
if (includeCompetitors) {
|
|
34614
35030
|
const existingCompetitors = new Set(
|
|
34615
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
35031
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
34616
35032
|
);
|
|
34617
35033
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
34618
35034
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -34631,7 +35047,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34631
35047
|
app.db.transaction((tx) => {
|
|
34632
35048
|
for (const query of promotedQueries) {
|
|
34633
35049
|
tx.insert(queries).values({
|
|
34634
|
-
id:
|
|
35050
|
+
id: crypto28.randomUUID(),
|
|
34635
35051
|
projectId: project.id,
|
|
34636
35052
|
query,
|
|
34637
35053
|
provenance,
|
|
@@ -34640,7 +35056,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
34640
35056
|
}
|
|
34641
35057
|
for (const domain of promotedCompetitors) {
|
|
34642
35058
|
tx.insert(competitors).values({
|
|
34643
|
-
id:
|
|
35059
|
+
id: crypto28.randomUUID(),
|
|
34644
35060
|
projectId: project.id,
|
|
34645
35061
|
domain,
|
|
34646
35062
|
provenance,
|
|
@@ -34730,8 +35146,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
34730
35146
|
}
|
|
34731
35147
|
|
|
34732
35148
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
34733
|
-
import
|
|
34734
|
-
import { eq as
|
|
35149
|
+
import crypto29 from "crypto";
|
|
35150
|
+
import { eq as eq37 } from "drizzle-orm";
|
|
34735
35151
|
var DEFAULT_MAX_PROBES = 100;
|
|
34736
35152
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
34737
35153
|
function classifyProbeBucket(input) {
|
|
@@ -34818,7 +35234,7 @@ async function executeDiscovery(opts) {
|
|
|
34818
35234
|
status: DiscoverySessionStatuses.seeding,
|
|
34819
35235
|
dedupThreshold,
|
|
34820
35236
|
startedAt
|
|
34821
|
-
}).where(
|
|
35237
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
34822
35238
|
const seedResult = await opts.deps.seed({
|
|
34823
35239
|
project: opts.project,
|
|
34824
35240
|
icpDescription: opts.icpDescription,
|
|
@@ -34879,7 +35295,7 @@ async function executeDiscovery(opts) {
|
|
|
34879
35295
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
34880
35296
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
34881
35297
|
warning
|
|
34882
|
-
}).where(
|
|
35298
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
34883
35299
|
const probeLocation = opts.locations?.[0];
|
|
34884
35300
|
const probeResults = await mapWithConcurrency(
|
|
34885
35301
|
probedCanonicals,
|
|
@@ -34898,7 +35314,7 @@ async function executeDiscovery(opts) {
|
|
|
34898
35314
|
probeRows.push({ citedDomains: probe.citedDomains, bucket });
|
|
34899
35315
|
buckets[bucket]++;
|
|
34900
35316
|
return {
|
|
34901
|
-
id:
|
|
35317
|
+
id: crypto29.randomUUID(),
|
|
34902
35318
|
sessionId: opts.sessionId,
|
|
34903
35319
|
projectId: opts.project.id,
|
|
34904
35320
|
query: probedCanonicals[index2],
|
|
@@ -34933,7 +35349,7 @@ async function executeDiscovery(opts) {
|
|
|
34933
35349
|
wastedCount: buckets["wasted-surface"],
|
|
34934
35350
|
competitorMap,
|
|
34935
35351
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34936
|
-
}).where(
|
|
35352
|
+
}).where(eq37(discoverySessions.id, opts.sessionId)).run();
|
|
34937
35353
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
34938
35354
|
return {
|
|
34939
35355
|
buckets,
|
|
@@ -34950,7 +35366,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
|
|
|
34950
35366
|
const domain = normalizeDomain(entry.domain);
|
|
34951
35367
|
if (!domain) continue;
|
|
34952
35368
|
db.insert(domainClassifications).values({
|
|
34953
|
-
id:
|
|
35369
|
+
id: crypto29.randomUUID(),
|
|
34954
35370
|
projectId,
|
|
34955
35371
|
domain,
|
|
34956
35372
|
competitorType: entry.competitorType,
|
|
@@ -34973,7 +35389,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
34973
35389
|
status: DiscoverySessionStatuses.failed,
|
|
34974
35390
|
error,
|
|
34975
35391
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34976
|
-
}).where(
|
|
35392
|
+
}).where(eq37(discoverySessions.id, sessionId)).run();
|
|
34977
35393
|
}
|
|
34978
35394
|
function dedupeStrings(input) {
|
|
34979
35395
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -34990,8 +35406,8 @@ function dedupeStrings(input) {
|
|
|
34990
35406
|
}
|
|
34991
35407
|
|
|
34992
35408
|
// ../api-routes/src/technical-aeo.ts
|
|
34993
|
-
import
|
|
34994
|
-
import { and as and29, asc as asc5, count, desc as desc17, eq as
|
|
35409
|
+
import crypto30 from "crypto";
|
|
35410
|
+
import { and as and29, asc as asc5, count, desc as desc17, eq as eq38, inArray as inArray14 } from "drizzle-orm";
|
|
34995
35411
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
34996
35412
|
function emptyScore(projectName) {
|
|
34997
35413
|
return {
|
|
@@ -35023,10 +35439,10 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
35023
35439
|
async function technicalAeoRoutes(app, opts) {
|
|
35024
35440
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
35025
35441
|
const project = resolveProject(app.db, request.params.name);
|
|
35026
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
35027
|
-
|
|
35028
|
-
|
|
35029
|
-
|
|
35442
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35443
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35444
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35445
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35030
35446
|
notProbeRun()
|
|
35031
35447
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
35032
35448
|
const latest = rows[0];
|
|
@@ -35058,18 +35474,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35058
35474
|
});
|
|
35059
35475
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
35060
35476
|
const project = resolveProject(app.db, request.params.name);
|
|
35061
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
35062
|
-
|
|
35063
|
-
|
|
35064
|
-
|
|
35477
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35478
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35479
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35480
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35065
35481
|
notProbeRun()
|
|
35066
35482
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
35067
35483
|
if (!latest) {
|
|
35068
35484
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
35069
35485
|
}
|
|
35070
35486
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
35071
|
-
const conds = [
|
|
35072
|
-
if (statusFilter) conds.push(
|
|
35487
|
+
const conds = [eq38(siteAuditPages.runId, latest.runId)];
|
|
35488
|
+
if (statusFilter) conds.push(eq38(siteAuditPages.status, statusFilter));
|
|
35073
35489
|
const where = and29(...conds);
|
|
35074
35490
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
35075
35491
|
const total = totalRow?.value ?? 0;
|
|
@@ -35094,10 +35510,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35094
35510
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
35095
35511
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
35096
35512
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
35097
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
35098
|
-
|
|
35099
|
-
|
|
35100
|
-
|
|
35513
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq38(siteAuditSnapshots.runId, runs.id)).where(and29(
|
|
35514
|
+
eq38(siteAuditSnapshots.projectId, project.id),
|
|
35515
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35516
|
+
inArray14(runs.status, SURFACEABLE_STATUSES),
|
|
35101
35517
|
notProbeRun()
|
|
35102
35518
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
35103
35519
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -35109,15 +35525,15 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
35109
35525
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
35110
35526
|
}
|
|
35111
35527
|
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and29(
|
|
35112
|
-
|
|
35113
|
-
|
|
35114
|
-
|
|
35528
|
+
eq38(runs.projectId, project.id),
|
|
35529
|
+
eq38(runs.kind, RunKinds["site-audit"]),
|
|
35530
|
+
inArray14(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
35115
35531
|
)).get();
|
|
35116
35532
|
if (existing) {
|
|
35117
35533
|
return { runId: existing.id, status: existing.status };
|
|
35118
35534
|
}
|
|
35119
35535
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
35120
|
-
const runId =
|
|
35536
|
+
const runId = crypto30.randomUUID();
|
|
35121
35537
|
app.db.insert(runs).values({
|
|
35122
35538
|
id: runId,
|
|
35123
35539
|
projectId: project.id,
|
|
@@ -35470,7 +35886,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
35470
35886
|
}
|
|
35471
35887
|
|
|
35472
35888
|
// src/intelligence-service.ts
|
|
35473
|
-
import
|
|
35889
|
+
import crypto31 from "crypto";
|
|
35474
35890
|
|
|
35475
35891
|
// src/logger.ts
|
|
35476
35892
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -35703,8 +36119,8 @@ var IntelligenceService = class {
|
|
|
35703
36119
|
analyzeAndPersist(runId, projectId) {
|
|
35704
36120
|
const recentRuns = this.db.select().from(runs).where(
|
|
35705
36121
|
and30(
|
|
35706
|
-
|
|
35707
|
-
or6(
|
|
36122
|
+
eq39(runs.projectId, projectId),
|
|
36123
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
35708
36124
|
// Defensive: RunCoordinator already skips probes before this is
|
|
35709
36125
|
// called, but if a future call site invokes analyzeAndPersist
|
|
35710
36126
|
// directly for a probe, probes still must not pollute the
|
|
@@ -35786,7 +36202,7 @@ var IntelligenceService = class {
|
|
|
35786
36202
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
35787
36203
|
*/
|
|
35788
36204
|
analyzeAndPersistGbp(runId, projectId) {
|
|
35789
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
36205
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq39(runs.id, runId)).get();
|
|
35790
36206
|
if (!runRow) {
|
|
35791
36207
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
35792
36208
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -35795,8 +36211,8 @@ var IntelligenceService = class {
|
|
|
35795
36211
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
35796
36212
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
35797
36213
|
const selected = this.db.select().from(gbpLocations).where(and30(
|
|
35798
|
-
|
|
35799
|
-
|
|
36214
|
+
eq39(gbpLocations.projectId, projectId),
|
|
36215
|
+
eq39(gbpLocations.selected, true),
|
|
35800
36216
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
35801
36217
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
35802
36218
|
)).all();
|
|
@@ -35831,12 +36247,12 @@ var IntelligenceService = class {
|
|
|
35831
36247
|
}
|
|
35832
36248
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
35833
36249
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
35834
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(
|
|
35835
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(
|
|
35836
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(
|
|
35837
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(
|
|
36250
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, projectId), eq39(gbpDailyMetrics.locationName, locationName))).all();
|
|
36251
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, projectId), eq39(gbpPlaceActions.locationName, locationName))).all();
|
|
36252
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and30(eq39(gbpLodgingSnapshots.projectId, projectId), eq39(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
36253
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and30(eq39(gbpLocations.projectId, projectId), eq39(gbpLocations.locationName, locationName))).get();
|
|
35838
36254
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
35839
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(
|
|
36255
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and30(eq39(gbpPlaceDetails.projectId, projectId), eq39(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
35840
36256
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
35841
36257
|
const summary = buildGbpSummary({
|
|
35842
36258
|
locationName,
|
|
@@ -35872,7 +36288,7 @@ var IntelligenceService = class {
|
|
|
35872
36288
|
/** Build the month-over-month keyword series for a location from the
|
|
35873
36289
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
35874
36290
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
35875
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(
|
|
36291
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and30(eq39(gbpKeywordMonthly.projectId, projectId), eq39(gbpKeywordMonthly.locationName, locationName))).all();
|
|
35876
36292
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
35877
36293
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
35878
36294
|
const recentMonth = months[0] ?? null;
|
|
@@ -35903,7 +36319,7 @@ var IntelligenceService = class {
|
|
|
35903
36319
|
*/
|
|
35904
36320
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
35905
36321
|
const covered = new Set(coveredLocationNames);
|
|
35906
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(
|
|
36322
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and30(eq39(insights.projectId, projectId), eq39(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
35907
36323
|
const staleIds = [];
|
|
35908
36324
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
35909
36325
|
for (const row of existing) {
|
|
@@ -35914,7 +36330,7 @@ var IntelligenceService = class {
|
|
|
35914
36330
|
}
|
|
35915
36331
|
this.db.transaction((tx) => {
|
|
35916
36332
|
for (const id of staleIds) {
|
|
35917
|
-
tx.delete(insights).where(
|
|
36333
|
+
tx.delete(insights).where(eq39(insights.id, id)).run();
|
|
35918
36334
|
}
|
|
35919
36335
|
for (const insight of gbpInsights) {
|
|
35920
36336
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -35992,7 +36408,7 @@ var IntelligenceService = class {
|
|
|
35992
36408
|
* create per run + aggregate). DB is left untouched.
|
|
35993
36409
|
*/
|
|
35994
36410
|
backfill(projectName, opts, onProgress) {
|
|
35995
|
-
const project = this.db.select().from(projects).where(
|
|
36411
|
+
const project = this.db.select().from(projects).where(eq39(projects.name, projectName)).get();
|
|
35996
36412
|
if (!project) {
|
|
35997
36413
|
throw new Error(`Project "${projectName}" not found`);
|
|
35998
36414
|
}
|
|
@@ -36006,8 +36422,8 @@ var IntelligenceService = class {
|
|
|
36006
36422
|
}
|
|
36007
36423
|
const allRuns = this.db.select().from(runs).where(
|
|
36008
36424
|
and30(
|
|
36009
|
-
|
|
36010
|
-
or6(
|
|
36425
|
+
eq39(runs.projectId, project.id),
|
|
36426
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
36011
36427
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
36012
36428
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36013
36429
|
)
|
|
@@ -36040,7 +36456,7 @@ var IntelligenceService = class {
|
|
|
36040
36456
|
let wouldDeleteTotal = 0;
|
|
36041
36457
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
36042
36458
|
if (isDryRun && targetRuns.length > 0) {
|
|
36043
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
36459
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray15(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
36044
36460
|
for (const r of rows) {
|
|
36045
36461
|
if (r.runId == null) continue;
|
|
36046
36462
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -36086,7 +36502,7 @@ var IntelligenceService = class {
|
|
|
36086
36502
|
return { processed, skipped, totalInsights };
|
|
36087
36503
|
}
|
|
36088
36504
|
loadTrackedCompetitors(projectId) {
|
|
36089
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
36505
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
36090
36506
|
}
|
|
36091
36507
|
/**
|
|
36092
36508
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -36107,15 +36523,15 @@ var IntelligenceService = class {
|
|
|
36107
36523
|
}
|
|
36108
36524
|
persistResult(result, runId, projectId) {
|
|
36109
36525
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
36110
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
36526
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq39(insights.runId, runId)).all();
|
|
36111
36527
|
for (const row of existingInsights) {
|
|
36112
36528
|
if (row.dismissed) {
|
|
36113
36529
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
36114
36530
|
}
|
|
36115
36531
|
}
|
|
36116
36532
|
this.db.transaction((tx) => {
|
|
36117
|
-
tx.delete(insights).where(
|
|
36118
|
-
tx.delete(healthSnapshots).where(
|
|
36533
|
+
tx.delete(insights).where(eq39(insights.runId, runId)).run();
|
|
36534
|
+
tx.delete(healthSnapshots).where(eq39(healthSnapshots.runId, runId)).run();
|
|
36119
36535
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36120
36536
|
for (const insight of result.insights) {
|
|
36121
36537
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -36135,7 +36551,7 @@ var IntelligenceService = class {
|
|
|
36135
36551
|
}).run();
|
|
36136
36552
|
}
|
|
36137
36553
|
tx.insert(healthSnapshots).values({
|
|
36138
|
-
id:
|
|
36554
|
+
id: crypto31.randomUUID(),
|
|
36139
36555
|
projectId,
|
|
36140
36556
|
runId,
|
|
36141
36557
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -36168,14 +36584,14 @@ var IntelligenceService = class {
|
|
|
36168
36584
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
36169
36585
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
36170
36586
|
if (regressions.length === 0) return rawInsights;
|
|
36171
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
36587
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq39(gscSearchData.projectId, projectId)).all();
|
|
36172
36588
|
const gscConnected = gscRows.length > 0;
|
|
36173
36589
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
36174
36590
|
for (const row of gscRows) {
|
|
36175
36591
|
const key = row.query.toLowerCase();
|
|
36176
36592
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
36177
36593
|
}
|
|
36178
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
36594
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
|
|
36179
36595
|
const locationCount = Math.max(
|
|
36180
36596
|
1,
|
|
36181
36597
|
(projectRow?.locations ?? []).length
|
|
@@ -36183,9 +36599,9 @@ var IntelligenceService = class {
|
|
|
36183
36599
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
36184
36600
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
36185
36601
|
and30(
|
|
36186
|
-
|
|
36187
|
-
|
|
36188
|
-
or6(
|
|
36602
|
+
eq39(runs.projectId, projectId),
|
|
36603
|
+
eq39(runs.kind, RunKinds["answer-visibility"]),
|
|
36604
|
+
or6(eq39(runs.status, "completed"), eq39(runs.status, "partial")),
|
|
36189
36605
|
// Defensive — see top of file.
|
|
36190
36606
|
ne5(runs.trigger, RunTriggers.probe)
|
|
36191
36607
|
)
|
|
@@ -36205,7 +36621,7 @@ var IntelligenceService = class {
|
|
|
36205
36621
|
const haveHistory = recentRunIds.length > 0;
|
|
36206
36622
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
36207
36623
|
if (haveHistory) {
|
|
36208
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(
|
|
36624
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and30(eq39(insights.type, "regression"), inArray15(insights.runId, recentRunIds))).all();
|
|
36209
36625
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
36210
36626
|
for (const row of priorRows) {
|
|
36211
36627
|
if (!row.runId) continue;
|
|
@@ -36234,7 +36650,7 @@ var IntelligenceService = class {
|
|
|
36234
36650
|
});
|
|
36235
36651
|
}
|
|
36236
36652
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
36237
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
36653
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq39(projects.id, projectId)).get();
|
|
36238
36654
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
36239
36655
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
36240
36656
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -36251,7 +36667,7 @@ var IntelligenceService = class {
|
|
|
36251
36667
|
citedDomains: querySnapshots.citedDomains,
|
|
36252
36668
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
36253
36669
|
snapshotLocation: querySnapshots.location
|
|
36254
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
36670
|
+
}).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(eq39(querySnapshots.runId, runId)).all();
|
|
36255
36671
|
const snapshots = [];
|
|
36256
36672
|
let orphanCount = 0;
|
|
36257
36673
|
for (const r of rows) {
|