@ainyc/canonry 4.83.0 → 4.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/SKILL.md +14 -10
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +7 -5
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +1 -1
- package/assets/agent-workspace/skills/aero/references/orchestration.md +22 -20
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +11 -9
- package/assets/agent-workspace/skills/aero/references/reporting.md +14 -9
- package/assets/agent-workspace/skills/aero/soul.md +5 -5
- package/assets/agent-workspace/skills/canonry/SKILL.md +1 -1
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +84 -36
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +44 -10
- package/assets/assets/{BacklinksPage-OrSg_iPA.js → BacklinksPage-CDAv0ggn.js} +1 -1
- package/assets/assets/{ChartPrimitives-DPBhAT_r.js → ChartPrimitives-CnAmsyt7.js} +1 -1
- package/assets/assets/{ProjectPage-CpMcEmtw.js → ProjectPage-C9KEgRxD.js} +1 -1
- package/assets/assets/{RunRow-2Rty0BAH.js → RunRow-CVZ5o8fg.js} +1 -1
- package/assets/assets/{RunsPage-B3ahqf8s.js → RunsPage-Bzy5c0MZ.js} +1 -1
- package/assets/assets/{SettingsPage-BIjeI85q.js → SettingsPage-B1ocxPBe.js} +1 -1
- package/assets/assets/{TrafficPage-DjGoj691.js → TrafficPage-D2zepQOC.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BgKG-2q3.js → TrafficSourceDetailPage-C7JuAkaK.js} +1 -1
- package/assets/assets/{arrow-left-Cf7wmru1.js → arrow-left-Bv3CWylm.js} +1 -1
- package/assets/assets/{extract-error-message-CANxezte.js → extract-error-message-BtVid5TP.js} +1 -1
- package/assets/assets/{index-CGlPx_cu.js → index-DmNti_xn.js} +72 -72
- package/assets/assets/{trash-2-6nHJZrvy.js → trash-2-BoimCsYz.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-GOWH42QV.js → chunk-3K3QRSYE.js} +709 -426
- package/dist/{chunk-Y3O3HBMN.js → chunk-62YB3ML7.js} +49 -1
- package/dist/{chunk-NRACXNI7.js → chunk-7BMSWI2K.js} +6 -4
- package/dist/{chunk-BNF3HXBW.js → chunk-I2BJC3DT.js} +1021 -942
- package/dist/cli.js +165 -29
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-FHQM7YMA.js → intelligence-service-AHHBQKRD.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
|
@@ -169,6 +169,7 @@ import {
|
|
|
169
169
|
notFound,
|
|
170
170
|
notImplemented,
|
|
171
171
|
notificationDtoSchema,
|
|
172
|
+
parseInclusiveEndMs,
|
|
172
173
|
parseRunError,
|
|
173
174
|
parseWindow,
|
|
174
175
|
pickClusterRepresentative,
|
|
@@ -230,6 +231,7 @@ import {
|
|
|
230
231
|
unsupportedKind,
|
|
231
232
|
validationError,
|
|
232
233
|
visibilityStateFromAnswerMentioned,
|
|
234
|
+
visibilityStatsDtoSchema,
|
|
233
235
|
windowCutoff,
|
|
234
236
|
winnabilityClassLabel,
|
|
235
237
|
winnabilityClassSchema,
|
|
@@ -246,10 +248,10 @@ import {
|
|
|
246
248
|
wordpressSchemaDeployResultDtoSchema,
|
|
247
249
|
wordpressSchemaStatusResultDtoSchema,
|
|
248
250
|
wordpressStatusDtoSchema
|
|
249
|
-
} from "./chunk-
|
|
251
|
+
} from "./chunk-I2BJC3DT.js";
|
|
250
252
|
|
|
251
253
|
// src/intelligence-service.ts
|
|
252
|
-
import { eq as
|
|
254
|
+
import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
253
255
|
|
|
254
256
|
// ../db/src/client.ts
|
|
255
257
|
import { mkdirSync } from "fs";
|
|
@@ -805,8 +807,16 @@ var healthSnapshots = sqliteTable("health_snapshots", {
|
|
|
805
807
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
806
808
|
runId: text("run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
807
809
|
overallCitedRate: text("overall_cited_rate").notNull(),
|
|
810
|
+
// Answer-text mention rate, independent of citation. Nullable because the
|
|
811
|
+
// column is added by migration v80 via ALTER TABLE ADD COLUMN — rows
|
|
812
|
+
// persisted before v80 read back as NULL ("not measured"); readers coalesce
|
|
813
|
+
// NULL→0. New writes always populate it (see intelligence-service persist).
|
|
814
|
+
overallMentionRate: text("overall_mention_rate"),
|
|
808
815
|
totalPairs: integer("total_pairs").notNull(),
|
|
809
816
|
citedPairs: integer("cited_pairs").notNull(),
|
|
817
|
+
// Count of pairs MENTIONED in the answer text. Nullable for the same
|
|
818
|
+
// legacy-row reason as overall_mention_rate; coalesced NULL→0 on read.
|
|
819
|
+
mentionedPairs: integer("mentioned_pairs"),
|
|
810
820
|
providerBreakdown: text("provider_breakdown", { mode: "json" }).$type().notNull().default({}),
|
|
811
821
|
createdAt: text("created_at").notNull()
|
|
812
822
|
}, (table) => [
|
|
@@ -3089,6 +3099,31 @@ var MIGRATION_VERSIONS = [
|
|
|
3089
3099
|
statements: [
|
|
3090
3100
|
`ALTER TABLE discovery_probes ADD COLUMN answer_mentioned INTEGER`
|
|
3091
3101
|
]
|
|
3102
|
+
},
|
|
3103
|
+
{
|
|
3104
|
+
// Mention-rate columns on the persisted health snapshot, mirroring the
|
|
3105
|
+
// existing cited columns (overall_cited_rate / cited_pairs) for the
|
|
3106
|
+
// independent answer-text mention signal. Nullable: rows written before
|
|
3107
|
+
// this version have no mention math, so they read back as NULL ("not
|
|
3108
|
+
// measured") and readers coalesce NULL→0.
|
|
3109
|
+
//
|
|
3110
|
+
// Guarded `run` rather than bare `statements` (the v66 pattern): the
|
|
3111
|
+
// table-existence check makes this a no-op when `health_snapshots` is
|
|
3112
|
+
// absent — only possible on a legacy fixture whose recorded
|
|
3113
|
+
// `_migrations` version skips v23's `CREATE TABLE` (the bootstrap is
|
|
3114
|
+
// bypassed). The column-existence check keeps a replay idempotent.
|
|
3115
|
+
version: 80,
|
|
3116
|
+
name: "health-snapshots-mention-rate",
|
|
3117
|
+
statements: [],
|
|
3118
|
+
run: (db) => {
|
|
3119
|
+
if (!tableExists(db, "health_snapshots")) return;
|
|
3120
|
+
if (!columnExists(db, "health_snapshots", "overall_mention_rate")) {
|
|
3121
|
+
db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN overall_mention_rate TEXT`));
|
|
3122
|
+
}
|
|
3123
|
+
if (!columnExists(db, "health_snapshots", "mentioned_pairs")) {
|
|
3124
|
+
db.run(sql.raw(`ALTER TABLE health_snapshots ADD COLUMN mentioned_pairs INTEGER`));
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3092
3127
|
}
|
|
3093
3128
|
];
|
|
3094
3129
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -3412,26 +3447,33 @@ function computeHealth(run) {
|
|
|
3412
3447
|
const providerStats = /* @__PURE__ */ new Map();
|
|
3413
3448
|
let totalPairs = 0;
|
|
3414
3449
|
let citedPairs = 0;
|
|
3450
|
+
let mentionedPairs = 0;
|
|
3415
3451
|
for (const snap of run.snapshots) {
|
|
3416
3452
|
totalPairs++;
|
|
3417
3453
|
if (snap.cited) citedPairs++;
|
|
3418
|
-
|
|
3454
|
+
if (snap.answerMentioned === true) mentionedPairs++;
|
|
3455
|
+
const stats = providerStats.get(snap.provider) ?? { cited: 0, mentioned: 0, total: 0 };
|
|
3419
3456
|
stats.total++;
|
|
3420
3457
|
if (snap.cited) stats.cited++;
|
|
3458
|
+
if (snap.answerMentioned === true) stats.mentioned++;
|
|
3421
3459
|
providerStats.set(snap.provider, stats);
|
|
3422
3460
|
}
|
|
3423
3461
|
const providerBreakdown = {};
|
|
3424
3462
|
for (const [provider, stats] of providerStats) {
|
|
3425
3463
|
providerBreakdown[provider] = {
|
|
3426
3464
|
citedRate: stats.total > 0 ? stats.cited / stats.total : 0,
|
|
3465
|
+
mentionRate: stats.total > 0 ? stats.mentioned / stats.total : 0,
|
|
3427
3466
|
cited: stats.cited,
|
|
3467
|
+
mentioned: stats.mentioned,
|
|
3428
3468
|
total: stats.total
|
|
3429
3469
|
};
|
|
3430
3470
|
}
|
|
3431
3471
|
return {
|
|
3432
3472
|
overallCitedRate: totalPairs > 0 ? citedPairs / totalPairs : 0,
|
|
3473
|
+
overallMentionRate: totalPairs > 0 ? mentionedPairs / totalPairs : 0,
|
|
3433
3474
|
totalPairs,
|
|
3434
3475
|
citedPairs,
|
|
3476
|
+
mentionedPairs,
|
|
3435
3477
|
providerBreakdown
|
|
3436
3478
|
};
|
|
3437
3479
|
}
|
|
@@ -4299,18 +4341,18 @@ function buildCitationScorecard(snapshots, queryLookup) {
|
|
|
4299
4341
|
answerMentioned: snap.answerMentioned ?? null,
|
|
4300
4342
|
model: snap.model
|
|
4301
4343
|
};
|
|
4302
|
-
const
|
|
4303
|
-
|
|
4304
|
-
if (snap.citationState === CitationStates.cited)
|
|
4305
|
-
providerCounts.set(snap.provider,
|
|
4344
|
+
const counts2 = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
4345
|
+
counts2.total++;
|
|
4346
|
+
if (snap.citationState === CitationStates.cited) counts2.cited++;
|
|
4347
|
+
providerCounts.set(snap.provider, counts2);
|
|
4306
4348
|
}
|
|
4307
4349
|
const providerRates = providerList.map((provider) => {
|
|
4308
|
-
const
|
|
4309
|
-
const citationRate =
|
|
4350
|
+
const counts2 = providerCounts.get(provider) ?? { cited: 0, total: 0 };
|
|
4351
|
+
const citationRate = counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0;
|
|
4310
4352
|
return {
|
|
4311
4353
|
provider,
|
|
4312
|
-
citedCount:
|
|
4313
|
-
totalCount:
|
|
4354
|
+
citedCount: counts2.cited,
|
|
4355
|
+
totalCount: counts2.total,
|
|
4314
4356
|
citationRate
|
|
4315
4357
|
};
|
|
4316
4358
|
});
|
|
@@ -7878,13 +7920,13 @@ function buildRankedList(domains, limit) {
|
|
|
7878
7920
|
bySurfaceClass
|
|
7879
7921
|
};
|
|
7880
7922
|
}
|
|
7881
|
-
function buildCategoryCounts(
|
|
7923
|
+
function buildCategoryCounts(counts2) {
|
|
7882
7924
|
let grandTotal = 0;
|
|
7883
|
-
for (const domains of
|
|
7925
|
+
for (const domains of counts2.values()) {
|
|
7884
7926
|
for (const count2 of domains.values()) grandTotal += count2;
|
|
7885
7927
|
}
|
|
7886
7928
|
const result = [];
|
|
7887
|
-
for (const [category, domains] of
|
|
7929
|
+
for (const [category, domains] of counts2) {
|
|
7888
7930
|
let categoryTotal = 0;
|
|
7889
7931
|
const domainEntries = [];
|
|
7890
7932
|
for (const [domain, count2] of domains) {
|
|
@@ -7912,8 +7954,10 @@ function emptyHealthSnapshot(projectId) {
|
|
|
7912
7954
|
projectId,
|
|
7913
7955
|
runId: null,
|
|
7914
7956
|
overallCitedRate: 0,
|
|
7957
|
+
overallMentionRate: 0,
|
|
7915
7958
|
totalPairs: 0,
|
|
7916
7959
|
citedPairs: 0,
|
|
7960
|
+
mentionedPairs: 0,
|
|
7917
7961
|
providerBreakdown: {},
|
|
7918
7962
|
createdAt: "",
|
|
7919
7963
|
status: "no-data",
|
|
@@ -7936,15 +7980,31 @@ function mapInsightRow(r) {
|
|
|
7936
7980
|
createdAt: r.createdAt
|
|
7937
7981
|
};
|
|
7938
7982
|
}
|
|
7983
|
+
function coalesceProviderBreakdown(breakdown) {
|
|
7984
|
+
const out = {};
|
|
7985
|
+
for (const [provider, entry] of Object.entries(breakdown)) {
|
|
7986
|
+
out[provider] = {
|
|
7987
|
+
citedRate: entry.citedRate,
|
|
7988
|
+
mentionRate: entry.mentionRate ?? 0,
|
|
7989
|
+
cited: entry.cited,
|
|
7990
|
+
mentioned: entry.mentioned ?? 0,
|
|
7991
|
+
total: entry.total
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7994
|
+
return out;
|
|
7995
|
+
}
|
|
7939
7996
|
function mapHealthRow(r) {
|
|
7940
7997
|
return {
|
|
7941
7998
|
id: r.id,
|
|
7942
7999
|
projectId: r.projectId,
|
|
7943
8000
|
runId: r.runId ?? null,
|
|
7944
8001
|
overallCitedRate: Number(r.overallCitedRate),
|
|
8002
|
+
// Legacy rows (persisted before v80) have NULL mention columns → 0.
|
|
8003
|
+
overallMentionRate: r.overallMentionRate == null ? 0 : Number(r.overallMentionRate),
|
|
7945
8004
|
totalPairs: r.totalPairs,
|
|
7946
8005
|
citedPairs: r.citedPairs,
|
|
7947
|
-
|
|
8006
|
+
mentionedPairs: r.mentionedPairs ?? 0,
|
|
8007
|
+
providerBreakdown: coalesceProviderBreakdown(r.providerBreakdown),
|
|
7948
8008
|
createdAt: r.createdAt,
|
|
7949
8009
|
status: "ready"
|
|
7950
8010
|
};
|
|
@@ -7953,26 +8013,31 @@ function aggregateHealthSnapshots(projectId, rows) {
|
|
|
7953
8013
|
if (rows.length === 1) return mapHealthRow(rows[0]);
|
|
7954
8014
|
let totalPairs = 0;
|
|
7955
8015
|
let citedPairs = 0;
|
|
8016
|
+
let mentionedPairs = 0;
|
|
7956
8017
|
const mergedProviders = {};
|
|
7957
8018
|
let newestCreatedAt = "";
|
|
7958
8019
|
const runIds = [];
|
|
7959
8020
|
for (const row of rows) {
|
|
7960
8021
|
totalPairs += row.totalPairs;
|
|
7961
8022
|
citedPairs += row.citedPairs;
|
|
8023
|
+
mentionedPairs += row.mentionedPairs ?? 0;
|
|
7962
8024
|
if (row.createdAt > newestCreatedAt) newestCreatedAt = row.createdAt;
|
|
7963
8025
|
if (row.runId) runIds.push(row.runId);
|
|
7964
8026
|
const providerBreakdown = row.providerBreakdown;
|
|
7965
8027
|
for (const [provider, entry] of Object.entries(providerBreakdown)) {
|
|
7966
|
-
const existing = mergedProviders[provider] ?? { total: 0, cited: 0, citedRate: 0 };
|
|
8028
|
+
const existing = mergedProviders[provider] ?? { total: 0, cited: 0, mentioned: 0, citedRate: 0, mentionRate: 0 };
|
|
7967
8029
|
existing.total += entry.total;
|
|
7968
8030
|
existing.cited += entry.cited;
|
|
8031
|
+
existing.mentioned += entry.mentioned ?? 0;
|
|
7969
8032
|
mergedProviders[provider] = existing;
|
|
7970
8033
|
}
|
|
7971
8034
|
}
|
|
7972
8035
|
for (const entry of Object.values(mergedProviders)) {
|
|
7973
8036
|
entry.citedRate = entry.total > 0 ? entry.cited / entry.total : 0;
|
|
8037
|
+
entry.mentionRate = entry.total > 0 ? entry.mentioned / entry.total : 0;
|
|
7974
8038
|
}
|
|
7975
8039
|
const overallCitedRate = totalPairs > 0 ? citedPairs / totalPairs : 0;
|
|
8040
|
+
const overallMentionRate = totalPairs > 0 ? mentionedPairs / totalPairs : 0;
|
|
7976
8041
|
return {
|
|
7977
8042
|
// Synthetic id so consumers can tell this is an aggregate; concatenate
|
|
7978
8043
|
// source runIds for traceability without inventing a new schema column.
|
|
@@ -7980,8 +8045,10 @@ function aggregateHealthSnapshots(projectId, rows) {
|
|
|
7980
8045
|
projectId,
|
|
7981
8046
|
runId: runIds[0] ?? null,
|
|
7982
8047
|
overallCitedRate,
|
|
8048
|
+
overallMentionRate,
|
|
7983
8049
|
totalPairs,
|
|
7984
8050
|
citedPairs,
|
|
8051
|
+
mentionedPairs,
|
|
7985
8052
|
providerBreakdown: mergedProviders,
|
|
7986
8053
|
createdAt: newestCreatedAt,
|
|
7987
8054
|
status: "ready"
|
|
@@ -11673,19 +11740,19 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
11673
11740
|
considered++;
|
|
11674
11741
|
if (snap.citationState === CitationStates.cited) citedQueryIds.add(snap.queryId);
|
|
11675
11742
|
if (snap.answerMentioned) mentionedQueryIds.add(snap.queryId);
|
|
11676
|
-
const
|
|
11677
|
-
|
|
11678
|
-
if (snap.citationState === CitationStates.cited)
|
|
11679
|
-
providerCounts.set(snap.provider,
|
|
11743
|
+
const counts2 = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
11744
|
+
counts2.total++;
|
|
11745
|
+
if (snap.citationState === CitationStates.cited) counts2.cited++;
|
|
11746
|
+
providerCounts.set(snap.provider, counts2);
|
|
11680
11747
|
}
|
|
11681
11748
|
if (considered === 0) continue;
|
|
11682
11749
|
const citedQueryCount = citedQueryIds.size;
|
|
11683
11750
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
11684
11751
|
const citationRate = totalQueries > 0 ? Math.round(citedQueryCount / totalQueries * 100) : 0;
|
|
11685
11752
|
const mentionRate = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
|
|
11686
|
-
const providerRates = [...providerCounts.entries()].map(([provider,
|
|
11753
|
+
const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
|
|
11687
11754
|
provider,
|
|
11688
|
-
citationRate:
|
|
11755
|
+
citationRate: counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0
|
|
11689
11756
|
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
11690
11757
|
points.push({
|
|
11691
11758
|
runId: run.id,
|
|
@@ -12655,8 +12722,165 @@ function normalizeDomain2(domain) {
|
|
|
12655
12722
|
return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
|
|
12656
12723
|
}
|
|
12657
12724
|
|
|
12725
|
+
// ../api-routes/src/visibility-stats.ts
|
|
12726
|
+
import { and as and11, desc as desc8, eq as eq16, inArray as inArray8 } from "drizzle-orm";
|
|
12727
|
+
function round42(value) {
|
|
12728
|
+
return Math.round(value * 1e4) / 1e4;
|
|
12729
|
+
}
|
|
12730
|
+
function emptyAgg() {
|
|
12731
|
+
return { total: 0, checked: 0, mentioned: 0, cited: 0, first: null, last: null };
|
|
12732
|
+
}
|
|
12733
|
+
function addSnapshot(agg, snap) {
|
|
12734
|
+
agg.total++;
|
|
12735
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) agg.checked++;
|
|
12736
|
+
if (snap.answerMentioned === true) agg.mentioned++;
|
|
12737
|
+
if (snap.citationState === CitationStates.cited) agg.cited++;
|
|
12738
|
+
if (agg.first === null || snap.createdAt < agg.first) agg.first = snap.createdAt;
|
|
12739
|
+
if (agg.last === null || snap.createdAt > agg.last) agg.last = snap.createdAt;
|
|
12740
|
+
}
|
|
12741
|
+
function counts(agg) {
|
|
12742
|
+
return {
|
|
12743
|
+
total: agg.total,
|
|
12744
|
+
checked: agg.checked,
|
|
12745
|
+
mentioned: agg.mentioned,
|
|
12746
|
+
cited: agg.cited,
|
|
12747
|
+
// mention proportion is over the CHECKED sample; citation proportion is
|
|
12748
|
+
// over the full total (every snapshot is checked for citation).
|
|
12749
|
+
mentionRate: agg.checked > 0 ? round42(agg.mentioned / agg.checked) : null,
|
|
12750
|
+
citedRate: agg.total > 0 ? round42(agg.cited / agg.total) : null
|
|
12751
|
+
};
|
|
12752
|
+
}
|
|
12753
|
+
function providerEntries(byProvider) {
|
|
12754
|
+
return [...byProvider.entries()].map(([provider, agg]) => ({
|
|
12755
|
+
provider,
|
|
12756
|
+
...counts(agg),
|
|
12757
|
+
// first/last are non-null once at least one snapshot landed in the agg,
|
|
12758
|
+
// which is guaranteed for any provider that made it into the map.
|
|
12759
|
+
firstObserved: agg.first ?? "",
|
|
12760
|
+
lastObserved: agg.last ?? ""
|
|
12761
|
+
})).sort((a, b) => a.provider.localeCompare(b.provider));
|
|
12762
|
+
}
|
|
12763
|
+
function computeVisibilityStats(input) {
|
|
12764
|
+
const { groupBy } = input;
|
|
12765
|
+
const wantProviders = groupBy === "provider";
|
|
12766
|
+
const queryById = /* @__PURE__ */ new Map();
|
|
12767
|
+
const queryByText = /* @__PURE__ */ new Map();
|
|
12768
|
+
for (const q of input.queries) {
|
|
12769
|
+
queryById.set(q.id, q);
|
|
12770
|
+
queryByText.set(q.query, q);
|
|
12771
|
+
}
|
|
12772
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
12773
|
+
const totals = emptyAgg();
|
|
12774
|
+
const totalsByProvider = /* @__PURE__ */ new Map();
|
|
12775
|
+
for (const snap of input.snapshots) {
|
|
12776
|
+
let resolved;
|
|
12777
|
+
if (snap.queryId && queryById.has(snap.queryId)) resolved = queryById.get(snap.queryId);
|
|
12778
|
+
else if (snap.queryText && queryByText.has(snap.queryText)) resolved = queryByText.get(snap.queryText);
|
|
12779
|
+
if (!resolved) continue;
|
|
12780
|
+
let bucket = byQuery.get(resolved.id);
|
|
12781
|
+
if (!bucket) {
|
|
12782
|
+
bucket = { id: resolved.id, query: resolved.query, agg: emptyAgg(), byProvider: /* @__PURE__ */ new Map() };
|
|
12783
|
+
byQuery.set(resolved.id, bucket);
|
|
12784
|
+
}
|
|
12785
|
+
addSnapshot(bucket.agg, snap);
|
|
12786
|
+
addSnapshot(totals, snap);
|
|
12787
|
+
if (wantProviders) {
|
|
12788
|
+
const qpAgg = bucket.byProvider.get(snap.provider) ?? emptyAgg();
|
|
12789
|
+
addSnapshot(qpAgg, snap);
|
|
12790
|
+
bucket.byProvider.set(snap.provider, qpAgg);
|
|
12791
|
+
const tpAgg = totalsByProvider.get(snap.provider) ?? emptyAgg();
|
|
12792
|
+
addSnapshot(tpAgg, snap);
|
|
12793
|
+
totalsByProvider.set(snap.provider, tpAgg);
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12796
|
+
const queryEntries = [...byQuery.values()].map((bucket) => ({
|
|
12797
|
+
queryId: bucket.id,
|
|
12798
|
+
query: bucket.query,
|
|
12799
|
+
...counts(bucket.agg),
|
|
12800
|
+
firstObserved: bucket.agg.first ?? "",
|
|
12801
|
+
lastObserved: bucket.agg.last ?? "",
|
|
12802
|
+
...wantProviders ? { providers: providerEntries(bucket.byProvider) } : {}
|
|
12803
|
+
})).sort((a, b) => a.query.localeCompare(b.query));
|
|
12804
|
+
return {
|
|
12805
|
+
totals: counts(totals),
|
|
12806
|
+
...wantProviders ? { byProvider: providerEntries(totalsByProvider) } : {},
|
|
12807
|
+
queries: queryEntries
|
|
12808
|
+
};
|
|
12809
|
+
}
|
|
12810
|
+
async function visibilityStatsRoutes(app) {
|
|
12811
|
+
app.get("/projects/:name/visibility-stats", async (request, reply) => {
|
|
12812
|
+
const project = resolveProject(app.db, request.params.name);
|
|
12813
|
+
const { since: sinceRaw, until: untilRaw, lastRuns: lastRunsRaw, groupBy: groupByRaw } = request.query;
|
|
12814
|
+
let groupBy = null;
|
|
12815
|
+
if (groupByRaw !== void 0 && groupByRaw !== "") {
|
|
12816
|
+
if (groupByRaw !== "provider") throw validationError('"groupBy" must be "provider"');
|
|
12817
|
+
groupBy = "provider";
|
|
12818
|
+
}
|
|
12819
|
+
const hasSince = sinceRaw !== void 0 && sinceRaw !== "";
|
|
12820
|
+
const hasUntil = untilRaw !== void 0 && untilRaw !== "";
|
|
12821
|
+
const hasLastRuns = lastRunsRaw !== void 0 && lastRunsRaw !== "";
|
|
12822
|
+
if (hasLastRuns && (hasSince || hasUntil)) {
|
|
12823
|
+
throw validationError('"lastRuns" cannot be combined with "since"/"until" \u2014 use one or the other');
|
|
12824
|
+
}
|
|
12825
|
+
let sinceMs = null;
|
|
12826
|
+
let untilMs = null;
|
|
12827
|
+
if (hasSince) {
|
|
12828
|
+
const ms = Date.parse(sinceRaw);
|
|
12829
|
+
if (Number.isNaN(ms)) throw validationError('"since" must be an ISO 8601 date/time');
|
|
12830
|
+
sinceMs = ms;
|
|
12831
|
+
}
|
|
12832
|
+
if (hasUntil) {
|
|
12833
|
+
const ms = parseInclusiveEndMs(untilRaw);
|
|
12834
|
+
if (ms === null) throw validationError('"until" must be an ISO 8601 date/time');
|
|
12835
|
+
untilMs = ms;
|
|
12836
|
+
}
|
|
12837
|
+
if (sinceMs !== null && untilMs !== null && untilMs < sinceMs) {
|
|
12838
|
+
throw validationError('"until" must be on or after "since"');
|
|
12839
|
+
}
|
|
12840
|
+
let lastRuns = null;
|
|
12841
|
+
if (hasLastRuns) {
|
|
12842
|
+
const n = Number(lastRunsRaw);
|
|
12843
|
+
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
12844
|
+
lastRuns = n;
|
|
12845
|
+
}
|
|
12846
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq16(queries.projectId, project.id)).all();
|
|
12847
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and11(eq16(runs.projectId, project.id), eq16(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
12848
|
+
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
12849
|
+
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
12850
|
+
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
12851
|
+
const runCount = projectRuns.length;
|
|
12852
|
+
const runIds = projectRuns.map((r) => r.id);
|
|
12853
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? app.db.select({
|
|
12854
|
+
queryId: querySnapshots.queryId,
|
|
12855
|
+
queryText: querySnapshots.queryText,
|
|
12856
|
+
provider: querySnapshots.provider,
|
|
12857
|
+
citationState: querySnapshots.citationState,
|
|
12858
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
12859
|
+
createdAt: querySnapshots.createdAt
|
|
12860
|
+
}).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all() : [];
|
|
12861
|
+
const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
|
|
12862
|
+
const response = {
|
|
12863
|
+
project: project.name,
|
|
12864
|
+
window: {
|
|
12865
|
+
since: hasSince ? sinceRaw : null,
|
|
12866
|
+
until: hasUntil ? untilRaw : null,
|
|
12867
|
+
lastRuns,
|
|
12868
|
+
runCount
|
|
12869
|
+
},
|
|
12870
|
+
totals: stats.totals,
|
|
12871
|
+
queries: stats.queries,
|
|
12872
|
+
// `groupBy` + `byProvider` appear together only when a breakdown was
|
|
12873
|
+
// requested; both are OMITTED otherwise (absent = no breakdown) so the
|
|
12874
|
+
// SDK types `groupBy` as `groupBy?: 'provider'` rather than a misleading
|
|
12875
|
+
// always-present literal.
|
|
12876
|
+
...groupBy === "provider" ? { groupBy, byProvider: stats.byProvider ?? [] } : {}
|
|
12877
|
+
};
|
|
12878
|
+
return reply.send(response);
|
|
12879
|
+
});
|
|
12880
|
+
}
|
|
12881
|
+
|
|
12658
12882
|
// ../api-routes/src/composites.ts
|
|
12659
|
-
import { eq as
|
|
12883
|
+
import { eq as eq17, and as and12, desc as desc9, sql as sql7, like, or as or4, inArray as inArray9 } from "drizzle-orm";
|
|
12660
12884
|
var TOP_INSIGHT_LIMIT = 5;
|
|
12661
12885
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
12662
12886
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -12674,7 +12898,7 @@ async function compositeRoutes(app) {
|
|
|
12674
12898
|
const project = resolveProject(app.db, request.params.name);
|
|
12675
12899
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
12676
12900
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
12677
|
-
const allRunsRaw = app.db.select().from(runs).where(
|
|
12901
|
+
const allRunsRaw = app.db.select().from(runs).where(and12(eq17(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
12678
12902
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
12679
12903
|
const totalRuns = allRuns.length;
|
|
12680
12904
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -12687,9 +12911,9 @@ async function compositeRoutes(app) {
|
|
|
12687
12911
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
12688
12912
|
const latestRunRow = allRuns[0] ?? null;
|
|
12689
12913
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
12690
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
12914
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq17(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
12691
12915
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
12692
|
-
const insightRows = app.db.select().from(insights).where(
|
|
12916
|
+
const insightRows = app.db.select().from(insights).where(eq17(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
12693
12917
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
12694
12918
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
12695
12919
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
@@ -12704,8 +12928,8 @@ async function compositeRoutes(app) {
|
|
|
12704
12928
|
previousSnapshots,
|
|
12705
12929
|
previousVisibilityRun?.createdAt ?? null
|
|
12706
12930
|
);
|
|
12707
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
12708
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
12931
|
+
const competitorRows = app.db.select().from(competitors).where(eq17(competitors.projectId, project.id)).all();
|
|
12932
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
12709
12933
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
12710
12934
|
const configuredApiProviders = project.providers.filter((p) => !p.startsWith("cdp:"));
|
|
12711
12935
|
const mentionShareCompetitors = competitorRows.map((c) => ({
|
|
@@ -12804,9 +13028,9 @@ async function compositeRoutes(app) {
|
|
|
12804
13028
|
citedDomains: querySnapshots.citedDomains,
|
|
12805
13029
|
rawResponse: querySnapshots.rawResponse,
|
|
12806
13030
|
createdAt: querySnapshots.createdAt
|
|
12807
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
12808
|
-
|
|
12809
|
-
|
|
13031
|
+
}).from(querySnapshots).innerJoin(queries, eq17(querySnapshots.queryId, queries.id)).where(
|
|
13032
|
+
and12(
|
|
13033
|
+
eq17(queries.projectId, project.id),
|
|
12810
13034
|
or4(
|
|
12811
13035
|
sql7`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
12812
13036
|
sql7`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -12814,10 +13038,10 @@ async function compositeRoutes(app) {
|
|
|
12814
13038
|
like(queries.query, pattern)
|
|
12815
13039
|
)
|
|
12816
13040
|
)
|
|
12817
|
-
).orderBy(
|
|
13041
|
+
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
12818
13042
|
const insightMatches = app.db.select().from(insights).where(
|
|
12819
|
-
|
|
12820
|
-
|
|
13043
|
+
and12(
|
|
13044
|
+
eq17(insights.projectId, project.id),
|
|
12821
13045
|
or4(
|
|
12822
13046
|
like(insights.title, pattern),
|
|
12823
13047
|
like(insights.query, pattern),
|
|
@@ -12825,7 +13049,7 @@ async function compositeRoutes(app) {
|
|
|
12825
13049
|
sql7`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
12826
13050
|
)
|
|
12827
13051
|
)
|
|
12828
|
-
).orderBy(
|
|
13052
|
+
).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
|
|
12829
13053
|
const hits = [];
|
|
12830
13054
|
for (const row of snapshotMatches) {
|
|
12831
13055
|
hits.push(buildSnapshotHit(row, rawQuery));
|
|
@@ -12895,7 +13119,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
12895
13119
|
answerText: querySnapshots.answerText,
|
|
12896
13120
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
12897
13121
|
citedDomains: querySnapshots.citedDomains
|
|
12898
|
-
}).from(querySnapshots).where(
|
|
13122
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, [...runIds])).all());
|
|
12899
13123
|
for (const row of rows) {
|
|
12900
13124
|
const list = result.get(row.runId) ?? [];
|
|
12901
13125
|
list.push({
|
|
@@ -13013,8 +13237,8 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
13013
13237
|
// NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
|
|
13014
13238
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
13015
13239
|
avgPosition: sql7`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
13016
|
-
}).from(gscSearchData).where(
|
|
13017
|
-
|
|
13240
|
+
}).from(gscSearchData).where(and12(
|
|
13241
|
+
eq17(gscSearchData.projectId, projectId),
|
|
13018
13242
|
sql7`${gscSearchData.date} >= ${cutoff}`,
|
|
13019
13243
|
sql7`${gscSearchData.impressions} > 0`
|
|
13020
13244
|
)).groupBy(gscSearchData.query).orderBy(sql7`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
@@ -13037,8 +13261,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
13037
13261
|
tooltip,
|
|
13038
13262
|
trend: []
|
|
13039
13263
|
};
|
|
13040
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
13041
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
13264
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
13265
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
13042
13266
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
13043
13267
|
if (!chosen) return empty;
|
|
13044
13268
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -13064,7 +13288,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
13064
13288
|
url: gscUrlInspections.url,
|
|
13065
13289
|
indexingState: gscUrlInspections.indexingState,
|
|
13066
13290
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
13067
|
-
}).from(gscUrlInspections).where(
|
|
13291
|
+
}).from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
13068
13292
|
if (rows.length === 0) return 0;
|
|
13069
13293
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
13070
13294
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -13178,14 +13402,27 @@ function mapInsightRow2(r) {
|
|
|
13178
13402
|
};
|
|
13179
13403
|
}
|
|
13180
13404
|
function mapHealthRow2(r) {
|
|
13405
|
+
const providerBreakdown = {};
|
|
13406
|
+
for (const [provider, entry] of Object.entries(r.providerBreakdown)) {
|
|
13407
|
+
providerBreakdown[provider] = {
|
|
13408
|
+
citedRate: entry.citedRate,
|
|
13409
|
+
mentionRate: entry.mentionRate ?? 0,
|
|
13410
|
+
cited: entry.cited,
|
|
13411
|
+
mentioned: entry.mentioned ?? 0,
|
|
13412
|
+
total: entry.total
|
|
13413
|
+
};
|
|
13414
|
+
}
|
|
13181
13415
|
return {
|
|
13182
13416
|
id: r.id,
|
|
13183
13417
|
projectId: r.projectId,
|
|
13184
13418
|
runId: r.runId ?? null,
|
|
13185
13419
|
overallCitedRate: Number(r.overallCitedRate),
|
|
13420
|
+
// Legacy rows (persisted before v80) have NULL mention columns → 0.
|
|
13421
|
+
overallMentionRate: r.overallMentionRate == null ? 0 : Number(r.overallMentionRate),
|
|
13186
13422
|
totalPairs: r.totalPairs,
|
|
13187
13423
|
citedPairs: r.citedPairs,
|
|
13188
|
-
|
|
13424
|
+
mentionedPairs: r.mentionedPairs ?? 0,
|
|
13425
|
+
providerBreakdown,
|
|
13189
13426
|
createdAt: r.createdAt,
|
|
13190
13427
|
status: "ready"
|
|
13191
13428
|
};
|
|
@@ -13397,6 +13634,7 @@ var SCHEMA_TABLE = {
|
|
|
13397
13634
|
TrafficSourceListResponse: trafficSourceListResponseSchema,
|
|
13398
13635
|
TrafficStatusResponse: trafficStatusResponseSchema,
|
|
13399
13636
|
TrafficSyncResponse: trafficSyncResponseSchema,
|
|
13637
|
+
VisibilityStatsDto: visibilityStatsDtoSchema,
|
|
13400
13638
|
WordpressAuditPageDto: wordpressAuditPageDtoSchema,
|
|
13401
13639
|
WordpressBulkMetaResultDto: wordpressBulkMetaResultDtoSchema,
|
|
13402
13640
|
WordpressDiffDto: wordpressDiffDtoSchema,
|
|
@@ -13622,6 +13860,30 @@ var analyticsWindowParameter = {
|
|
|
13622
13860
|
description: "Time window for analytics queries.",
|
|
13623
13861
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
13624
13862
|
};
|
|
13863
|
+
var sinceQueryParameter = {
|
|
13864
|
+
name: "since",
|
|
13865
|
+
in: "query",
|
|
13866
|
+
description: 'Inclusive lower bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) is the start of that UTC day. Mutually exclusive with "lastRuns".',
|
|
13867
|
+
schema: stringSchema
|
|
13868
|
+
};
|
|
13869
|
+
var untilQueryParameter = {
|
|
13870
|
+
name: "until",
|
|
13871
|
+
in: "query",
|
|
13872
|
+
description: 'Inclusive upper bound on run createdAt (ISO 8601). A date-only value (YYYY-MM-DD) covers the whole UTC day (through 23:59:59.999). Mutually exclusive with "lastRuns".',
|
|
13873
|
+
schema: stringSchema
|
|
13874
|
+
};
|
|
13875
|
+
var lastRunsQueryParameter = {
|
|
13876
|
+
name: "lastRuns",
|
|
13877
|
+
in: "query",
|
|
13878
|
+
description: 'Aggregate only the most recent N answer-visibility runs. Mutually exclusive with "since"/"until".',
|
|
13879
|
+
schema: integerSchema
|
|
13880
|
+
};
|
|
13881
|
+
var groupByProviderQueryParameter = {
|
|
13882
|
+
name: "groupBy",
|
|
13883
|
+
in: "query",
|
|
13884
|
+
description: 'Set to "provider" to include a per-provider breakdown whose counts sum to the pooled counts.',
|
|
13885
|
+
schema: { type: "string", enum: ["provider"] }
|
|
13886
|
+
};
|
|
13625
13887
|
var wordpressEnvQueryParameter = {
|
|
13626
13888
|
name: "env",
|
|
13627
13889
|
in: "query",
|
|
@@ -14348,6 +14610,19 @@ var routeCatalog = [
|
|
|
14348
14610
|
404: errorResponse("Project not found.")
|
|
14349
14611
|
}
|
|
14350
14612
|
},
|
|
14613
|
+
{
|
|
14614
|
+
method: "get",
|
|
14615
|
+
path: "/api/v1/projects/{name}/visibility-stats",
|
|
14616
|
+
summary: "Get aggregated mention/citation stats per query",
|
|
14617
|
+
description: "Per-query mention (answer-text) and citation (source-list) counts with a sample size, pooled across many answer-visibility runs (probe-excluded). Tri-state aware: `checked` counts only snapshots where answerMentioned was recorded (null = not checked is excluded). Lets a consumer compute confidence-aware (e.g. Wilson) proportions without N+1 run fetches. With no since/until/lastRuns, EVERY completed/partial answer-visibility run is pooled \u2014 `window.runCount` reports how many; bound the window with lastRuns or since/until for a recent sample. Set groupBy=provider for a per-provider breakdown whose counts sum to the pooled counts.",
|
|
14618
|
+
tags: ["analytics"],
|
|
14619
|
+
parameters: [nameParameter, sinceQueryParameter, untilQueryParameter, lastRunsQueryParameter, groupByProviderQueryParameter],
|
|
14620
|
+
responses: {
|
|
14621
|
+
200: jsonResponse("Aggregated visibility stats returned.", "VisibilityStatsDto"),
|
|
14622
|
+
400: errorResponse("Invalid query parameters."),
|
|
14623
|
+
404: errorResponse("Project not found.")
|
|
14624
|
+
}
|
|
14625
|
+
},
|
|
14351
14626
|
{
|
|
14352
14627
|
method: "get",
|
|
14353
14628
|
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
@@ -17613,7 +17888,7 @@ async function settingsRoutes(app, opts) {
|
|
|
17613
17888
|
|
|
17614
17889
|
// ../api-routes/src/keys.ts
|
|
17615
17890
|
import crypto12 from "crypto";
|
|
17616
|
-
import { desc as
|
|
17891
|
+
import { desc as desc10, eq as eq18 } from "drizzle-orm";
|
|
17617
17892
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
17618
17893
|
function toApiKeyDto(row) {
|
|
17619
17894
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -17630,7 +17905,7 @@ function toApiKeyDto(row) {
|
|
|
17630
17905
|
}
|
|
17631
17906
|
async function keysRoutes(app) {
|
|
17632
17907
|
app.get("/keys", async () => {
|
|
17633
|
-
const rows = app.db.select().from(apiKeys).orderBy(
|
|
17908
|
+
const rows = app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
17634
17909
|
return { keys: rows.map(toApiKeyDto) };
|
|
17635
17910
|
});
|
|
17636
17911
|
app.get("/keys/self", async (request) => {
|
|
@@ -17638,7 +17913,7 @@ async function keysRoutes(app) {
|
|
|
17638
17913
|
if (!id) {
|
|
17639
17914
|
throw notFound("API key", "self");
|
|
17640
17915
|
}
|
|
17641
|
-
const row = app.db.select().from(apiKeys).where(
|
|
17916
|
+
const row = app.db.select().from(apiKeys).where(eq18(apiKeys.id, id)).get();
|
|
17642
17917
|
if (!row) {
|
|
17643
17918
|
throw notFound("API key", id);
|
|
17644
17919
|
}
|
|
@@ -17690,7 +17965,7 @@ async function keysRoutes(app) {
|
|
|
17690
17965
|
app.post("/keys/:id/revoke", async (request) => {
|
|
17691
17966
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
17692
17967
|
const { id } = request.params;
|
|
17693
|
-
const row = app.db.select().from(apiKeys).where(
|
|
17968
|
+
const row = app.db.select().from(apiKeys).where(eq18(apiKeys.id, id)).get();
|
|
17694
17969
|
if (!row) {
|
|
17695
17970
|
throw notFound("API key", id);
|
|
17696
17971
|
}
|
|
@@ -17702,7 +17977,7 @@ async function keysRoutes(app) {
|
|
|
17702
17977
|
}
|
|
17703
17978
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17704
17979
|
app.db.transaction((tx) => {
|
|
17705
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
17980
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq18(apiKeys.id, id)).run();
|
|
17706
17981
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
17707
17982
|
actor: "api",
|
|
17708
17983
|
action: "api-key.revoked",
|
|
@@ -17775,7 +18050,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
17775
18050
|
|
|
17776
18051
|
// ../api-routes/src/schedules.ts
|
|
17777
18052
|
import crypto13 from "crypto";
|
|
17778
|
-
import { and as
|
|
18053
|
+
import { and as and13, eq as eq19 } from "drizzle-orm";
|
|
17779
18054
|
function parseKindParam(raw) {
|
|
17780
18055
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
17781
18056
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -17802,7 +18077,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17802
18077
|
if (!sourceId) {
|
|
17803
18078
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
17804
18079
|
}
|
|
17805
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
18080
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq19(trafficSources.id, sourceId)).get();
|
|
17806
18081
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
17807
18082
|
throw notFound("Traffic source", sourceId);
|
|
17808
18083
|
}
|
|
@@ -17847,7 +18122,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17847
18122
|
}
|
|
17848
18123
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17849
18124
|
const enabledBool = enabled !== false;
|
|
17850
|
-
const existing = app.db.select().from(schedules).where(
|
|
18125
|
+
const existing = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17851
18126
|
if (existing) {
|
|
17852
18127
|
app.db.update(schedules).set({
|
|
17853
18128
|
cronExpr,
|
|
@@ -17857,7 +18132,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
17857
18132
|
sourceId: sourceId ?? null,
|
|
17858
18133
|
enabled: enabledBool,
|
|
17859
18134
|
updatedAt: now
|
|
17860
|
-
}).where(
|
|
18135
|
+
}).where(eq19(schedules.id, existing.id)).run();
|
|
17861
18136
|
} else {
|
|
17862
18137
|
app.db.insert(schedules).values({
|
|
17863
18138
|
id: crypto13.randomUUID(),
|
|
@@ -17881,13 +18156,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
17881
18156
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
17882
18157
|
});
|
|
17883
18158
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
17884
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18159
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17885
18160
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
17886
18161
|
});
|
|
17887
18162
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
17888
18163
|
const project = resolveProject(app.db, request.params.name);
|
|
17889
18164
|
const kind = parseKindParam(request.query?.kind);
|
|
17890
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18165
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17891
18166
|
if (!schedule) {
|
|
17892
18167
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
17893
18168
|
}
|
|
@@ -17896,11 +18171,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
17896
18171
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
17897
18172
|
const project = resolveProject(app.db, request.params.name);
|
|
17898
18173
|
const kind = parseKindParam(request.query?.kind);
|
|
17899
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18174
|
+
const schedule = app.db.select().from(schedules).where(and13(eq19(schedules.projectId, project.id), eq19(schedules.kind, kind))).get();
|
|
17900
18175
|
if (!schedule) {
|
|
17901
18176
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
17902
18177
|
}
|
|
17903
|
-
app.db.delete(schedules).where(
|
|
18178
|
+
app.db.delete(schedules).where(eq19(schedules.id, schedule.id)).run();
|
|
17904
18179
|
writeAuditLog(app.db, {
|
|
17905
18180
|
projectId: project.id,
|
|
17906
18181
|
actor: "api",
|
|
@@ -17933,7 +18208,7 @@ function formatSchedule(row) {
|
|
|
17933
18208
|
|
|
17934
18209
|
// ../api-routes/src/notifications.ts
|
|
17935
18210
|
import crypto14 from "crypto";
|
|
17936
|
-
import { eq as
|
|
18211
|
+
import { eq as eq20 } from "drizzle-orm";
|
|
17937
18212
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
17938
18213
|
async function notificationRoutes(app, opts = {}) {
|
|
17939
18214
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -17973,22 +18248,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
17973
18248
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
17974
18249
|
});
|
|
17975
18250
|
return reply.status(201).send({
|
|
17976
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
18251
|
+
...formatNotification(app.db.select().from(notifications).where(eq20(notifications.id, id)).get()),
|
|
17977
18252
|
webhookSecret
|
|
17978
18253
|
});
|
|
17979
18254
|
});
|
|
17980
18255
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
17981
18256
|
const project = resolveProject(app.db, request.params.name);
|
|
17982
|
-
const rows = app.db.select().from(notifications).where(
|
|
18257
|
+
const rows = app.db.select().from(notifications).where(eq20(notifications.projectId, project.id)).all();
|
|
17983
18258
|
return reply.send(rows.map(formatNotification));
|
|
17984
18259
|
});
|
|
17985
18260
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
17986
18261
|
const project = resolveProject(app.db, request.params.name);
|
|
17987
|
-
const notification = app.db.select().from(notifications).where(
|
|
18262
|
+
const notification = app.db.select().from(notifications).where(eq20(notifications.id, request.params.id)).get();
|
|
17988
18263
|
if (!notification || notification.projectId !== project.id) {
|
|
17989
18264
|
throw notFound("Notification", request.params.id);
|
|
17990
18265
|
}
|
|
17991
|
-
app.db.delete(notifications).where(
|
|
18266
|
+
app.db.delete(notifications).where(eq20(notifications.id, notification.id)).run();
|
|
17992
18267
|
writeAuditLog(app.db, {
|
|
17993
18268
|
projectId: project.id,
|
|
17994
18269
|
actor: "api",
|
|
@@ -18000,7 +18275,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18000
18275
|
});
|
|
18001
18276
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
18002
18277
|
const project = resolveProject(app.db, request.params.name);
|
|
18003
|
-
const notification = app.db.select().from(notifications).where(
|
|
18278
|
+
const notification = app.db.select().from(notifications).where(eq20(notifications.id, request.params.id)).get();
|
|
18004
18279
|
if (!notification || notification.projectId !== project.id) {
|
|
18005
18280
|
throw notFound("Notification", request.params.id);
|
|
18006
18281
|
}
|
|
@@ -18053,7 +18328,7 @@ function formatNotification(row) {
|
|
|
18053
18328
|
|
|
18054
18329
|
// ../api-routes/src/google.ts
|
|
18055
18330
|
import crypto17 from "crypto";
|
|
18056
|
-
import { eq as
|
|
18331
|
+
import { eq as eq21, and as and14, desc as desc11, sql as sql8, inArray as inArray10 } from "drizzle-orm";
|
|
18057
18332
|
|
|
18058
18333
|
// ../api-routes/src/gbp-summary.ts
|
|
18059
18334
|
function computeMetricTotals(rows) {
|
|
@@ -19779,7 +20054,7 @@ async function googleRoutes(app, opts) {
|
|
|
19779
20054
|
if (!projectId) {
|
|
19780
20055
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
19781
20056
|
}
|
|
19782
|
-
const project = app.db.select().from(projects).where(
|
|
20057
|
+
const project = app.db.select().from(projects).where(eq21(projects.id, projectId)).get();
|
|
19783
20058
|
if (!project) {
|
|
19784
20059
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
19785
20060
|
}
|
|
@@ -19911,14 +20186,14 @@ async function googleRoutes(app, opts) {
|
|
|
19911
20186
|
if (opts.onGscSyncRequested) {
|
|
19912
20187
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
19913
20188
|
}
|
|
19914
|
-
const run = app.db.select().from(runs).where(
|
|
20189
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
19915
20190
|
return run;
|
|
19916
20191
|
});
|
|
19917
20192
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
19918
20193
|
const project = resolveProject(app.db, request.params.name);
|
|
19919
20194
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
19920
20195
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
19921
|
-
const conditions = [
|
|
20196
|
+
const conditions = [eq21(gscSearchData.projectId, project.id)];
|
|
19922
20197
|
if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
|
|
19923
20198
|
else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
|
|
19924
20199
|
if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -19926,7 +20201,7 @@ async function googleRoutes(app, opts) {
|
|
|
19926
20201
|
if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
19927
20202
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
19928
20203
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
19929
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
20204
|
+
const rows = app.db.select().from(gscSearchData).where(and14(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
19930
20205
|
return rows.map((r) => ({
|
|
19931
20206
|
date: r.date,
|
|
19932
20207
|
query: r.query,
|
|
@@ -19943,7 +20218,7 @@ async function googleRoutes(app, opts) {
|
|
|
19943
20218
|
const project = resolveProject(app.db, request.params.name);
|
|
19944
20219
|
const { startDate, endDate } = request.query;
|
|
19945
20220
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
19946
|
-
const conditions = [
|
|
20221
|
+
const conditions = [eq21(gscSearchData.projectId, project.id)];
|
|
19947
20222
|
if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
|
|
19948
20223
|
else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
|
|
19949
20224
|
if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
|
|
@@ -19951,7 +20226,7 @@ async function googleRoutes(app, opts) {
|
|
|
19951
20226
|
date: gscSearchData.date,
|
|
19952
20227
|
clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
19953
20228
|
impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
19954
|
-
}).from(gscSearchData).where(
|
|
20229
|
+
}).from(gscSearchData).where(and14(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
19955
20230
|
const daily = rows.map((r) => ({
|
|
19956
20231
|
date: r.date,
|
|
19957
20232
|
clicks: r.clicks,
|
|
@@ -20034,9 +20309,9 @@ async function googleRoutes(app, opts) {
|
|
|
20034
20309
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
20035
20310
|
const project = resolveProject(app.db, request.params.name);
|
|
20036
20311
|
const { url, limit } = request.query;
|
|
20037
|
-
const conditions = [
|
|
20038
|
-
if (url) conditions.push(
|
|
20039
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
20312
|
+
const conditions = [eq21(gscUrlInspections.projectId, project.id)];
|
|
20313
|
+
if (url) conditions.push(eq21(gscUrlInspections.url, url));
|
|
20314
|
+
const rows = app.db.select().from(gscUrlInspections).where(and14(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
20040
20315
|
return rows.map((r) => ({
|
|
20041
20316
|
id: r.id,
|
|
20042
20317
|
url: r.url,
|
|
@@ -20055,7 +20330,7 @@ async function googleRoutes(app, opts) {
|
|
|
20055
20330
|
});
|
|
20056
20331
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
20057
20332
|
const project = resolveProject(app.db, request.params.name);
|
|
20058
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20333
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20059
20334
|
const byUrl = /* @__PURE__ */ new Map();
|
|
20060
20335
|
for (const row of allInspections) {
|
|
20061
20336
|
const existing = byUrl.get(row.url);
|
|
@@ -20083,7 +20358,7 @@ async function googleRoutes(app, opts) {
|
|
|
20083
20358
|
});
|
|
20084
20359
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
20085
20360
|
const project = resolveProject(app.db, request.params.name);
|
|
20086
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20361
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20087
20362
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
20088
20363
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20089
20364
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -20132,7 +20407,7 @@ async function googleRoutes(app, opts) {
|
|
|
20132
20407
|
const total = latestByUrl.size;
|
|
20133
20408
|
const indexed = indexedUrls.length;
|
|
20134
20409
|
const notIndexed = notIndexedUrls.length;
|
|
20135
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
20410
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq21(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
20136
20411
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
20137
20412
|
const formatRow = (r) => ({
|
|
20138
20413
|
id: r.id,
|
|
@@ -20183,7 +20458,7 @@ async function googleRoutes(app, opts) {
|
|
|
20183
20458
|
const project = resolveProject(app.db, request.params.name);
|
|
20184
20459
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
20185
20460
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
20186
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
20461
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq21(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
20187
20462
|
return rows.map((r) => ({
|
|
20188
20463
|
date: r.date,
|
|
20189
20464
|
indexed: r.indexed,
|
|
@@ -20252,7 +20527,7 @@ async function googleRoutes(app, opts) {
|
|
|
20252
20527
|
if (opts.onInspectSitemapRequested) {
|
|
20253
20528
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
20254
20529
|
}
|
|
20255
|
-
const run = app.db.select().from(runs).where(
|
|
20530
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
20256
20531
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
20257
20532
|
});
|
|
20258
20533
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -20279,7 +20554,7 @@ async function googleRoutes(app, opts) {
|
|
|
20279
20554
|
if (opts.onInspectSitemapRequested) {
|
|
20280
20555
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
20281
20556
|
}
|
|
20282
|
-
const run = app.db.select().from(runs).where(
|
|
20557
|
+
const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
|
|
20283
20558
|
return run;
|
|
20284
20559
|
});
|
|
20285
20560
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -20326,7 +20601,7 @@ async function googleRoutes(app, opts) {
|
|
|
20326
20601
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
20327
20602
|
let urlsToNotify = request.body?.urls ?? [];
|
|
20328
20603
|
if (request.body?.allUnindexed) {
|
|
20329
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
20604
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq21(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20330
20605
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20331
20606
|
for (const row of allInspections) {
|
|
20332
20607
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -20437,7 +20712,7 @@ async function googleRoutes(app, opts) {
|
|
|
20437
20712
|
};
|
|
20438
20713
|
}
|
|
20439
20714
|
function listSelectionResponse(projectId) {
|
|
20440
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
20715
|
+
const rows = app.db.select().from(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).all();
|
|
20441
20716
|
const dtos = rows.map(rowToDto2);
|
|
20442
20717
|
return {
|
|
20443
20718
|
locations: dtos,
|
|
@@ -20446,15 +20721,15 @@ async function googleRoutes(app, opts) {
|
|
|
20446
20721
|
};
|
|
20447
20722
|
}
|
|
20448
20723
|
function clearGbpProjectData(tx, projectId) {
|
|
20449
|
-
tx.delete(gbpDailyMetrics).where(
|
|
20450
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
20451
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
20452
|
-
tx.delete(gbpPlaceActions).where(
|
|
20453
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
20454
|
-
tx.delete(gbpLocations).where(
|
|
20724
|
+
tx.delete(gbpDailyMetrics).where(eq21(gbpDailyMetrics.projectId, projectId)).run();
|
|
20725
|
+
tx.delete(gbpKeywordImpressions).where(eq21(gbpKeywordImpressions.projectId, projectId)).run();
|
|
20726
|
+
tx.delete(gbpKeywordMonthly).where(eq21(gbpKeywordMonthly.projectId, projectId)).run();
|
|
20727
|
+
tx.delete(gbpPlaceActions).where(eq21(gbpPlaceActions.projectId, projectId)).run();
|
|
20728
|
+
tx.delete(gbpLodgingSnapshots).where(eq21(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
20729
|
+
tx.delete(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).run();
|
|
20455
20730
|
}
|
|
20456
20731
|
function currentProjectAccount(projectId) {
|
|
20457
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
20732
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq21(gbpLocations.projectId, projectId)).limit(1).get();
|
|
20458
20733
|
return row?.accountName ?? null;
|
|
20459
20734
|
}
|
|
20460
20735
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -20524,7 +20799,7 @@ async function googleRoutes(app, opts) {
|
|
|
20524
20799
|
app.db.transaction((tx) => {
|
|
20525
20800
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
20526
20801
|
for (const remote of remoteLocations) {
|
|
20527
|
-
const existing = tx.select().from(gbpLocations).where(
|
|
20802
|
+
const existing = tx.select().from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.locationName, remote.name))).get();
|
|
20528
20803
|
if (existing) {
|
|
20529
20804
|
tx.update(gbpLocations).set({
|
|
20530
20805
|
accountName,
|
|
@@ -20535,7 +20810,7 @@ async function googleRoutes(app, opts) {
|
|
|
20535
20810
|
placeId: remote.metadata?.placeId ?? null,
|
|
20536
20811
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
20537
20812
|
updatedAt: now
|
|
20538
|
-
}).where(
|
|
20813
|
+
}).where(eq21(gbpLocations.id, existing.id)).run();
|
|
20539
20814
|
} else {
|
|
20540
20815
|
tx.insert(gbpLocations).values({
|
|
20541
20816
|
id: crypto17.randomUUID(),
|
|
@@ -20618,11 +20893,11 @@ async function googleRoutes(app, opts) {
|
|
|
20618
20893
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
20619
20894
|
}
|
|
20620
20895
|
const { selected } = parsed.data;
|
|
20621
|
-
const existing = app.db.select().from(gbpLocations).where(
|
|
20896
|
+
const existing = app.db.select().from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.locationName, locationName))).get();
|
|
20622
20897
|
if (!existing) throw notFound("GBP location", locationName);
|
|
20623
20898
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20624
20899
|
app.db.transaction((tx) => {
|
|
20625
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
20900
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq21(gbpLocations.id, existing.id)).run();
|
|
20626
20901
|
writeAuditLog(tx, {
|
|
20627
20902
|
projectId: project.id,
|
|
20628
20903
|
actor: "api",
|
|
@@ -20631,7 +20906,7 @@ async function googleRoutes(app, opts) {
|
|
|
20631
20906
|
entityId: locationName
|
|
20632
20907
|
});
|
|
20633
20908
|
});
|
|
20634
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
20909
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq21(gbpLocations.id, existing.id)).get();
|
|
20635
20910
|
return rowToDto2(refreshed);
|
|
20636
20911
|
});
|
|
20637
20912
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -20675,10 +20950,10 @@ async function googleRoutes(app, opts) {
|
|
|
20675
20950
|
});
|
|
20676
20951
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
20677
20952
|
const project = resolveProject(app.db, request.params.name);
|
|
20678
|
-
const conditions = [
|
|
20679
|
-
if (request.query.locationName) conditions.push(
|
|
20680
|
-
if (request.query.metric) conditions.push(
|
|
20681
|
-
const rows = app.db.select().from(gbpDailyMetrics).where(
|
|
20953
|
+
const conditions = [eq21(gbpDailyMetrics.projectId, project.id)];
|
|
20954
|
+
if (request.query.locationName) conditions.push(eq21(gbpDailyMetrics.locationName, request.query.locationName));
|
|
20955
|
+
if (request.query.metric) conditions.push(eq21(gbpDailyMetrics.metric, request.query.metric));
|
|
20956
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and14(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
20682
20957
|
return {
|
|
20683
20958
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
20684
20959
|
total: rows.length
|
|
@@ -20686,9 +20961,9 @@ async function googleRoutes(app, opts) {
|
|
|
20686
20961
|
});
|
|
20687
20962
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
20688
20963
|
const project = resolveProject(app.db, request.params.name);
|
|
20689
|
-
const conditions = [
|
|
20690
|
-
if (request.query.locationName) conditions.push(
|
|
20691
|
-
const rows = app.db.select().from(gbpKeywordImpressions).where(
|
|
20964
|
+
const conditions = [eq21(gbpKeywordImpressions.projectId, project.id)];
|
|
20965
|
+
if (request.query.locationName) conditions.push(eq21(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
20966
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and14(...conditions)).all();
|
|
20692
20967
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
20693
20968
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
20694
20969
|
return {
|
|
@@ -20706,9 +20981,9 @@ async function googleRoutes(app, opts) {
|
|
|
20706
20981
|
});
|
|
20707
20982
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
20708
20983
|
const project = resolveProject(app.db, request.params.name);
|
|
20709
|
-
const conditions = [
|
|
20710
|
-
if (request.query.locationName) conditions.push(
|
|
20711
|
-
const rows = app.db.select().from(gbpPlaceActions).where(
|
|
20984
|
+
const conditions = [eq21(gbpPlaceActions.projectId, project.id)];
|
|
20985
|
+
if (request.query.locationName) conditions.push(eq21(gbpPlaceActions.locationName, request.query.locationName));
|
|
20986
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and14(...conditions)).all();
|
|
20712
20987
|
return {
|
|
20713
20988
|
placeActions: rows.map((r) => ({
|
|
20714
20989
|
locationName: r.locationName,
|
|
@@ -20723,9 +20998,9 @@ async function googleRoutes(app, opts) {
|
|
|
20723
20998
|
});
|
|
20724
20999
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
20725
21000
|
const project = resolveProject(app.db, request.params.name);
|
|
20726
|
-
const conditions = [
|
|
20727
|
-
if (request.query.locationName) conditions.push(
|
|
20728
|
-
const rows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21001
|
+
const conditions = [eq21(gbpLodgingSnapshots.projectId, project.id)];
|
|
21002
|
+
if (request.query.locationName) conditions.push(eq21(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
21003
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and14(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
20729
21004
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
20730
21005
|
for (const row of rows) {
|
|
20731
21006
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -20740,9 +21015,9 @@ async function googleRoutes(app, opts) {
|
|
|
20740
21015
|
});
|
|
20741
21016
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
20742
21017
|
const project = resolveProject(app.db, request.params.name);
|
|
20743
|
-
const conditions = [
|
|
20744
|
-
if (request.query.locationName) conditions.push(
|
|
20745
|
-
const rows = app.db.select().from(gbpPlaceDetails).where(
|
|
21018
|
+
const conditions = [eq21(gbpPlaceDetails.projectId, project.id)];
|
|
21019
|
+
if (request.query.locationName) conditions.push(eq21(gbpPlaceDetails.locationName, request.query.locationName));
|
|
21020
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and14(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
20746
21021
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
20747
21022
|
for (const row of rows) {
|
|
20748
21023
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -20761,7 +21036,7 @@ async function googleRoutes(app, opts) {
|
|
|
20761
21036
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
20762
21037
|
const project = resolveProject(app.db, request.params.name);
|
|
20763
21038
|
const locationName = request.query.locationName ?? null;
|
|
20764
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(
|
|
21039
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and14(eq21(gbpLocations.projectId, project.id), eq21(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
20765
21040
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
20766
21041
|
if (locationNames.length === 0) {
|
|
20767
21042
|
return buildGbpSummary({
|
|
@@ -20774,10 +21049,10 @@ async function googleRoutes(app, opts) {
|
|
|
20774
21049
|
lodging: []
|
|
20775
21050
|
});
|
|
20776
21051
|
}
|
|
20777
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(
|
|
20778
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(
|
|
20779
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(
|
|
20780
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21052
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and14(eq21(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
|
|
21053
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and14(eq21(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
21054
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and14(eq21(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
|
|
21055
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and14(eq21(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
20781
21056
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
20782
21057
|
for (const row of lodgingRows) {
|
|
20783
21058
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -20798,7 +21073,7 @@ async function googleRoutes(app, opts) {
|
|
|
20798
21073
|
|
|
20799
21074
|
// ../api-routes/src/ads.ts
|
|
20800
21075
|
import crypto18 from "crypto";
|
|
20801
|
-
import { eq as
|
|
21076
|
+
import { eq as eq22, and as and15, asc as asc2, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
|
|
20802
21077
|
function statusDto(row) {
|
|
20803
21078
|
if (!row) return { connected: false };
|
|
20804
21079
|
return {
|
|
@@ -20847,7 +21122,7 @@ async function adsRoutes(app, opts) {
|
|
|
20847
21122
|
createdAt: existingCfg?.createdAt ?? now,
|
|
20848
21123
|
updatedAt: now
|
|
20849
21124
|
});
|
|
20850
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
21125
|
+
const existingRow = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20851
21126
|
app.db.transaction((tx) => {
|
|
20852
21127
|
if (existingRow) {
|
|
20853
21128
|
tx.update(adsConnections).set({
|
|
@@ -20857,7 +21132,7 @@ async function adsRoutes(app, opts) {
|
|
|
20857
21132
|
timezone: account.timezone,
|
|
20858
21133
|
status: account.status,
|
|
20859
21134
|
updatedAt: now
|
|
20860
|
-
}).where(
|
|
21135
|
+
}).where(eq22(adsConnections.id, existingRow.id)).run();
|
|
20861
21136
|
} else {
|
|
20862
21137
|
tx.insert(adsConnections).values({
|
|
20863
21138
|
id: crypto18.randomUUID(),
|
|
@@ -20879,16 +21154,16 @@ async function adsRoutes(app, opts) {
|
|
|
20879
21154
|
entityId: account.id
|
|
20880
21155
|
}));
|
|
20881
21156
|
});
|
|
20882
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21157
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20883
21158
|
return statusDto(row);
|
|
20884
21159
|
}
|
|
20885
21160
|
);
|
|
20886
21161
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
20887
21162
|
const project = resolveProject(app.db, request.params.name);
|
|
20888
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21163
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20889
21164
|
if (row) {
|
|
20890
21165
|
app.db.transaction((tx) => {
|
|
20891
|
-
tx.delete(adsConnections).where(
|
|
21166
|
+
tx.delete(adsConnections).where(eq22(adsConnections.id, row.id)).run();
|
|
20892
21167
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
20893
21168
|
projectId: project.id,
|
|
20894
21169
|
actor: "api",
|
|
@@ -20904,19 +21179,19 @@ async function adsRoutes(app, opts) {
|
|
|
20904
21179
|
});
|
|
20905
21180
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
20906
21181
|
const project = resolveProject(app.db, request.params.name);
|
|
20907
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21182
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20908
21183
|
return statusDto(row);
|
|
20909
21184
|
});
|
|
20910
21185
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
20911
21186
|
const project = resolveProject(app.db, request.params.name);
|
|
20912
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21187
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
20913
21188
|
if (!row) {
|
|
20914
21189
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
20915
21190
|
}
|
|
20916
|
-
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
20917
|
-
|
|
20918
|
-
|
|
20919
|
-
|
|
21191
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and15(
|
|
21192
|
+
eq22(runs.projectId, project.id),
|
|
21193
|
+
eq22(runs.kind, RunKinds["ads-sync"]),
|
|
21194
|
+
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
20920
21195
|
)).get();
|
|
20921
21196
|
if (inFlight) {
|
|
20922
21197
|
const existing = { runId: inFlight.id, status: inFlight.status };
|
|
@@ -20937,9 +21212,9 @@ async function adsRoutes(app, opts) {
|
|
|
20937
21212
|
});
|
|
20938
21213
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
20939
21214
|
const project = resolveProject(app.db, request.params.name);
|
|
20940
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
20941
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
20942
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
21215
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq22(adsCampaigns.projectId, project.id)).all();
|
|
21216
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq22(adsAdGroups.projectId, project.id)).all();
|
|
21217
|
+
const adRows = app.db.select().from(adsAds).where(eq22(adsAds.projectId, project.id)).all();
|
|
20943
21218
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
20944
21219
|
for (const ad of adRows) {
|
|
20945
21220
|
const dto = {
|
|
@@ -20993,12 +21268,12 @@ async function adsRoutes(app, opts) {
|
|
|
20993
21268
|
}
|
|
20994
21269
|
parsedLevel = result.data;
|
|
20995
21270
|
}
|
|
20996
|
-
const conditions = [
|
|
20997
|
-
if (parsedLevel) conditions.push(
|
|
20998
|
-
if (entityId) conditions.push(
|
|
21271
|
+
const conditions = [eq22(adsInsightsDaily.projectId, project.id)];
|
|
21272
|
+
if (parsedLevel) conditions.push(eq22(adsInsightsDaily.level, parsedLevel));
|
|
21273
|
+
if (entityId) conditions.push(eq22(adsInsightsDaily.entityId, entityId));
|
|
20999
21274
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
21000
21275
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
21001
|
-
const rows = app.db.select().from(adsInsightsDaily).where(
|
|
21276
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and15(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
|
|
21002
21277
|
const dtoRows = rows.map((row) => ({
|
|
21003
21278
|
level: row.level,
|
|
21004
21279
|
entityId: row.entityId,
|
|
@@ -21009,19 +21284,19 @@ async function adsRoutes(app, opts) {
|
|
|
21009
21284
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
21010
21285
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
21011
21286
|
}));
|
|
21012
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
21287
|
+
const conn = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
21013
21288
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
21014
21289
|
return response;
|
|
21015
21290
|
});
|
|
21016
21291
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
21017
21292
|
const project = resolveProject(app.db, request.params.name);
|
|
21018
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21019
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
21020
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
21021
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
21022
|
-
const campaignInsights = app.db.select().from(adsInsightsDaily).where(
|
|
21023
|
-
|
|
21024
|
-
|
|
21293
|
+
const row = app.db.select().from(adsConnections).where(eq22(adsConnections.projectId, project.id)).get();
|
|
21294
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq22(adsCampaigns.projectId, project.id)).all().length;
|
|
21295
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq22(adsAdGroups.projectId, project.id)).all().length;
|
|
21296
|
+
const adCount = app.db.select().from(adsAds).where(eq22(adsAds.projectId, project.id)).all().length;
|
|
21297
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and15(
|
|
21298
|
+
eq22(adsInsightsDaily.projectId, project.id),
|
|
21299
|
+
eq22(adsInsightsDaily.level, "campaign")
|
|
21025
21300
|
)).all();
|
|
21026
21301
|
let impressions = 0;
|
|
21027
21302
|
let clicks = 0;
|
|
@@ -21058,7 +21333,7 @@ async function adsRoutes(app, opts) {
|
|
|
21058
21333
|
|
|
21059
21334
|
// ../api-routes/src/bing.ts
|
|
21060
21335
|
import crypto19 from "crypto";
|
|
21061
|
-
import { eq as
|
|
21336
|
+
import { eq as eq23, and as and16, desc as desc12 } from "drizzle-orm";
|
|
21062
21337
|
|
|
21063
21338
|
// ../integration-bing/src/constants.ts
|
|
21064
21339
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -21432,7 +21707,7 @@ async function bingRoutes(app, opts) {
|
|
|
21432
21707
|
const store = requireConnectionStore();
|
|
21433
21708
|
const project = resolveProject(app.db, request.params.name);
|
|
21434
21709
|
requireConnection(store, project.canonicalDomain);
|
|
21435
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21710
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
21436
21711
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21437
21712
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
21438
21713
|
for (const row of allInspections) {
|
|
@@ -21521,7 +21796,7 @@ async function bingRoutes(app, opts) {
|
|
|
21521
21796
|
const project = resolveProject(app.db, request.params.name);
|
|
21522
21797
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
21523
21798
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
21524
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
21799
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq23(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
21525
21800
|
return rows.map((r) => ({
|
|
21526
21801
|
date: r.date,
|
|
21527
21802
|
indexed: r.indexed,
|
|
@@ -21533,8 +21808,8 @@ async function bingRoutes(app, opts) {
|
|
|
21533
21808
|
requireConnectionStore();
|
|
21534
21809
|
const project = resolveProject(app.db, request.params.name);
|
|
21535
21810
|
const { url, limit } = request.query;
|
|
21536
|
-
const whereClause = url ?
|
|
21537
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
21811
|
+
const whereClause = url ? and16(eq23(bingUrlInspections.projectId, project.id), eq23(bingUrlInspections.url, url)) : eq23(bingUrlInspections.projectId, project.id);
|
|
21812
|
+
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();
|
|
21538
21813
|
return filtered.map((r) => ({
|
|
21539
21814
|
id: r.id,
|
|
21540
21815
|
url: r.url,
|
|
@@ -21623,7 +21898,7 @@ async function bingRoutes(app, opts) {
|
|
|
21623
21898
|
anchorCount: result.AnchorCount ?? null,
|
|
21624
21899
|
discoveryDate
|
|
21625
21900
|
}).run();
|
|
21626
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
21901
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq23(runs.id, runId)).run();
|
|
21627
21902
|
return {
|
|
21628
21903
|
id,
|
|
21629
21904
|
url,
|
|
@@ -21639,7 +21914,7 @@ async function bingRoutes(app, opts) {
|
|
|
21639
21914
|
} catch (e) {
|
|
21640
21915
|
const msg = e instanceof Error ? e.message : String(e);
|
|
21641
21916
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
21642
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
21917
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
|
|
21643
21918
|
throw e;
|
|
21644
21919
|
}
|
|
21645
21920
|
});
|
|
@@ -21666,7 +21941,7 @@ async function bingRoutes(app, opts) {
|
|
|
21666
21941
|
} else {
|
|
21667
21942
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
21668
21943
|
}
|
|
21669
|
-
const run = app.db.select().from(runs).where(
|
|
21944
|
+
const run = app.db.select().from(runs).where(eq23(runs.id, runId)).get();
|
|
21670
21945
|
return run;
|
|
21671
21946
|
});
|
|
21672
21947
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -21678,7 +21953,7 @@ async function bingRoutes(app, opts) {
|
|
|
21678
21953
|
}
|
|
21679
21954
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
21680
21955
|
if (request.body?.allUnindexed) {
|
|
21681
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
21956
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq23(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
21682
21957
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21683
21958
|
for (const row of allInspections) {
|
|
21684
21959
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21765,14 +22040,14 @@ async function bingRoutes(app, opts) {
|
|
|
21765
22040
|
import fs from "fs";
|
|
21766
22041
|
import path from "path";
|
|
21767
22042
|
import os from "os";
|
|
21768
|
-
import { eq as
|
|
22043
|
+
import { eq as eq24, and as and17 } from "drizzle-orm";
|
|
21769
22044
|
function getScreenshotDir() {
|
|
21770
22045
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
21771
22046
|
}
|
|
21772
22047
|
async function cdpRoutes(app, opts) {
|
|
21773
22048
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
21774
22049
|
const { snapshotId } = request.params;
|
|
21775
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
22050
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq24(querySnapshots.id, snapshotId)).get();
|
|
21776
22051
|
if (!snapshot?.screenshotPath) {
|
|
21777
22052
|
const err = notFound("Screenshot", snapshotId);
|
|
21778
22053
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21839,7 +22114,7 @@ async function cdpRoutes(app, opts) {
|
|
|
21839
22114
|
async (request, reply) => {
|
|
21840
22115
|
const project = resolveProject(app.db, request.params.name);
|
|
21841
22116
|
const { runId } = request.params;
|
|
21842
|
-
const run = app.db.select().from(runs).where(
|
|
22117
|
+
const run = app.db.select().from(runs).where(and17(eq24(runs.id, runId), eq24(runs.projectId, project.id))).get();
|
|
21843
22118
|
if (!run) {
|
|
21844
22119
|
const err = notFound("Run", runId);
|
|
21845
22120
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -21852,8 +22127,8 @@ async function cdpRoutes(app, opts) {
|
|
|
21852
22127
|
citedDomains: querySnapshots.citedDomains,
|
|
21853
22128
|
screenshotPath: querySnapshots.screenshotPath,
|
|
21854
22129
|
rawResponse: querySnapshots.rawResponse
|
|
21855
|
-
}).from(querySnapshots).where(
|
|
21856
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
22130
|
+
}).from(querySnapshots).where(eq24(querySnapshots.runId, runId)).all());
|
|
22131
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq24(queries.projectId, project.id)).all();
|
|
21857
22132
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
21858
22133
|
const byQuery = /* @__PURE__ */ new Map();
|
|
21859
22134
|
for (const snap of snapshots) {
|
|
@@ -21936,7 +22211,7 @@ async function cdpRoutes(app, opts) {
|
|
|
21936
22211
|
|
|
21937
22212
|
// ../api-routes/src/ga.ts
|
|
21938
22213
|
import crypto20 from "crypto";
|
|
21939
|
-
import { eq as
|
|
22214
|
+
import { eq as eq25, desc as desc13, and as and18, sql as sql9 } from "drizzle-orm";
|
|
21940
22215
|
function gaLog(level, action, ctx) {
|
|
21941
22216
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
21942
22217
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -22143,10 +22418,10 @@ async function ga4Routes(app, opts) {
|
|
|
22143
22418
|
if (!saConn && !oauthConn) {
|
|
22144
22419
|
throw notFound("GA4 connection", project.name);
|
|
22145
22420
|
}
|
|
22146
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
22147
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
22148
|
-
app.db.delete(gaAiReferrals).where(
|
|
22149
|
-
app.db.delete(gaSocialReferrals).where(
|
|
22421
|
+
app.db.delete(gaTrafficSnapshots).where(eq25(gaTrafficSnapshots.projectId, project.id)).run();
|
|
22422
|
+
app.db.delete(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).run();
|
|
22423
|
+
app.db.delete(gaAiReferrals).where(eq25(gaAiReferrals.projectId, project.id)).run();
|
|
22424
|
+
app.db.delete(gaSocialReferrals).where(eq25(gaSocialReferrals.projectId, project.id)).run();
|
|
22150
22425
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
22151
22426
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
22152
22427
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -22167,7 +22442,7 @@ async function ga4Routes(app, opts) {
|
|
|
22167
22442
|
if (!connected) {
|
|
22168
22443
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
22169
22444
|
}
|
|
22170
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22445
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
22171
22446
|
return {
|
|
22172
22447
|
connected: true,
|
|
22173
22448
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -22231,8 +22506,8 @@ async function ga4Routes(app, opts) {
|
|
|
22231
22506
|
app.db.transaction((tx) => {
|
|
22232
22507
|
if (syncTraffic) {
|
|
22233
22508
|
tx.delete(gaTrafficSnapshots).where(
|
|
22234
|
-
|
|
22235
|
-
|
|
22509
|
+
and18(
|
|
22510
|
+
eq25(gaTrafficSnapshots.projectId, project.id),
|
|
22236
22511
|
sql9`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
22237
22512
|
sql9`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
22238
22513
|
)
|
|
@@ -22255,8 +22530,8 @@ async function ga4Routes(app, opts) {
|
|
|
22255
22530
|
}
|
|
22256
22531
|
if (syncAi) {
|
|
22257
22532
|
tx.delete(gaAiReferrals).where(
|
|
22258
|
-
|
|
22259
|
-
|
|
22533
|
+
and18(
|
|
22534
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22260
22535
|
sql9`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
22261
22536
|
sql9`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
22262
22537
|
)
|
|
@@ -22281,8 +22556,8 @@ async function ga4Routes(app, opts) {
|
|
|
22281
22556
|
}
|
|
22282
22557
|
if (syncSocial) {
|
|
22283
22558
|
tx.delete(gaSocialReferrals).where(
|
|
22284
|
-
|
|
22285
|
-
|
|
22559
|
+
and18(
|
|
22560
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22286
22561
|
sql9`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
22287
22562
|
sql9`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
22288
22563
|
)
|
|
@@ -22303,7 +22578,7 @@ async function ga4Routes(app, opts) {
|
|
|
22303
22578
|
}
|
|
22304
22579
|
}
|
|
22305
22580
|
if (syncSummary) {
|
|
22306
|
-
tx.delete(gaTrafficSummaries).where(
|
|
22581
|
+
tx.delete(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).run();
|
|
22307
22582
|
tx.insert(gaTrafficSummaries).values({
|
|
22308
22583
|
id: crypto20.randomUUID(),
|
|
22309
22584
|
projectId: project.id,
|
|
@@ -22315,7 +22590,7 @@ async function ga4Routes(app, opts) {
|
|
|
22315
22590
|
syncedAt: now,
|
|
22316
22591
|
syncRunId: runId
|
|
22317
22592
|
}).run();
|
|
22318
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
22593
|
+
tx.delete(gaTrafficWindowSummaries).where(eq25(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
22319
22594
|
for (const ws of windowSummaries) {
|
|
22320
22595
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
22321
22596
|
id: crypto20.randomUUID(),
|
|
@@ -22333,7 +22608,7 @@ async function ga4Routes(app, opts) {
|
|
|
22333
22608
|
}
|
|
22334
22609
|
}
|
|
22335
22610
|
});
|
|
22336
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22611
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq25(runs.id, runId)).run();
|
|
22337
22612
|
const syncedComponents = only ? [
|
|
22338
22613
|
...syncTraffic ? ["traffic"] : [],
|
|
22339
22614
|
...syncSummary ? ["summary"] : [],
|
|
@@ -22362,7 +22637,7 @@ async function ga4Routes(app, opts) {
|
|
|
22362
22637
|
} catch (e) {
|
|
22363
22638
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22364
22639
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
22365
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22640
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
|
|
22366
22641
|
throw e;
|
|
22367
22642
|
}
|
|
22368
22643
|
});
|
|
@@ -22373,11 +22648,11 @@ async function ga4Routes(app, opts) {
|
|
|
22373
22648
|
const window = parseWindow(request.query.window);
|
|
22374
22649
|
const cutoff = windowCutoff(window);
|
|
22375
22650
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
22376
|
-
const snapshotConditions = [
|
|
22651
|
+
const snapshotConditions = [eq25(gaTrafficSnapshots.projectId, project.id)];
|
|
22377
22652
|
if (cutoffDate) snapshotConditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
22378
|
-
const aiConditions = [
|
|
22653
|
+
const aiConditions = [eq25(gaAiReferrals.projectId, project.id)];
|
|
22379
22654
|
if (cutoffDate) aiConditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
22380
|
-
const socialConditions = [
|
|
22655
|
+
const socialConditions = [eq25(gaSocialReferrals.projectId, project.id)];
|
|
22381
22656
|
if (cutoffDate) socialConditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
22382
22657
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
22383
22658
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
@@ -22385,42 +22660,42 @@ async function ga4Routes(app, opts) {
|
|
|
22385
22660
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
22386
22661
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
22387
22662
|
}).from(gaTrafficWindowSummaries).where(
|
|
22388
|
-
|
|
22389
|
-
|
|
22390
|
-
|
|
22663
|
+
and18(
|
|
22664
|
+
eq25(gaTrafficWindowSummaries.projectId, project.id),
|
|
22665
|
+
eq25(gaTrafficWindowSummaries.windowKey, window)
|
|
22391
22666
|
)
|
|
22392
22667
|
).get() : null;
|
|
22393
22668
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
22394
22669
|
totalSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
22395
22670
|
totalOrganicSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
22396
22671
|
totalUsers: sql9`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
22397
|
-
}).from(gaTrafficSnapshots).where(
|
|
22672
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).get() : null;
|
|
22398
22673
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
22399
22674
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
22400
22675
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
22401
22676
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
22402
|
-
}).from(gaTrafficSummaries).where(
|
|
22677
|
+
}).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).get();
|
|
22403
22678
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
22404
22679
|
totalDirectSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
22405
|
-
}).from(gaTrafficSnapshots).where(
|
|
22680
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).get();
|
|
22406
22681
|
const summaryMeta = app.db.select({
|
|
22407
22682
|
periodStart: gaTrafficSummaries.periodStart,
|
|
22408
22683
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
22409
|
-
}).from(gaTrafficSummaries).where(
|
|
22684
|
+
}).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).get();
|
|
22410
22685
|
const rows = app.db.select({
|
|
22411
22686
|
landingPage: sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
22412
22687
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22413
22688
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22414
22689
|
directSessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
22415
22690
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22416
|
-
}).from(gaTrafficSnapshots).where(
|
|
22691
|
+
}).from(gaTrafficSnapshots).where(and18(...snapshotConditions)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
22417
22692
|
const aiReferralRows = app.db.select({
|
|
22418
22693
|
source: gaAiReferrals.source,
|
|
22419
22694
|
medium: gaAiReferrals.medium,
|
|
22420
22695
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
22421
22696
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22422
22697
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22423
|
-
}).from(gaAiReferrals).where(
|
|
22698
|
+
}).from(gaAiReferrals).where(and18(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
22424
22699
|
const aiReferralLandingPageRows = app.db.select({
|
|
22425
22700
|
source: gaAiReferrals.source,
|
|
22426
22701
|
medium: gaAiReferrals.medium,
|
|
@@ -22428,7 +22703,7 @@ async function ga4Routes(app, opts) {
|
|
|
22428
22703
|
landingPage: sql9`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
22429
22704
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22430
22705
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22431
|
-
}).from(gaAiReferrals).where(
|
|
22706
|
+
}).from(gaAiReferrals).where(and18(...aiConditions)).groupBy(
|
|
22432
22707
|
gaAiReferrals.source,
|
|
22433
22708
|
gaAiReferrals.medium,
|
|
22434
22709
|
gaAiReferrals.sourceDimension,
|
|
@@ -22465,7 +22740,7 @@ async function ga4Routes(app, opts) {
|
|
|
22465
22740
|
channelGroup: gaAiReferrals.channelGroup,
|
|
22466
22741
|
sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
22467
22742
|
users: sql9`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
22468
|
-
}).from(gaAiReferrals).where(
|
|
22743
|
+
}).from(gaAiReferrals).where(and18(...aiConditions, eq25(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
22469
22744
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
22470
22745
|
let aiBySessionUsers = 0;
|
|
22471
22746
|
for (const row of aiBySessionRows) {
|
|
@@ -22479,12 +22754,12 @@ async function ga4Routes(app, opts) {
|
|
|
22479
22754
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
22480
22755
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
22481
22756
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
22482
|
-
}).from(gaSocialReferrals).where(
|
|
22757
|
+
}).from(gaSocialReferrals).where(and18(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql9`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
22483
22758
|
const socialTotals = app.db.select({
|
|
22484
22759
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`,
|
|
22485
22760
|
users: sql9`SUM(${gaSocialReferrals.users})`
|
|
22486
|
-
}).from(gaSocialReferrals).where(
|
|
22487
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
22761
|
+
}).from(gaSocialReferrals).where(and18(...socialConditions)).get();
|
|
22762
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq25(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
22488
22763
|
const total = summaryRow?.totalSessions ?? 0;
|
|
22489
22764
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
22490
22765
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -22564,7 +22839,7 @@ async function ga4Routes(app, opts) {
|
|
|
22564
22839
|
const project = resolveProject(app.db, request.params.name);
|
|
22565
22840
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22566
22841
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22567
|
-
const conditions = [
|
|
22842
|
+
const conditions = [eq25(gaAiReferrals.projectId, project.id)];
|
|
22568
22843
|
if (cutoffDate) conditions.push(sql9`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
22569
22844
|
const rows = app.db.select({
|
|
22570
22845
|
date: gaAiReferrals.date,
|
|
@@ -22574,7 +22849,7 @@ async function ga4Routes(app, opts) {
|
|
|
22574
22849
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
22575
22850
|
sessions: sql9`SUM(${gaAiReferrals.sessions})`,
|
|
22576
22851
|
users: sql9`SUM(${gaAiReferrals.users})`
|
|
22577
|
-
}).from(gaAiReferrals).where(
|
|
22852
|
+
}).from(gaAiReferrals).where(and18(...conditions)).groupBy(
|
|
22578
22853
|
gaAiReferrals.date,
|
|
22579
22854
|
gaAiReferrals.source,
|
|
22580
22855
|
gaAiReferrals.medium,
|
|
@@ -22587,7 +22862,7 @@ async function ga4Routes(app, opts) {
|
|
|
22587
22862
|
const project = resolveProject(app.db, request.params.name);
|
|
22588
22863
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22589
22864
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22590
|
-
const conditions = [
|
|
22865
|
+
const conditions = [eq25(gaSocialReferrals.projectId, project.id)];
|
|
22591
22866
|
if (cutoffDate) conditions.push(sql9`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
22592
22867
|
const rows = app.db.select({
|
|
22593
22868
|
date: gaSocialReferrals.date,
|
|
@@ -22596,7 +22871,7 @@ async function ga4Routes(app, opts) {
|
|
|
22596
22871
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
22597
22872
|
sessions: gaSocialReferrals.sessions,
|
|
22598
22873
|
users: gaSocialReferrals.users
|
|
22599
|
-
}).from(gaSocialReferrals).where(
|
|
22874
|
+
}).from(gaSocialReferrals).where(and18(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
22600
22875
|
return rows;
|
|
22601
22876
|
});
|
|
22602
22877
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -22609,8 +22884,8 @@ async function ga4Routes(app, opts) {
|
|
|
22609
22884
|
d.setDate(d.getDate() - n);
|
|
22610
22885
|
return fmt(d);
|
|
22611
22886
|
};
|
|
22612
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22613
|
-
|
|
22887
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and18(
|
|
22888
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22614
22889
|
sql9`${gaSocialReferrals.date} >= ${from}`,
|
|
22615
22890
|
sql9`${gaSocialReferrals.date} < ${to}`
|
|
22616
22891
|
)).get();
|
|
@@ -22622,16 +22897,16 @@ async function ga4Routes(app, opts) {
|
|
|
22622
22897
|
const sourceCurrent = app.db.select({
|
|
22623
22898
|
source: gaSocialReferrals.source,
|
|
22624
22899
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
22625
|
-
}).from(gaSocialReferrals).where(
|
|
22626
|
-
|
|
22900
|
+
}).from(gaSocialReferrals).where(and18(
|
|
22901
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22627
22902
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
22628
22903
|
sql9`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
22629
22904
|
)).groupBy(gaSocialReferrals.source).all();
|
|
22630
22905
|
const sourcePrev = app.db.select({
|
|
22631
22906
|
source: gaSocialReferrals.source,
|
|
22632
22907
|
sessions: sql9`SUM(${gaSocialReferrals.sessions})`
|
|
22633
|
-
}).from(gaSocialReferrals).where(
|
|
22634
|
-
|
|
22908
|
+
}).from(gaSocialReferrals).where(and18(
|
|
22909
|
+
eq25(gaSocialReferrals.projectId, project.id),
|
|
22635
22910
|
sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
22636
22911
|
sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
22637
22912
|
)).groupBy(gaSocialReferrals.source).all();
|
|
@@ -22672,16 +22947,16 @@ async function ga4Routes(app, opts) {
|
|
|
22672
22947
|
return fmt(d);
|
|
22673
22948
|
};
|
|
22674
22949
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
22675
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22676
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22677
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
22678
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22679
|
-
|
|
22950
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22951
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22952
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and18(eq25(gaTrafficSnapshots.projectId, project.id), sql9`${gaTrafficSnapshots.date} >= ${from}`, sql9`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
22953
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22954
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22680
22955
|
sql9`${gaAiReferrals.date} >= ${from}`,
|
|
22681
22956
|
sql9`${gaAiReferrals.date} < ${to}`,
|
|
22682
|
-
|
|
22957
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22683
22958
|
)).get();
|
|
22684
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
22959
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql9`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${from}`, sql9`${gaSocialReferrals.date} < ${to}`)).get();
|
|
22685
22960
|
const todayStr = fmt(today);
|
|
22686
22961
|
const buildTrend = (sum) => {
|
|
22687
22962
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -22690,17 +22965,17 @@ async function ga4Routes(app, opts) {
|
|
|
22690
22965
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
22691
22966
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
22692
22967
|
};
|
|
22693
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22694
|
-
|
|
22968
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22969
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22695
22970
|
sql9`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
22696
22971
|
sql9`${gaAiReferrals.date} < ${todayStr}`,
|
|
22697
|
-
|
|
22972
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22698
22973
|
)).groupBy(gaAiReferrals.source).all();
|
|
22699
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
22700
|
-
|
|
22974
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql9`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and18(
|
|
22975
|
+
eq25(gaAiReferrals.projectId, project.id),
|
|
22701
22976
|
sql9`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
22702
22977
|
sql9`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
22703
|
-
|
|
22978
|
+
eq25(gaAiReferrals.sourceDimension, "session")
|
|
22704
22979
|
)).groupBy(gaAiReferrals.source).all();
|
|
22705
22980
|
const findBiggestMover = (current, prev) => {
|
|
22706
22981
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -22716,8 +22991,8 @@ async function ga4Routes(app, opts) {
|
|
|
22716
22991
|
}
|
|
22717
22992
|
return mover;
|
|
22718
22993
|
};
|
|
22719
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
22720
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
22994
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql9`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
22995
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql9`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and18(eq25(gaSocialReferrals.projectId, project.id), sql9`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql9`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
22721
22996
|
return {
|
|
22722
22997
|
total: buildTrend(sumTotal),
|
|
22723
22998
|
organic: buildTrend(sumOrganic),
|
|
@@ -22732,14 +23007,14 @@ async function ga4Routes(app, opts) {
|
|
|
22732
23007
|
const project = resolveProject(app.db, request.params.name);
|
|
22733
23008
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
22734
23009
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
22735
|
-
const conditions = [
|
|
23010
|
+
const conditions = [eq25(gaTrafficSnapshots.projectId, project.id)];
|
|
22736
23011
|
if (cutoffDate) conditions.push(sql9`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
22737
23012
|
const rows = app.db.select({
|
|
22738
23013
|
date: gaTrafficSnapshots.date,
|
|
22739
23014
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22740
23015
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22741
23016
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22742
|
-
}).from(gaTrafficSnapshots).where(
|
|
23017
|
+
}).from(gaTrafficSnapshots).where(and18(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
22743
23018
|
return rows.map((r) => ({
|
|
22744
23019
|
date: r.date,
|
|
22745
23020
|
sessions: r.sessions ?? 0,
|
|
@@ -22755,7 +23030,7 @@ async function ga4Routes(app, opts) {
|
|
|
22755
23030
|
sessions: sql9`SUM(${gaTrafficSnapshots.sessions})`,
|
|
22756
23031
|
organicSessions: sql9`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
22757
23032
|
users: sql9`SUM(${gaTrafficSnapshots.users})`
|
|
22758
|
-
}).from(gaTrafficSnapshots).where(
|
|
23033
|
+
}).from(gaTrafficSnapshots).where(eq25(gaTrafficSnapshots.projectId, project.id)).groupBy(sql9`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql9`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
22759
23034
|
return {
|
|
22760
23035
|
pages: trafficPages.map((r) => ({
|
|
22761
23036
|
landingPage: r.landingPage,
|
|
@@ -24403,7 +24678,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
24403
24678
|
|
|
24404
24679
|
// ../api-routes/src/backlinks.ts
|
|
24405
24680
|
import crypto22 from "crypto";
|
|
24406
|
-
import { and as
|
|
24681
|
+
import { and as and20, asc as asc3, desc as desc14, eq as eq26, sql as sql10 } from "drizzle-orm";
|
|
24407
24682
|
|
|
24408
24683
|
// ../integration-commoncrawl/src/constants.ts
|
|
24409
24684
|
import os2 from "os";
|
|
@@ -24806,7 +25081,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
24806
25081
|
}
|
|
24807
25082
|
|
|
24808
25083
|
// ../api-routes/src/backlinks-filter.ts
|
|
24809
|
-
import { and as
|
|
25084
|
+
import { and as and19, ne as ne3, notLike } from "drizzle-orm";
|
|
24810
25085
|
var BACKLINK_FILTER_PATTERNS = [
|
|
24811
25086
|
"*.google.com",
|
|
24812
25087
|
"*.googleusercontent.com",
|
|
@@ -24829,7 +25104,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
24829
25104
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
24830
25105
|
}
|
|
24831
25106
|
}
|
|
24832
|
-
const combined =
|
|
25107
|
+
const combined = and19(...conditions);
|
|
24833
25108
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
24834
25109
|
return combined;
|
|
24835
25110
|
}
|
|
@@ -24900,11 +25175,11 @@ function mapRunRow(row) {
|
|
|
24900
25175
|
}
|
|
24901
25176
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
24902
25177
|
const conditions = [
|
|
24903
|
-
|
|
24904
|
-
|
|
25178
|
+
eq26(backlinkSummaries.projectId, projectId),
|
|
25179
|
+
eq26(backlinkSummaries.source, source)
|
|
24905
25180
|
];
|
|
24906
|
-
if (release) conditions.push(
|
|
24907
|
-
return db.select().from(backlinkSummaries).where(
|
|
25181
|
+
if (release) conditions.push(eq26(backlinkSummaries.release, release));
|
|
25182
|
+
return db.select().from(backlinkSummaries).where(and20(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24908
25183
|
}
|
|
24909
25184
|
function parseExcludeCrawlers(value) {
|
|
24910
25185
|
if (!value) return false;
|
|
@@ -24912,12 +25187,12 @@ function parseExcludeCrawlers(value) {
|
|
|
24912
25187
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
24913
25188
|
}
|
|
24914
25189
|
function computeFilteredSummary(db, base) {
|
|
24915
|
-
const baseDomainCondition =
|
|
24916
|
-
|
|
24917
|
-
|
|
24918
|
-
|
|
25190
|
+
const baseDomainCondition = and20(
|
|
25191
|
+
eq26(backlinkDomains.projectId, base.projectId),
|
|
25192
|
+
eq26(backlinkDomains.source, base.source),
|
|
25193
|
+
eq26(backlinkDomains.release, base.release)
|
|
24919
25194
|
);
|
|
24920
|
-
const filteredCondition =
|
|
25195
|
+
const filteredCondition = and20(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
24921
25196
|
const unfilteredAgg = db.select({
|
|
24922
25197
|
count: sql10`count(*)`,
|
|
24923
25198
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -24926,7 +25201,7 @@ function computeFilteredSummary(db, base) {
|
|
|
24926
25201
|
count: sql10`count(*)`,
|
|
24927
25202
|
total: sql10`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
24928
25203
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
24929
|
-
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(
|
|
25204
|
+
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
24930
25205
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
24931
25206
|
const totalHosts = Number(filteredAgg?.total ?? 0);
|
|
24932
25207
|
const unfilteredLinkingDomains = Number(unfilteredAgg?.count ?? 0);
|
|
@@ -24947,13 +25222,13 @@ function computeFilteredSummary(db, base) {
|
|
|
24947
25222
|
};
|
|
24948
25223
|
}
|
|
24949
25224
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
24950
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
25225
|
+
const summary = db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, projectId), eq26(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
24951
25226
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
24952
25227
|
if (summary && excludeCrawlers) {
|
|
24953
|
-
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(
|
|
24954
|
-
|
|
24955
|
-
|
|
24956
|
-
|
|
25228
|
+
const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and20(
|
|
25229
|
+
eq26(backlinkDomains.projectId, projectId),
|
|
25230
|
+
eq26(backlinkDomains.source, source),
|
|
25231
|
+
eq26(backlinkDomains.release, summary.release),
|
|
24957
25232
|
backlinkCrawlerExclusionClause()
|
|
24958
25233
|
)).get();
|
|
24959
25234
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -24968,7 +25243,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
24968
25243
|
};
|
|
24969
25244
|
}
|
|
24970
25245
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
24971
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
25246
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
24972
25247
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
24973
25248
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
24974
25249
|
const sources = [
|
|
@@ -25024,7 +25299,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25024
25299
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
25025
25300
|
);
|
|
25026
25301
|
}
|
|
25027
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
25302
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.release, release)).get();
|
|
25028
25303
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25029
25304
|
if (existing) {
|
|
25030
25305
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -25035,9 +25310,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
25035
25310
|
phaseDetail: null,
|
|
25036
25311
|
error: null,
|
|
25037
25312
|
updatedAt: now
|
|
25038
|
-
}).where(
|
|
25313
|
+
}).where(eq26(ccReleaseSyncs.id, existing.id)).run();
|
|
25039
25314
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
25040
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
25315
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.id, existing.id)).get();
|
|
25041
25316
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
25042
25317
|
}
|
|
25043
25318
|
const id = crypto22.randomUUID();
|
|
@@ -25049,15 +25324,15 @@ async function backlinksRoutes(app, opts) {
|
|
|
25049
25324
|
updatedAt: now
|
|
25050
25325
|
}).run();
|
|
25051
25326
|
opts.onReleaseSyncRequested(id, release);
|
|
25052
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
25327
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.id, id)).get();
|
|
25053
25328
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
25054
25329
|
});
|
|
25055
25330
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
25056
|
-
const row = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
25331
|
+
const row = app.db.select().from(ccReleaseSyncs).orderBy(desc14(ccReleaseSyncs.updatedAt)).limit(1).get();
|
|
25057
25332
|
return reply.send(row ? mapSyncRow(row) : null);
|
|
25058
25333
|
});
|
|
25059
25334
|
app.get("/backlinks/syncs", async (_request, reply) => {
|
|
25060
|
-
const rows = app.db.select().from(ccReleaseSyncs).orderBy(
|
|
25335
|
+
const rows = app.db.select().from(ccReleaseSyncs).orderBy(desc14(ccReleaseSyncs.updatedAt)).all();
|
|
25061
25336
|
return reply.send(rows.map(mapSyncRow));
|
|
25062
25337
|
});
|
|
25063
25338
|
app.get("/backlinks/releases", async (_request, reply) => {
|
|
@@ -25107,7 +25382,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25107
25382
|
createdAt: now
|
|
25108
25383
|
}).run();
|
|
25109
25384
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
25110
|
-
const run = app.db.select().from(runs).where(
|
|
25385
|
+
const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
|
|
25111
25386
|
return reply.status(201).send(mapRunRow(run));
|
|
25112
25387
|
});
|
|
25113
25388
|
app.get(
|
|
@@ -25133,18 +25408,18 @@ async function backlinksRoutes(app, opts) {
|
|
|
25133
25408
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
25134
25409
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
25135
25410
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25136
|
-
const baseDomainCondition =
|
|
25137
|
-
|
|
25138
|
-
|
|
25139
|
-
|
|
25411
|
+
const baseDomainCondition = and20(
|
|
25412
|
+
eq26(backlinkDomains.projectId, project.id),
|
|
25413
|
+
eq26(backlinkDomains.source, source),
|
|
25414
|
+
eq26(backlinkDomains.release, targetRelease)
|
|
25140
25415
|
);
|
|
25141
|
-
const domainCondition = excludeCrawlers ?
|
|
25416
|
+
const domainCondition = excludeCrawlers ? and20(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
25142
25417
|
const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
25143
25418
|
const rows = app.db.select({
|
|
25144
25419
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
25145
25420
|
numHosts: backlinkDomains.numHosts,
|
|
25146
25421
|
source: backlinkDomains.source
|
|
25147
|
-
}).from(backlinkDomains).where(domainCondition).orderBy(
|
|
25422
|
+
}).from(backlinkDomains).where(domainCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
|
|
25148
25423
|
let summary = null;
|
|
25149
25424
|
if (summaryRow) {
|
|
25150
25425
|
summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
|
|
@@ -25162,7 +25437,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25162
25437
|
async (request, reply) => {
|
|
25163
25438
|
const project = resolveProject(app.db, request.params.name);
|
|
25164
25439
|
const source = parseSourceParam(request.query.source);
|
|
25165
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
25440
|
+
const rows = app.db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, project.id), eq26(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
|
|
25166
25441
|
const response = rows.map((r) => ({
|
|
25167
25442
|
release: r.release,
|
|
25168
25443
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -25209,7 +25484,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25209
25484
|
createdAt: now
|
|
25210
25485
|
}).run();
|
|
25211
25486
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25212
|
-
const run = app.db.select().from(runs).where(
|
|
25487
|
+
const run = app.db.select().from(runs).where(eq26(runs.id, runId)).get();
|
|
25213
25488
|
return reply.status(201).send(mapRunRow(run));
|
|
25214
25489
|
}
|
|
25215
25490
|
);
|
|
@@ -25218,7 +25493,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25218
25493
|
// ../api-routes/src/traffic.ts
|
|
25219
25494
|
import crypto24 from "crypto";
|
|
25220
25495
|
import { Agent as UndiciAgent } from "undici";
|
|
25221
|
-
import { and as
|
|
25496
|
+
import { and as and21, desc as desc15, eq as eq27, gte as gte4, lte as lte3, sql as sql11 } from "drizzle-orm";
|
|
25222
25497
|
|
|
25223
25498
|
// ../integration-cloud-run/src/auth.ts
|
|
25224
25499
|
import crypto23 from "crypto";
|
|
@@ -29035,8 +29310,8 @@ async function runBackfillTask(options) {
|
|
|
29035
29310
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29036
29311
|
try {
|
|
29037
29312
|
app.db.transaction((tx) => {
|
|
29038
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29039
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
29313
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq27(runs.id, runId)).run();
|
|
29314
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29040
29315
|
});
|
|
29041
29316
|
} catch {
|
|
29042
29317
|
}
|
|
@@ -29051,7 +29326,7 @@ async function runBackfillTask(options) {
|
|
|
29051
29326
|
if (allEvents.length === 0) {
|
|
29052
29327
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
29053
29328
|
try {
|
|
29054
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
29329
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq27(runs.id, runId)).run();
|
|
29055
29330
|
} catch {
|
|
29056
29331
|
}
|
|
29057
29332
|
return;
|
|
@@ -29073,29 +29348,29 @@ async function runBackfillTask(options) {
|
|
|
29073
29348
|
try {
|
|
29074
29349
|
app.db.transaction((tx) => {
|
|
29075
29350
|
tx.delete(crawlerEventsHourly).where(
|
|
29076
|
-
|
|
29077
|
-
|
|
29351
|
+
and21(
|
|
29352
|
+
eq27(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
29078
29353
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
29079
29354
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
29080
29355
|
)
|
|
29081
29356
|
).run();
|
|
29082
29357
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
29083
|
-
|
|
29084
|
-
|
|
29358
|
+
and21(
|
|
29359
|
+
eq27(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
29085
29360
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
29086
29361
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
29087
29362
|
)
|
|
29088
29363
|
).run();
|
|
29089
29364
|
tx.delete(aiReferralEventsHourly).where(
|
|
29090
|
-
|
|
29091
|
-
|
|
29365
|
+
and21(
|
|
29366
|
+
eq27(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
29092
29367
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
29093
29368
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
29094
29369
|
)
|
|
29095
29370
|
).run();
|
|
29096
29371
|
tx.delete(rawEventSamples).where(
|
|
29097
|
-
|
|
29098
|
-
|
|
29372
|
+
and21(
|
|
29373
|
+
eq27(rawEventSamples.sourceId, sourceRow.id),
|
|
29099
29374
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
29100
29375
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
29101
29376
|
)
|
|
@@ -29184,8 +29459,8 @@ async function runBackfillTask(options) {
|
|
|
29184
29459
|
lastError: null,
|
|
29185
29460
|
lastEventIds: newRingBuffer,
|
|
29186
29461
|
updatedAt: finishedAt
|
|
29187
|
-
}).where(
|
|
29188
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
29462
|
+
}).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29463
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
|
|
29189
29464
|
});
|
|
29190
29465
|
} catch (e) {
|
|
29191
29466
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -29267,7 +29542,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29267
29542
|
createdAt: existing?.createdAt ?? now,
|
|
29268
29543
|
updatedAt: now
|
|
29269
29544
|
});
|
|
29270
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29545
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
29271
29546
|
const config = {
|
|
29272
29547
|
gcpProjectId,
|
|
29273
29548
|
serviceName: serviceName ?? null,
|
|
@@ -29283,8 +29558,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29283
29558
|
lastError: null,
|
|
29284
29559
|
configJson: config,
|
|
29285
29560
|
updatedAt: now
|
|
29286
|
-
}).where(
|
|
29287
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29561
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29562
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29288
29563
|
} else {
|
|
29289
29564
|
const newId = crypto24.randomUUID();
|
|
29290
29565
|
app.db.insert(trafficSources).values({
|
|
@@ -29301,7 +29576,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29301
29576
|
createdAt: now,
|
|
29302
29577
|
updatedAt: now
|
|
29303
29578
|
}).run();
|
|
29304
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29579
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29305
29580
|
}
|
|
29306
29581
|
writeAuditLog(app.db, {
|
|
29307
29582
|
projectId: project.id,
|
|
@@ -29353,7 +29628,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29353
29628
|
createdAt: existing?.createdAt ?? now,
|
|
29354
29629
|
updatedAt: now
|
|
29355
29630
|
});
|
|
29356
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29631
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
29357
29632
|
const config = { baseUrl, username };
|
|
29358
29633
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
29359
29634
|
let sourceRow;
|
|
@@ -29364,8 +29639,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29364
29639
|
lastError: null,
|
|
29365
29640
|
configJson: config,
|
|
29366
29641
|
updatedAt: now
|
|
29367
|
-
}).where(
|
|
29368
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29642
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29643
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29369
29644
|
} else {
|
|
29370
29645
|
const newId = crypto24.randomUUID();
|
|
29371
29646
|
app.db.insert(trafficSources).values({
|
|
@@ -29382,7 +29657,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29382
29657
|
createdAt: now,
|
|
29383
29658
|
updatedAt: now
|
|
29384
29659
|
}).run();
|
|
29385
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
29660
|
+
sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29386
29661
|
}
|
|
29387
29662
|
writeAuditLog(app.db, {
|
|
29388
29663
|
projectId: project.id,
|
|
@@ -29436,7 +29711,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29436
29711
|
createdAt: existing?.createdAt ?? now,
|
|
29437
29712
|
updatedAt: now
|
|
29438
29713
|
});
|
|
29439
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
29714
|
+
const activeSource = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
29440
29715
|
const config = { projectId, teamId, environment };
|
|
29441
29716
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
29442
29717
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -29448,8 +29723,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29448
29723
|
lastError: null,
|
|
29449
29724
|
configJson: config,
|
|
29450
29725
|
updatedAt: now
|
|
29451
|
-
}).where(
|
|
29452
|
-
row = tx.select().from(trafficSources).where(
|
|
29726
|
+
}).where(eq27(trafficSources.id, activeSource.id)).run();
|
|
29727
|
+
row = tx.select().from(trafficSources).where(eq27(trafficSources.id, activeSource.id)).get();
|
|
29453
29728
|
} else {
|
|
29454
29729
|
const newId = crypto24.randomUUID();
|
|
29455
29730
|
tx.insert(trafficSources).values({
|
|
@@ -29474,12 +29749,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29474
29749
|
createdAt: now,
|
|
29475
29750
|
updatedAt: now
|
|
29476
29751
|
}).run();
|
|
29477
|
-
row = tx.select().from(trafficSources).where(
|
|
29752
|
+
row = tx.select().from(trafficSources).where(eq27(trafficSources.id, newId)).get();
|
|
29478
29753
|
}
|
|
29479
29754
|
const existingSchedule = tx.select().from(schedules).where(
|
|
29480
|
-
|
|
29481
|
-
|
|
29482
|
-
|
|
29755
|
+
and21(
|
|
29756
|
+
eq27(schedules.projectId, project.id),
|
|
29757
|
+
eq27(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
29483
29758
|
)
|
|
29484
29759
|
).get();
|
|
29485
29760
|
let created = false;
|
|
@@ -29528,7 +29803,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29528
29803
|
});
|
|
29529
29804
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
29530
29805
|
const project = resolveProject(app.db, request.params.name);
|
|
29531
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
29806
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
29532
29807
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
29533
29808
|
throw notFound("Traffic source", request.params.id);
|
|
29534
29809
|
}
|
|
@@ -29554,8 +29829,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29554
29829
|
const markFailed = (msg, errorCode) => {
|
|
29555
29830
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29556
29831
|
app.db.transaction((tx) => {
|
|
29557
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29558
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
29832
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq27(runs.id, runId)).run();
|
|
29833
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
29559
29834
|
});
|
|
29560
29835
|
try {
|
|
29561
29836
|
opts.onTrafficSynced?.({
|
|
@@ -29636,7 +29911,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29636
29911
|
}
|
|
29637
29912
|
const credential = credentialStore.getConnection(project.name);
|
|
29638
29913
|
if (!credential) {
|
|
29639
|
-
app.db.delete(runs).where(
|
|
29914
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29640
29915
|
throw validationError(
|
|
29641
29916
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
29642
29917
|
);
|
|
@@ -29685,12 +29960,12 @@ async function trafficRoutes(app, opts) {
|
|
|
29685
29960
|
auditAction = "traffic.vercel.synced";
|
|
29686
29961
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
29687
29962
|
if (!credentialStore) {
|
|
29688
|
-
app.db.delete(runs).where(
|
|
29963
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29689
29964
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
29690
29965
|
}
|
|
29691
29966
|
const credential = credentialStore.getConnection(project.name);
|
|
29692
29967
|
if (!credential) {
|
|
29693
|
-
app.db.delete(runs).where(
|
|
29968
|
+
app.db.delete(runs).where(eq27(runs.id, runId)).run();
|
|
29694
29969
|
throw validationError(
|
|
29695
29970
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
29696
29971
|
);
|
|
@@ -29790,7 +30065,7 @@ async function trafficRoutes(app, opts) {
|
|
|
29790
30065
|
let aiReferralHitsCount = 0;
|
|
29791
30066
|
let unknownHitsCount = 0;
|
|
29792
30067
|
app.db.transaction((tx) => {
|
|
29793
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
30068
|
+
const latestRow = tx.select().from(trafficSources).where(eq27(trafficSources.id, sourceRow.id)).get();
|
|
29794
30069
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
29795
30070
|
const seenEventIds = new Set(previousIds);
|
|
29796
30071
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -29958,8 +30233,8 @@ async function trafficRoutes(app, opts) {
|
|
|
29958
30233
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
29959
30234
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
29960
30235
|
}
|
|
29961
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
29962
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
30236
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
30237
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
|
|
29963
30238
|
});
|
|
29964
30239
|
writeAuditLog(app.db, {
|
|
29965
30240
|
projectId: project.id,
|
|
@@ -30011,7 +30286,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30011
30286
|
});
|
|
30012
30287
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
30013
30288
|
const project = resolveProject(app.db, request.params.name);
|
|
30014
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30289
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
30015
30290
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30016
30291
|
throw notFound("Traffic source", request.params.id);
|
|
30017
30292
|
}
|
|
@@ -30196,36 +30471,36 @@ async function trafficRoutes(app, opts) {
|
|
|
30196
30471
|
});
|
|
30197
30472
|
function buildSourceDetail(projectId, row, since) {
|
|
30198
30473
|
const crawlerTotals = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
30199
|
-
|
|
30200
|
-
|
|
30474
|
+
and21(
|
|
30475
|
+
eq27(crawlerEventsHourly.sourceId, row.id),
|
|
30201
30476
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
30202
30477
|
)
|
|
30203
30478
|
).get();
|
|
30204
30479
|
const aiUserFetchTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
30205
|
-
|
|
30206
|
-
|
|
30480
|
+
and21(
|
|
30481
|
+
eq27(aiUserFetchEventsHourly.sourceId, row.id),
|
|
30207
30482
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
30208
30483
|
)
|
|
30209
30484
|
).get();
|
|
30210
30485
|
const aiTotals = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
30211
|
-
|
|
30212
|
-
|
|
30486
|
+
and21(
|
|
30487
|
+
eq27(aiReferralEventsHourly.sourceId, row.id),
|
|
30213
30488
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
30214
30489
|
)
|
|
30215
30490
|
).get();
|
|
30216
30491
|
const sampleTotals = app.db.select({ total: sql11`COUNT(*)` }).from(rawEventSamples).where(
|
|
30217
|
-
|
|
30218
|
-
|
|
30492
|
+
and21(
|
|
30493
|
+
eq27(rawEventSamples.sourceId, row.id),
|
|
30219
30494
|
gte4(rawEventSamples.ts, since)
|
|
30220
30495
|
)
|
|
30221
30496
|
).get();
|
|
30222
30497
|
const latestRun = app.db.select().from(runs).where(
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30498
|
+
and21(
|
|
30499
|
+
eq27(runs.projectId, projectId),
|
|
30500
|
+
eq27(runs.kind, RunKinds["traffic-sync"]),
|
|
30501
|
+
eq27(runs.sourceId, row.id)
|
|
30227
30502
|
)
|
|
30228
|
-
).orderBy(
|
|
30503
|
+
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
30229
30504
|
return {
|
|
30230
30505
|
...rowToDto(row),
|
|
30231
30506
|
totals24h: {
|
|
@@ -30251,7 +30526,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30251
30526
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
30252
30527
|
);
|
|
30253
30528
|
}
|
|
30254
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30529
|
+
const sourceRow = app.db.select().from(trafficSources).where(and21(eq27(trafficSources.projectId, project.id), eq27(trafficSources.id, request.params.id))).get();
|
|
30255
30530
|
if (!sourceRow) {
|
|
30256
30531
|
throw notFound("traffic source", request.params.id);
|
|
30257
30532
|
}
|
|
@@ -30268,7 +30543,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30268
30543
|
status: TrafficSourceStatuses.connected,
|
|
30269
30544
|
lastError: null,
|
|
30270
30545
|
updatedAt: now
|
|
30271
|
-
}).where(
|
|
30546
|
+
}).where(eq27(trafficSources.id, sourceRow.id)).run();
|
|
30272
30547
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
30273
30548
|
projectId: project.id,
|
|
30274
30549
|
actor: "api",
|
|
@@ -30276,20 +30551,20 @@ async function trafficRoutes(app, opts) {
|
|
|
30276
30551
|
entityType: "traffic_source",
|
|
30277
30552
|
entityId: sourceRow.id
|
|
30278
30553
|
}));
|
|
30279
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
30554
|
+
updatedRow = tx.select().from(trafficSources).where(eq27(trafficSources.id, sourceRow.id)).get();
|
|
30280
30555
|
});
|
|
30281
30556
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
30282
30557
|
});
|
|
30283
30558
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
30284
30559
|
const project = resolveProject(app.db, request.params.name);
|
|
30285
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30560
|
+
const rows = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
30286
30561
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
30287
30562
|
const response = { sources };
|
|
30288
30563
|
return response;
|
|
30289
30564
|
});
|
|
30290
30565
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
30291
30566
|
const project = resolveProject(app.db, request.params.name);
|
|
30292
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
30567
|
+
const rows = app.db.select().from(trafficSources).where(eq27(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
30293
30568
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
30294
30569
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
30295
30570
|
const response = { sources };
|
|
@@ -30299,7 +30574,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30299
30574
|
"/projects/:name/traffic/sources/:id",
|
|
30300
30575
|
async (request) => {
|
|
30301
30576
|
const project = resolveProject(app.db, request.params.name);
|
|
30302
|
-
const row = app.db.select().from(trafficSources).where(
|
|
30577
|
+
const row = app.db.select().from(trafficSources).where(eq27(trafficSources.id, request.params.id)).get();
|
|
30303
30578
|
if (!row || row.projectId !== project.id) {
|
|
30304
30579
|
throw notFound("Traffic source", request.params.id);
|
|
30305
30580
|
}
|
|
@@ -30350,15 +30625,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30350
30625
|
let aiReferralTotal = 0;
|
|
30351
30626
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
30352
30627
|
const crawlerFilters = [
|
|
30353
|
-
|
|
30628
|
+
eq27(crawlerEventsHourly.projectId, project.id),
|
|
30354
30629
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
30355
30630
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
30356
30631
|
];
|
|
30357
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
30358
|
-
const crawlerWhere =
|
|
30632
|
+
if (sourceIdParam) crawlerFilters.push(eq27(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
30633
|
+
const crawlerWhere = and21(...crawlerFilters);
|
|
30359
30634
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
30360
30635
|
crawlerTotal = Number(total?.total ?? 0);
|
|
30361
|
-
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(
|
|
30636
|
+
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc15(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
30362
30637
|
for (const r of rows) {
|
|
30363
30638
|
events.push({
|
|
30364
30639
|
kind: TrafficEventKinds.crawler,
|
|
@@ -30375,15 +30650,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30375
30650
|
}
|
|
30376
30651
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
30377
30652
|
const userFetchFilters = [
|
|
30378
|
-
|
|
30653
|
+
eq27(aiUserFetchEventsHourly.projectId, project.id),
|
|
30379
30654
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
30380
30655
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
30381
30656
|
];
|
|
30382
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
30383
|
-
const userFetchWhere =
|
|
30657
|
+
if (sourceIdParam) userFetchFilters.push(eq27(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
30658
|
+
const userFetchWhere = and21(...userFetchFilters);
|
|
30384
30659
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
30385
30660
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
30386
|
-
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(
|
|
30661
|
+
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
30387
30662
|
for (const r of rows) {
|
|
30388
30663
|
events.push({
|
|
30389
30664
|
kind: TrafficEventKinds["ai-user-fetch"],
|
|
@@ -30400,15 +30675,15 @@ async function trafficRoutes(app, opts) {
|
|
|
30400
30675
|
}
|
|
30401
30676
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
30402
30677
|
const aiFilters = [
|
|
30403
|
-
|
|
30678
|
+
eq27(aiReferralEventsHourly.projectId, project.id),
|
|
30404
30679
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
30405
30680
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
30406
30681
|
];
|
|
30407
|
-
if (sourceIdParam) aiFilters.push(
|
|
30408
|
-
const aiWhere =
|
|
30682
|
+
if (sourceIdParam) aiFilters.push(eq27(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
30683
|
+
const aiWhere = and21(...aiFilters);
|
|
30409
30684
|
const total = app.db.select({ total: sql11`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
30410
30685
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
30411
|
-
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(
|
|
30686
|
+
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
30412
30687
|
for (const r of rows) {
|
|
30413
30688
|
events.push({
|
|
30414
30689
|
kind: TrafficEventKinds["ai-referral"],
|
|
@@ -30609,7 +30884,7 @@ function readInstalledManifest(skillDir) {
|
|
|
30609
30884
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
30610
30885
|
|
|
30611
30886
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
30612
|
-
import { and as
|
|
30887
|
+
import { and as and22, eq as eq28 } from "drizzle-orm";
|
|
30613
30888
|
function skippedNoProject() {
|
|
30614
30889
|
return {
|
|
30615
30890
|
status: CheckStatuses.skipped,
|
|
@@ -30625,8 +30900,8 @@ var BACKLINKS_CHECKS = [
|
|
|
30625
30900
|
title: "Backlinks source connected",
|
|
30626
30901
|
run: (ctx) => {
|
|
30627
30902
|
if (!ctx.project) return skippedNoProject();
|
|
30628
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
30629
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
30903
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq28(projects.id, ctx.project.id)).get();
|
|
30904
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq28(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
30630
30905
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
30631
30906
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
30632
30907
|
const connected = [];
|
|
@@ -30641,9 +30916,9 @@ var BACKLINKS_CHECKS = [
|
|
|
30641
30916
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
30642
30917
|
};
|
|
30643
30918
|
}
|
|
30644
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
30645
|
-
|
|
30646
|
-
|
|
30919
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and22(
|
|
30920
|
+
eq28(backlinkSummaries.projectId, ctx.project.id),
|
|
30921
|
+
eq28(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
30647
30922
|
)).limit(1).get() : false;
|
|
30648
30923
|
return {
|
|
30649
30924
|
status: CheckStatuses.ok,
|
|
@@ -30803,7 +31078,7 @@ var BING_AUTH_CHECKS = [
|
|
|
30803
31078
|
];
|
|
30804
31079
|
|
|
30805
31080
|
// ../api-routes/src/doctor/checks/content.ts
|
|
30806
|
-
import { eq as
|
|
31081
|
+
import { eq as eq29 } from "drizzle-orm";
|
|
30807
31082
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
30808
31083
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
30809
31084
|
function skippedNoProject2() {
|
|
@@ -30816,7 +31091,7 @@ function skippedNoProject2() {
|
|
|
30816
31091
|
}
|
|
30817
31092
|
function loadProject(ctx) {
|
|
30818
31093
|
if (!ctx.project) return null;
|
|
30819
|
-
return ctx.db.select().from(projects).where(
|
|
31094
|
+
return ctx.db.select().from(projects).where(eq29(projects.id, ctx.project.id)).get() ?? null;
|
|
30820
31095
|
}
|
|
30821
31096
|
function percent(value) {
|
|
30822
31097
|
return Math.round(value * 100);
|
|
@@ -30908,7 +31183,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
30908
31183
|
);
|
|
30909
31184
|
|
|
30910
31185
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
30911
|
-
import { eq as
|
|
31186
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
30912
31187
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
30913
31188
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
30914
31189
|
var adsConnectionCheck = {
|
|
@@ -30925,7 +31200,7 @@ var adsConnectionCheck = {
|
|
|
30925
31200
|
remediation: null
|
|
30926
31201
|
};
|
|
30927
31202
|
}
|
|
30928
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
31203
|
+
const row = ctx.db.select().from(adsConnections).where(eq30(adsConnections.projectId, ctx.project.id)).get();
|
|
30929
31204
|
if (!row) {
|
|
30930
31205
|
return {
|
|
30931
31206
|
status: CheckStatuses.skipped,
|
|
@@ -30975,7 +31250,7 @@ var adsRecentSyncCheck = {
|
|
|
30975
31250
|
remediation: null
|
|
30976
31251
|
};
|
|
30977
31252
|
}
|
|
30978
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
31253
|
+
const row = ctx.db.select().from(adsConnections).where(eq30(adsConnections.projectId, ctx.project.id)).get();
|
|
30979
31254
|
if (!row) {
|
|
30980
31255
|
return {
|
|
30981
31256
|
status: CheckStatuses.skipped,
|
|
@@ -31166,7 +31441,7 @@ var ga4ConnectionCheck = {
|
|
|
31166
31441
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
31167
31442
|
|
|
31168
31443
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
31169
|
-
import { and as
|
|
31444
|
+
import { and as and23, eq as eq31 } from "drizzle-orm";
|
|
31170
31445
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
31171
31446
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
31172
31447
|
function skippedNoProject3() {
|
|
@@ -31399,7 +31674,7 @@ var recentSyncCheck = {
|
|
|
31399
31674
|
title: "GBP recent sync",
|
|
31400
31675
|
run: (ctx) => {
|
|
31401
31676
|
if (!ctx.project) return skippedNoProject3();
|
|
31402
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
31677
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and23(eq31(gbpLocations.projectId, ctx.project.id), eq31(gbpLocations.selected, true))).all();
|
|
31403
31678
|
if (selected.length === 0) {
|
|
31404
31679
|
return {
|
|
31405
31680
|
status: CheckStatuses.skipped,
|
|
@@ -31459,7 +31734,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
31459
31734
|
);
|
|
31460
31735
|
|
|
31461
31736
|
// ../api-routes/src/doctor/checks/places.ts
|
|
31462
|
-
import { eq as
|
|
31737
|
+
import { eq as eq32 } from "drizzle-orm";
|
|
31463
31738
|
var apiKeyCheck = {
|
|
31464
31739
|
id: "gbp.places.api-key",
|
|
31465
31740
|
category: CheckCategories.auth,
|
|
@@ -31504,7 +31779,7 @@ var apiKeyCheck = {
|
|
|
31504
31779
|
details: { tier: cfg.tier }
|
|
31505
31780
|
};
|
|
31506
31781
|
}
|
|
31507
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
31782
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq32(gbpLocations.projectId, ctx.project.id)).all();
|
|
31508
31783
|
const selected = rows.filter((r) => r.selected);
|
|
31509
31784
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
31510
31785
|
const details = {
|
|
@@ -31955,7 +32230,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
31955
32230
|
];
|
|
31956
32231
|
|
|
31957
32232
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
31958
|
-
import { and as
|
|
32233
|
+
import { and as and24, eq as eq33, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
|
|
31959
32234
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
31960
32235
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
31961
32236
|
function skippedNoProject5() {
|
|
@@ -31969,8 +32244,8 @@ function skippedNoProject5() {
|
|
|
31969
32244
|
function loadProbes(ctx) {
|
|
31970
32245
|
if (!ctx.project) return [];
|
|
31971
32246
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
31972
|
-
|
|
31973
|
-
|
|
32247
|
+
and24(
|
|
32248
|
+
eq33(trafficSources.projectId, ctx.project.id),
|
|
31974
32249
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
31975
32250
|
)
|
|
31976
32251
|
).all();
|
|
@@ -32050,16 +32325,16 @@ var recentDataCheck = {
|
|
|
32050
32325
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32051
32326
|
const recentCrawlers = Number(
|
|
32052
32327
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
32053
|
-
|
|
32054
|
-
|
|
32328
|
+
and24(
|
|
32329
|
+
eq33(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32055
32330
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
32056
32331
|
)
|
|
32057
32332
|
).get()?.total ?? 0
|
|
32058
32333
|
);
|
|
32059
32334
|
const recentReferrals = Number(
|
|
32060
32335
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32061
|
-
|
|
32062
|
-
|
|
32336
|
+
and24(
|
|
32337
|
+
eq33(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32063
32338
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
32064
32339
|
)
|
|
32065
32340
|
).get()?.total ?? 0
|
|
@@ -32074,16 +32349,16 @@ var recentDataCheck = {
|
|
|
32074
32349
|
}
|
|
32075
32350
|
const olderCrawlers = Number(
|
|
32076
32351
|
ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
32077
|
-
|
|
32078
|
-
|
|
32352
|
+
and24(
|
|
32353
|
+
eq33(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32079
32354
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
32080
32355
|
)
|
|
32081
32356
|
).get()?.total ?? 0
|
|
32082
32357
|
);
|
|
32083
32358
|
const olderReferrals = Number(
|
|
32084
32359
|
ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
32085
|
-
|
|
32086
|
-
|
|
32360
|
+
and24(
|
|
32361
|
+
eq33(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32087
32362
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
32088
32363
|
)
|
|
32089
32364
|
).get()?.total ?? 0
|
|
@@ -32497,7 +32772,7 @@ async function doctorRoutes(app, opts) {
|
|
|
32497
32772
|
|
|
32498
32773
|
// ../api-routes/src/discovery/routes.ts
|
|
32499
32774
|
import crypto26 from "crypto";
|
|
32500
|
-
import { and as
|
|
32775
|
+
import { and as and25, desc as desc16, eq as eq34, gte as gte6, inArray as inArray12 } from "drizzle-orm";
|
|
32501
32776
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
32502
32777
|
async function discoveryRoutes(app, opts) {
|
|
32503
32778
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -32529,16 +32804,16 @@ async function discoveryRoutes(app, opts) {
|
|
|
32529
32804
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32530
32805
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
32531
32806
|
const decision = app.db.transaction((tx) => {
|
|
32532
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
32533
|
-
|
|
32534
|
-
|
|
32535
|
-
|
|
32807
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and25(
|
|
32808
|
+
eq34(discoverySessions.projectId, project.id),
|
|
32809
|
+
eq34(discoverySessions.icpDescription, icpDescription),
|
|
32810
|
+
inArray12(discoverySessions.status, [
|
|
32536
32811
|
DiscoverySessionStatuses.queued,
|
|
32537
32812
|
DiscoverySessionStatuses.seeding,
|
|
32538
32813
|
DiscoverySessionStatuses.probing
|
|
32539
32814
|
]),
|
|
32540
32815
|
gte6(discoverySessions.createdAt, ageFloorIso)
|
|
32541
|
-
)).orderBy(
|
|
32816
|
+
)).orderBy(desc16(discoverySessions.createdAt)).get();
|
|
32542
32817
|
if (existing && existing.runId) {
|
|
32543
32818
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
32544
32819
|
}
|
|
@@ -32601,7 +32876,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32601
32876
|
const project = resolveProject(app.db, request.params.name);
|
|
32602
32877
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
32603
32878
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
32604
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
32879
|
+
const rows = app.db.select().from(discoverySessions).where(eq34(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
32605
32880
|
return reply.send(rows.map(serializeSession));
|
|
32606
32881
|
}
|
|
32607
32882
|
);
|
|
@@ -32609,11 +32884,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
32609
32884
|
"/projects/:name/discover/sessions/:id",
|
|
32610
32885
|
async (request, reply) => {
|
|
32611
32886
|
const project = resolveProject(app.db, request.params.name);
|
|
32612
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32887
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32613
32888
|
if (!session || session.projectId !== project.id) {
|
|
32614
32889
|
throw notFound("Discovery session", request.params.id);
|
|
32615
32890
|
}
|
|
32616
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32891
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32617
32892
|
const detail = {
|
|
32618
32893
|
...serializeSession(session),
|
|
32619
32894
|
probes: probeRows.map(serializeProbe)
|
|
@@ -32625,12 +32900,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
32625
32900
|
"/projects/:name/discover/sessions/:id/promote",
|
|
32626
32901
|
async (request, reply) => {
|
|
32627
32902
|
const project = resolveProject(app.db, request.params.name);
|
|
32628
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32903
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32629
32904
|
if (!session || session.projectId !== project.id) {
|
|
32630
32905
|
throw notFound("Discovery session", request.params.id);
|
|
32631
32906
|
}
|
|
32632
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32633
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32907
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32908
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
32634
32909
|
const seenCompetitors = new Set(existingCompetitors);
|
|
32635
32910
|
const cited = /* @__PURE__ */ new Set();
|
|
32636
32911
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -32659,7 +32934,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32659
32934
|
);
|
|
32660
32935
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
32661
32936
|
const project = resolveProject(app.db, request.params.name);
|
|
32662
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
32937
|
+
const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
|
|
32663
32938
|
if (!session || session.projectId !== project.id) {
|
|
32664
32939
|
throw notFound("Discovery session", request.params.id);
|
|
32665
32940
|
}
|
|
@@ -32682,7 +32957,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32682
32957
|
const bucketSet = new Set(buckets);
|
|
32683
32958
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
32684
32959
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
32685
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
32960
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
|
|
32686
32961
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
32687
32962
|
for (const probe of probeRows) {
|
|
32688
32963
|
if (!probe.bucket) continue;
|
|
@@ -32690,7 +32965,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32690
32965
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
32691
32966
|
}
|
|
32692
32967
|
const existingQueries = new Set(
|
|
32693
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
32968
|
+
app.db.select({ query: queries.query }).from(queries).where(eq34(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
32694
32969
|
);
|
|
32695
32970
|
const promotedQueries = [];
|
|
32696
32971
|
const skippedQueries = [];
|
|
@@ -32706,7 +32981,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
32706
32981
|
const skippedCompetitors = [];
|
|
32707
32982
|
if (includeCompetitors) {
|
|
32708
32983
|
const existingCompetitors = new Set(
|
|
32709
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32984
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
32710
32985
|
);
|
|
32711
32986
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
32712
32987
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -32813,7 +33088,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
32813
33088
|
|
|
32814
33089
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
32815
33090
|
import crypto27 from "crypto";
|
|
32816
|
-
import { eq as
|
|
33091
|
+
import { eq as eq35 } from "drizzle-orm";
|
|
32817
33092
|
var DEFAULT_MAX_PROBES = 100;
|
|
32818
33093
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
32819
33094
|
function classifyProbeBucket(input) {
|
|
@@ -32826,7 +33101,7 @@ function classifyProbeBucket(input) {
|
|
|
32826
33101
|
}
|
|
32827
33102
|
function buildCompetitorMap(probes, project, classification = {}) {
|
|
32828
33103
|
const canonical = new Set(project.canonicalDomains.map((d) => d.toLowerCase()));
|
|
32829
|
-
const
|
|
33104
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
32830
33105
|
for (const probe of probes) {
|
|
32831
33106
|
const seenInProbe = /* @__PURE__ */ new Set();
|
|
32832
33107
|
for (const raw of probe.citedDomains) {
|
|
@@ -32834,10 +33109,10 @@ function buildCompetitorMap(probes, project, classification = {}) {
|
|
|
32834
33109
|
if (canonical.has(domain)) continue;
|
|
32835
33110
|
if (seenInProbe.has(domain)) continue;
|
|
32836
33111
|
seenInProbe.add(domain);
|
|
32837
|
-
|
|
33112
|
+
counts2.set(domain, (counts2.get(domain) ?? 0) + 1);
|
|
32838
33113
|
}
|
|
32839
33114
|
}
|
|
32840
|
-
return Array.from(
|
|
33115
|
+
return Array.from(counts2.entries()).map(([domain, hits]) => ({
|
|
32841
33116
|
domain,
|
|
32842
33117
|
hits,
|
|
32843
33118
|
competitorType: classification[domain] ?? DiscoveryCompetitorTypes.unknown
|
|
@@ -32867,7 +33142,7 @@ async function executeDiscovery(opts) {
|
|
|
32867
33142
|
status: DiscoverySessionStatuses.seeding,
|
|
32868
33143
|
dedupThreshold,
|
|
32869
33144
|
startedAt
|
|
32870
|
-
}).where(
|
|
33145
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
32871
33146
|
const seedResult = await opts.deps.seed({
|
|
32872
33147
|
project: opts.project,
|
|
32873
33148
|
icpDescription: opts.icpDescription,
|
|
@@ -32893,7 +33168,7 @@ async function executeDiscovery(opts) {
|
|
|
32893
33168
|
seedCountRaw,
|
|
32894
33169
|
seedCount,
|
|
32895
33170
|
warning
|
|
32896
|
-
}).where(
|
|
33171
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
32897
33172
|
const probeRows = [];
|
|
32898
33173
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
32899
33174
|
for (const query of probedCanonicals) {
|
|
@@ -32934,7 +33209,7 @@ async function executeDiscovery(opts) {
|
|
|
32934
33209
|
wastedCount: buckets["wasted-surface"],
|
|
32935
33210
|
competitorMap,
|
|
32936
33211
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32937
|
-
}).where(
|
|
33212
|
+
}).where(eq35(discoverySessions.id, opts.sessionId)).run();
|
|
32938
33213
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
32939
33214
|
return {
|
|
32940
33215
|
buckets,
|
|
@@ -32974,7 +33249,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
32974
33249
|
status: DiscoverySessionStatuses.failed,
|
|
32975
33250
|
error,
|
|
32976
33251
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
32977
|
-
}).where(
|
|
33252
|
+
}).where(eq35(discoverySessions.id, sessionId)).run();
|
|
32978
33253
|
}
|
|
32979
33254
|
function dedupeStrings(input) {
|
|
32980
33255
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -32992,7 +33267,7 @@ function dedupeStrings(input) {
|
|
|
32992
33267
|
|
|
32993
33268
|
// ../api-routes/src/technical-aeo.ts
|
|
32994
33269
|
import crypto28 from "crypto";
|
|
32995
|
-
import { and as
|
|
33270
|
+
import { and as and26, asc as asc4, count, desc as desc17, eq as eq36, inArray as inArray13 } from "drizzle-orm";
|
|
32996
33271
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
32997
33272
|
function emptyScore(projectName) {
|
|
32998
33273
|
return {
|
|
@@ -33024,12 +33299,12 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
33024
33299
|
async function technicalAeoRoutes(app, opts) {
|
|
33025
33300
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
33026
33301
|
const project = resolveProject(app.db, request.params.name);
|
|
33027
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33028
|
-
|
|
33029
|
-
|
|
33030
|
-
|
|
33302
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33303
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33304
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33305
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33031
33306
|
notProbeRun()
|
|
33032
|
-
)).orderBy(
|
|
33307
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
33033
33308
|
const latest = rows[0];
|
|
33034
33309
|
if (!latest) return emptyScore(project.name);
|
|
33035
33310
|
const snap = latest.snap;
|
|
@@ -33059,24 +33334,24 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33059
33334
|
});
|
|
33060
33335
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
33061
33336
|
const project = resolveProject(app.db, request.params.name);
|
|
33062
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33337
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33338
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33339
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33340
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33066
33341
|
notProbeRun()
|
|
33067
|
-
)).orderBy(
|
|
33342
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
33068
33343
|
if (!latest) {
|
|
33069
33344
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
33070
33345
|
}
|
|
33071
33346
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
33072
|
-
const conds = [
|
|
33073
|
-
if (statusFilter) conds.push(
|
|
33074
|
-
const where =
|
|
33347
|
+
const conds = [eq36(siteAuditPages.runId, latest.runId)];
|
|
33348
|
+
if (statusFilter) conds.push(eq36(siteAuditPages.status, statusFilter));
|
|
33349
|
+
const where = and26(...conds);
|
|
33075
33350
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
33076
33351
|
const total = totalRow?.value ?? 0;
|
|
33077
33352
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
33078
33353
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
33079
|
-
const orderBy = request.query.sort === "score-desc" ?
|
|
33354
|
+
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
|
|
33080
33355
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
33081
33356
|
const pages = rows.map((row) => ({
|
|
33082
33357
|
url: row.url,
|
|
@@ -33095,12 +33370,12 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33095
33370
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
33096
33371
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
33097
33372
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
33098
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
33099
|
-
|
|
33100
|
-
|
|
33101
|
-
|
|
33373
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq36(siteAuditSnapshots.runId, runs.id)).where(and26(
|
|
33374
|
+
eq36(siteAuditSnapshots.projectId, project.id),
|
|
33375
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33376
|
+
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33102
33377
|
notProbeRun()
|
|
33103
|
-
)).orderBy(
|
|
33378
|
+
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
33104
33379
|
return { project: project.name, points: rows.reverse() };
|
|
33105
33380
|
});
|
|
33106
33381
|
app.post("/projects/:name/technical-aeo/runs", async (request) => {
|
|
@@ -33109,10 +33384,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33109
33384
|
if (!parsed.success) {
|
|
33110
33385
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
33111
33386
|
}
|
|
33112
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33387
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and26(
|
|
33388
|
+
eq36(runs.projectId, project.id),
|
|
33389
|
+
eq36(runs.kind, RunKinds["site-audit"]),
|
|
33390
|
+
inArray13(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
33116
33391
|
)).get();
|
|
33117
33392
|
if (existing) {
|
|
33118
33393
|
return { runId: existing.id, status: existing.status };
|
|
@@ -33215,6 +33490,7 @@ async function apiRoutes(app, opts) {
|
|
|
33215
33490
|
await api.register(intelligenceRoutes);
|
|
33216
33491
|
await api.register(reportRoutes);
|
|
33217
33492
|
await api.register(citationRoutes);
|
|
33493
|
+
await api.register(visibilityStatsRoutes);
|
|
33218
33494
|
await api.register(compositeRoutes);
|
|
33219
33495
|
await api.register(contentRoutes, {
|
|
33220
33496
|
explainContentRecommendation: opts.explainContentRecommendation,
|
|
@@ -33698,16 +33974,16 @@ var IntelligenceService = class {
|
|
|
33698
33974
|
*/
|
|
33699
33975
|
analyzeAndPersist(runId, projectId) {
|
|
33700
33976
|
const recentRuns = this.db.select().from(runs).where(
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
or5(
|
|
33977
|
+
and27(
|
|
33978
|
+
eq37(runs.projectId, projectId),
|
|
33979
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
33704
33980
|
// Defensive: RunCoordinator already skips probes before this is
|
|
33705
33981
|
// called, but if a future call site invokes analyzeAndPersist
|
|
33706
33982
|
// directly for a probe, probes still must not pollute the
|
|
33707
33983
|
// intelligence window.
|
|
33708
33984
|
ne5(runs.trigger, RunTriggers.probe)
|
|
33709
33985
|
)
|
|
33710
|
-
).orderBy(
|
|
33986
|
+
).orderBy(desc18(runs.finishedAt), desc18(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
33711
33987
|
if (recentRuns.length === 0) {
|
|
33712
33988
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
33713
33989
|
return null;
|
|
@@ -33782,7 +34058,7 @@ var IntelligenceService = class {
|
|
|
33782
34058
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
33783
34059
|
*/
|
|
33784
34060
|
analyzeAndPersistGbp(runId, projectId) {
|
|
33785
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
34061
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq37(runs.id, runId)).get();
|
|
33786
34062
|
if (!runRow) {
|
|
33787
34063
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
33788
34064
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -33790,9 +34066,9 @@ var IntelligenceService = class {
|
|
|
33790
34066
|
}
|
|
33791
34067
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
33792
34068
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
33793
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
33794
|
-
|
|
33795
|
-
|
|
34069
|
+
const selected = this.db.select().from(gbpLocations).where(and27(
|
|
34070
|
+
eq37(gbpLocations.projectId, projectId),
|
|
34071
|
+
eq37(gbpLocations.selected, true),
|
|
33796
34072
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
33797
34073
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
33798
34074
|
)).all();
|
|
@@ -33827,10 +34103,10 @@ var IntelligenceService = class {
|
|
|
33827
34103
|
}
|
|
33828
34104
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
33829
34105
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
33830
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
33831
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
33832
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
33833
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
34106
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and27(eq37(gbpDailyMetrics.projectId, projectId), eq37(gbpDailyMetrics.locationName, locationName))).all();
|
|
34107
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and27(eq37(gbpPlaceActions.projectId, projectId), eq37(gbpPlaceActions.locationName, locationName))).all();
|
|
34108
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and27(eq37(gbpLodgingSnapshots.projectId, projectId), eq37(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
34109
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and27(eq37(gbpPlaceDetails.projectId, projectId), eq37(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
33834
34110
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
33835
34111
|
const summary = buildGbpSummary({
|
|
33836
34112
|
locationName,
|
|
@@ -33862,7 +34138,7 @@ var IntelligenceService = class {
|
|
|
33862
34138
|
/** Build the month-over-month keyword series for a location from the
|
|
33863
34139
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
33864
34140
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
33865
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
34141
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and27(eq37(gbpKeywordMonthly.projectId, projectId), eq37(gbpKeywordMonthly.locationName, locationName))).all();
|
|
33866
34142
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
33867
34143
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
33868
34144
|
const recentMonth = months[0] ?? null;
|
|
@@ -33893,7 +34169,7 @@ var IntelligenceService = class {
|
|
|
33893
34169
|
*/
|
|
33894
34170
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
33895
34171
|
const covered = new Set(coveredLocationNames);
|
|
33896
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
34172
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and27(eq37(insights.projectId, projectId), eq37(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
33897
34173
|
const staleIds = [];
|
|
33898
34174
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
33899
34175
|
for (const row of existing) {
|
|
@@ -33904,7 +34180,7 @@ var IntelligenceService = class {
|
|
|
33904
34180
|
}
|
|
33905
34181
|
this.db.transaction((tx) => {
|
|
33906
34182
|
for (const id of staleIds) {
|
|
33907
|
-
tx.delete(insights).where(
|
|
34183
|
+
tx.delete(insights).where(eq37(insights.id, id)).run();
|
|
33908
34184
|
}
|
|
33909
34185
|
for (const insight of gbpInsights) {
|
|
33910
34186
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -33982,7 +34258,7 @@ var IntelligenceService = class {
|
|
|
33982
34258
|
* create per run + aggregate). DB is left untouched.
|
|
33983
34259
|
*/
|
|
33984
34260
|
backfill(projectName, opts, onProgress) {
|
|
33985
|
-
const project = this.db.select().from(projects).where(
|
|
34261
|
+
const project = this.db.select().from(projects).where(eq37(projects.name, projectName)).get();
|
|
33986
34262
|
if (!project) {
|
|
33987
34263
|
throw new Error(`Project "${projectName}" not found`);
|
|
33988
34264
|
}
|
|
@@ -33995,9 +34271,9 @@ var IntelligenceService = class {
|
|
|
33995
34271
|
sinceTimestamp = parsed;
|
|
33996
34272
|
}
|
|
33997
34273
|
const allRuns = this.db.select().from(runs).where(
|
|
33998
|
-
|
|
33999
|
-
|
|
34000
|
-
or5(
|
|
34274
|
+
and27(
|
|
34275
|
+
eq37(runs.projectId, project.id),
|
|
34276
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
34001
34277
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
34002
34278
|
ne5(runs.trigger, RunTriggers.probe)
|
|
34003
34279
|
)
|
|
@@ -34030,7 +34306,7 @@ var IntelligenceService = class {
|
|
|
34030
34306
|
let wouldDeleteTotal = 0;
|
|
34031
34307
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
34032
34308
|
if (isDryRun && targetRuns.length > 0) {
|
|
34033
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
34309
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray14(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
34034
34310
|
for (const r of rows) {
|
|
34035
34311
|
if (r.runId == null) continue;
|
|
34036
34312
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -34076,7 +34352,7 @@ var IntelligenceService = class {
|
|
|
34076
34352
|
return { processed, skipped, totalInsights };
|
|
34077
34353
|
}
|
|
34078
34354
|
loadTrackedCompetitors(projectId) {
|
|
34079
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
34355
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
34080
34356
|
}
|
|
34081
34357
|
/**
|
|
34082
34358
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -34097,15 +34373,15 @@ var IntelligenceService = class {
|
|
|
34097
34373
|
}
|
|
34098
34374
|
persistResult(result, runId, projectId) {
|
|
34099
34375
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
34100
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
34376
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq37(insights.runId, runId)).all();
|
|
34101
34377
|
for (const row of existingInsights) {
|
|
34102
34378
|
if (row.dismissed) {
|
|
34103
34379
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
34104
34380
|
}
|
|
34105
34381
|
}
|
|
34106
34382
|
this.db.transaction((tx) => {
|
|
34107
|
-
tx.delete(insights).where(
|
|
34108
|
-
tx.delete(healthSnapshots).where(
|
|
34383
|
+
tx.delete(insights).where(eq37(insights.runId, runId)).run();
|
|
34384
|
+
tx.delete(healthSnapshots).where(eq37(healthSnapshots.runId, runId)).run();
|
|
34109
34385
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34110
34386
|
for (const insight of result.insights) {
|
|
34111
34387
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -34129,8 +34405,10 @@ var IntelligenceService = class {
|
|
|
34129
34405
|
projectId,
|
|
34130
34406
|
runId,
|
|
34131
34407
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
34408
|
+
overallMentionRate: String(result.health.overallMentionRate),
|
|
34132
34409
|
totalPairs: result.health.totalPairs,
|
|
34133
34410
|
citedPairs: result.health.citedPairs,
|
|
34411
|
+
mentionedPairs: result.health.mentionedPairs,
|
|
34134
34412
|
providerBreakdown: result.health.providerBreakdown,
|
|
34135
34413
|
createdAt: now
|
|
34136
34414
|
}).run();
|
|
@@ -34156,28 +34434,28 @@ var IntelligenceService = class {
|
|
|
34156
34434
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
34157
34435
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
34158
34436
|
if (regressions.length === 0) return rawInsights;
|
|
34159
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
34437
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq37(gscSearchData.projectId, projectId)).all();
|
|
34160
34438
|
const gscConnected = gscRows.length > 0;
|
|
34161
34439
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
34162
34440
|
for (const row of gscRows) {
|
|
34163
34441
|
const key = row.query.toLowerCase();
|
|
34164
34442
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
34165
34443
|
}
|
|
34166
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
34444
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq37(projects.id, projectId)).get();
|
|
34167
34445
|
const locationCount = Math.max(
|
|
34168
34446
|
1,
|
|
34169
34447
|
(projectRow?.locations ?? []).length
|
|
34170
34448
|
);
|
|
34171
34449
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
34172
34450
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
34173
|
-
|
|
34174
|
-
|
|
34175
|
-
|
|
34176
|
-
or5(
|
|
34451
|
+
and27(
|
|
34452
|
+
eq37(runs.projectId, projectId),
|
|
34453
|
+
eq37(runs.kind, RunKinds["answer-visibility"]),
|
|
34454
|
+
or5(eq37(runs.status, "completed"), eq37(runs.status, "partial")),
|
|
34177
34455
|
// Defensive — see top of file.
|
|
34178
34456
|
ne5(runs.trigger, RunTriggers.probe)
|
|
34179
34457
|
)
|
|
34180
|
-
).orderBy(
|
|
34458
|
+
).orderBy(desc18(runs.createdAt), desc18(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
34181
34459
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
34182
34460
|
const recentRunIds = [];
|
|
34183
34461
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -34193,7 +34471,7 @@ var IntelligenceService = class {
|
|
|
34193
34471
|
const haveHistory = recentRunIds.length > 0;
|
|
34194
34472
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
34195
34473
|
if (haveHistory) {
|
|
34196
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
34474
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and27(eq37(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
|
|
34197
34475
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
34198
34476
|
for (const row of priorRows) {
|
|
34199
34477
|
if (!row.runId) continue;
|
|
@@ -34222,7 +34500,7 @@ var IntelligenceService = class {
|
|
|
34222
34500
|
});
|
|
34223
34501
|
}
|
|
34224
34502
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
34225
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
34503
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq37(projects.id, projectId)).get();
|
|
34226
34504
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
34227
34505
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
34228
34506
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -34235,10 +34513,11 @@ var IntelligenceService = class {
|
|
|
34235
34513
|
queryText: querySnapshots.queryText,
|
|
34236
34514
|
provider: querySnapshots.provider,
|
|
34237
34515
|
citationState: querySnapshots.citationState,
|
|
34516
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
34238
34517
|
citedDomains: querySnapshots.citedDomains,
|
|
34239
34518
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
34240
34519
|
snapshotLocation: querySnapshots.location
|
|
34241
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
34520
|
+
}).from(querySnapshots).leftJoin(queries, eq37(querySnapshots.queryId, queries.id)).where(eq37(querySnapshots.runId, runId)).all();
|
|
34242
34521
|
const snapshots = [];
|
|
34243
34522
|
let orphanCount = 0;
|
|
34244
34523
|
for (const r of rows) {
|
|
@@ -34253,6 +34532,10 @@ var IntelligenceService = class {
|
|
|
34253
34532
|
query: resolvedQuery,
|
|
34254
34533
|
provider: r.provider,
|
|
34255
34534
|
cited: r.citationState === CitationStates.cited,
|
|
34535
|
+
// Independent answer-text signal. Tri-state passes through untouched
|
|
34536
|
+
// (true / false / null = "not checked"); computeHealth counts only
|
|
34537
|
+
// exact `true`. Never coerce null→false here.
|
|
34538
|
+
answerMentioned: r.answerMentioned,
|
|
34256
34539
|
// The project's OWN cited domain — never a co-cited competitor that
|
|
34257
34540
|
// happens to sort first in the full citedDomains set.
|
|
34258
34541
|
citationUrl: pickProjectCitedDomain(domains, projectDomains),
|