@ainyc/canonry 4.100.1 → 4.102.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/assets/assets/{BacklinksPage-v2RC8wlg.js → BacklinksPage-lXt19SLR.js} +1 -1
- package/assets/assets/{ChartPrimitives-B74e5pqn.js → ChartPrimitives-C-sLgrDV.js} +1 -1
- package/assets/assets/ProjectPage-wtvaWIRA.js +7 -0
- package/assets/assets/{RunRow-BmVPjO-m.js → RunRow-Db32k-uH.js} +1 -1
- package/assets/assets/{RunsPage-CK5atXvL.js → RunsPage-DwW3Mely.js} +1 -1
- package/assets/assets/{SettingsPage-DarPFUPl.js → SettingsPage-gXn6OGsu.js} +1 -1
- package/assets/assets/{TrafficPage-Ddnn_WqU.js → TrafficPage-I49ci4gj.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-g08SKnwZ.js → TrafficSourceDetailPage-BCvLztx9.js} +1 -1
- package/assets/assets/{arrow-left-D39Vxasb.js → arrow-left-BpA06YY0.js} +1 -1
- package/assets/assets/{extract-error-message-BiNWqBeh.js → extract-error-message-BZSIQ5iV.js} +1 -1
- package/assets/assets/{index-Dd4J1VhB.js → index-DSSLug80.js} +64 -64
- package/assets/assets/{trash-2-xh6gsnap.js → trash-2-CLkUNdvb.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-IDI6LMYL.js → chunk-2FUODJQR.js} +6 -2
- package/dist/{chunk-ALJSES3X.js → chunk-3PVCILSN.js} +565 -51
- package/dist/{chunk-YBIT5O62.js → chunk-B2H2RRIT.js} +927 -614
- package/dist/{chunk-JF7EXHGA.js → chunk-EOQXB63S.js} +30 -2
- package/dist/cli.js +44 -11
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-TWCEKVKZ.js → intelligence-service-RC4BY5HF.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/ProjectPage-BCzS7TQn.js +0 -6
|
@@ -263,10 +263,10 @@ import {
|
|
|
263
263
|
wordpressSchemaDeployResultDtoSchema,
|
|
264
264
|
wordpressSchemaStatusResultDtoSchema,
|
|
265
265
|
wordpressStatusDtoSchema
|
|
266
|
-
} from "./chunk-
|
|
266
|
+
} from "./chunk-EOQXB63S.js";
|
|
267
267
|
|
|
268
268
|
// src/intelligence-service.ts
|
|
269
|
-
import { eq as
|
|
269
|
+
import { eq as eq38, desc as desc18, asc as asc6, and as and28, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
270
270
|
|
|
271
271
|
// ../db/src/client.ts
|
|
272
272
|
import { mkdirSync } from "fs";
|
|
@@ -284,6 +284,7 @@ __export(schema_exports, {
|
|
|
284
284
|
adsInsightsDaily: () => adsInsightsDaily,
|
|
285
285
|
agentMemory: () => agentMemory,
|
|
286
286
|
agentSessions: () => agentSessions,
|
|
287
|
+
agentToolEvents: () => agentToolEvents,
|
|
287
288
|
aiReferralEventsHourly: () => aiReferralEventsHourly,
|
|
288
289
|
aiUserFetchEventsHourly: () => aiUserFetchEventsHourly,
|
|
289
290
|
apiKeys: () => apiKeys,
|
|
@@ -317,10 +318,12 @@ __export(schema_exports, {
|
|
|
317
318
|
gbpPlaceDetails: () => gbpPlaceDetails,
|
|
318
319
|
googleConnections: () => googleConnections,
|
|
319
320
|
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
321
|
+
gscDailyTotals: () => gscDailyTotals,
|
|
320
322
|
gscSearchData: () => gscSearchData,
|
|
321
323
|
gscUrlInspections: () => gscUrlInspections,
|
|
322
324
|
healthSnapshots: () => healthSnapshots,
|
|
323
325
|
insights: () => insights,
|
|
326
|
+
llmUsageEvents: () => llmUsageEvents,
|
|
324
327
|
migrationsTable: () => migrationsTable,
|
|
325
328
|
notifications: () => notifications,
|
|
326
329
|
projects: () => projects,
|
|
@@ -466,11 +469,20 @@ var apiKeys = sqliteTable("api_keys", {
|
|
|
466
469
|
keyHash: text("key_hash").notNull().unique(),
|
|
467
470
|
keyPrefix: text("key_prefix").notNull(),
|
|
468
471
|
scopes: text("scopes", { mode: "json" }).$type().notNull().default(["*"]),
|
|
472
|
+
/**
|
|
473
|
+
* When set, the key is scoped to a SINGLE project: every project-scoped read
|
|
474
|
+
* or write is gated to this project id (enforced centrally in `auth.ts`).
|
|
475
|
+
* NULL keeps full-instance access — the historical default for every key
|
|
476
|
+
* `canonry init` / `canonry key create` writes. Cascade-deletes with the
|
|
477
|
+
* project so a stale scoped key never outlives its project.
|
|
478
|
+
*/
|
|
479
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
469
480
|
createdAt: text("created_at").notNull(),
|
|
470
481
|
lastUsedAt: text("last_used_at"),
|
|
471
482
|
revokedAt: text("revoked_at")
|
|
472
483
|
}, (table) => [
|
|
473
|
-
index("idx_api_keys_prefix").on(table.keyPrefix)
|
|
484
|
+
index("idx_api_keys_prefix").on(table.keyPrefix),
|
|
485
|
+
index("idx_api_keys_project").on(table.projectId)
|
|
474
486
|
]);
|
|
475
487
|
var schedules = sqliteTable("schedules", {
|
|
476
488
|
id: text("id").primaryKey(),
|
|
@@ -546,6 +558,20 @@ var gscSearchData = sqliteTable("gsc_search_data", {
|
|
|
546
558
|
index("idx_gsc_search_query").on(table.query),
|
|
547
559
|
index("idx_gsc_search_run").on(table.syncRunId)
|
|
548
560
|
]);
|
|
561
|
+
var gscDailyTotals = sqliteTable("gsc_daily_totals", {
|
|
562
|
+
id: text("id").primaryKey(),
|
|
563
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
564
|
+
date: text("date").notNull(),
|
|
565
|
+
clicks: integer("clicks").notNull(),
|
|
566
|
+
impressions: integer("impressions").notNull(),
|
|
567
|
+
// Stored as a string like `gscSearchData.position` (parsed to a number on
|
|
568
|
+
// read). CTR is derived (clicks / impressions) and intentionally not stored.
|
|
569
|
+
position: text("position").notNull(),
|
|
570
|
+
createdAt: text("created_at").notNull()
|
|
571
|
+
}, (table) => [
|
|
572
|
+
uniqueIndex("idx_gsc_daily_totals_project_date").on(table.projectId, table.date),
|
|
573
|
+
index("idx_gsc_daily_totals_project").on(table.projectId)
|
|
574
|
+
]);
|
|
549
575
|
var gscUrlInspections = sqliteTable("gsc_url_inspections", {
|
|
550
576
|
id: text("id").primaryKey(),
|
|
551
577
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -924,6 +950,53 @@ var agentMemory = sqliteTable("agent_memory", {
|
|
|
924
950
|
uniqueIndex("uniq_agent_memory_project_key").on(table.projectId, table.key),
|
|
925
951
|
index("idx_agent_memory_project_updated").on(table.projectId, table.updatedAt)
|
|
926
952
|
]);
|
|
953
|
+
var llmUsageEvents = sqliteTable("llm_usage_events", {
|
|
954
|
+
id: text("id").primaryKey(),
|
|
955
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
956
|
+
runId: text("run_id").references(() => runs.id, { onDelete: "set null" }),
|
|
957
|
+
agentSessionId: text("agent_session_id").references(() => agentSessions.id, { onDelete: "set null" }),
|
|
958
|
+
feature: text("feature").notNull(),
|
|
959
|
+
provider: text("provider").notNull(),
|
|
960
|
+
model: text("model").notNull(),
|
|
961
|
+
responseId: text("response_id"),
|
|
962
|
+
inputTokens: integer("input_tokens").notNull().default(0),
|
|
963
|
+
outputTokens: integer("output_tokens").notNull().default(0),
|
|
964
|
+
cacheReadTokens: integer("cache_read_tokens").notNull().default(0),
|
|
965
|
+
cacheWriteTokens: integer("cache_write_tokens").notNull().default(0),
|
|
966
|
+
totalTokens: integer("total_tokens").notNull().default(0),
|
|
967
|
+
costMillicents: integer("cost_millicents").notNull().default(0),
|
|
968
|
+
promptFamily: text("prompt_family"),
|
|
969
|
+
promptVersion: text("prompt_version"),
|
|
970
|
+
metadata: text("metadata", { mode: "json" }).$type(),
|
|
971
|
+
createdAt: text("created_at").notNull()
|
|
972
|
+
}, (table) => [
|
|
973
|
+
index("idx_llm_usage_project_created").on(table.projectId, table.createdAt),
|
|
974
|
+
index("idx_llm_usage_feature_created").on(table.feature, table.createdAt),
|
|
975
|
+
index("idx_llm_usage_session_created").on(table.agentSessionId, table.createdAt),
|
|
976
|
+
index("idx_llm_usage_run_created").on(table.runId, table.createdAt)
|
|
977
|
+
]);
|
|
978
|
+
var agentToolEvents = sqliteTable("agent_tool_events", {
|
|
979
|
+
id: text("id").primaryKey(),
|
|
980
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
981
|
+
agentSessionId: text("agent_session_id").references(() => agentSessions.id, { onDelete: "set null" }),
|
|
982
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
983
|
+
toolName: text("tool_name").notNull(),
|
|
984
|
+
assistantResponseId: text("assistant_response_id"),
|
|
985
|
+
provider: text("provider"),
|
|
986
|
+
model: text("model"),
|
|
987
|
+
status: text("status").notNull(),
|
|
988
|
+
durationMs: integer("duration_ms").notNull().default(0),
|
|
989
|
+
argsBytes: integer("args_bytes").notNull().default(0),
|
|
990
|
+
resultTextChars: integer("result_text_chars").notNull().default(0),
|
|
991
|
+
resultBytes: integer("result_bytes").notNull().default(0),
|
|
992
|
+
metadata: text("metadata", { mode: "json" }).$type(),
|
|
993
|
+
createdAt: text("created_at").notNull()
|
|
994
|
+
}, (table) => [
|
|
995
|
+
index("idx_agent_tool_events_project_created").on(table.projectId, table.createdAt),
|
|
996
|
+
index("idx_agent_tool_events_session_created").on(table.agentSessionId, table.createdAt),
|
|
997
|
+
index("idx_agent_tool_events_tool_created").on(table.toolName, table.createdAt),
|
|
998
|
+
index("idx_agent_tool_events_status_created").on(table.status, table.createdAt)
|
|
999
|
+
]);
|
|
927
1000
|
var trafficSources = sqliteTable("traffic_sources", {
|
|
928
1001
|
id: text("id").primaryKey(),
|
|
929
1002
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -3211,6 +3284,91 @@ var MIGRATION_VERSIONS = [
|
|
|
3211
3284
|
`ALTER TABLE ads_insights_daily ADD COLUMN conversions INTEGER NOT NULL DEFAULT 0`,
|
|
3212
3285
|
`ALTER TABLE ads_connections ADD COLUMN conversion_tracking_configured INTEGER NOT NULL DEFAULT 0`
|
|
3213
3286
|
]
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
version: 84,
|
|
3290
|
+
name: "llm-usage-events",
|
|
3291
|
+
statements: [
|
|
3292
|
+
`CREATE TABLE IF NOT EXISTS llm_usage_events (
|
|
3293
|
+
id TEXT PRIMARY KEY,
|
|
3294
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
3295
|
+
run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
3296
|
+
agent_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
|
3297
|
+
feature TEXT NOT NULL,
|
|
3298
|
+
provider TEXT NOT NULL,
|
|
3299
|
+
model TEXT NOT NULL,
|
|
3300
|
+
response_id TEXT,
|
|
3301
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3302
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3303
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3304
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3305
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
3306
|
+
cost_millicents INTEGER NOT NULL DEFAULT 0,
|
|
3307
|
+
prompt_family TEXT,
|
|
3308
|
+
prompt_version TEXT,
|
|
3309
|
+
metadata TEXT,
|
|
3310
|
+
created_at TEXT NOT NULL
|
|
3311
|
+
)`,
|
|
3312
|
+
`CREATE INDEX IF NOT EXISTS idx_llm_usage_project_created ON llm_usage_events(project_id, created_at)`,
|
|
3313
|
+
`CREATE INDEX IF NOT EXISTS idx_llm_usage_feature_created ON llm_usage_events(feature, created_at)`,
|
|
3314
|
+
`CREATE INDEX IF NOT EXISTS idx_llm_usage_session_created ON llm_usage_events(agent_session_id, created_at)`,
|
|
3315
|
+
`CREATE INDEX IF NOT EXISTS idx_llm_usage_run_created ON llm_usage_events(run_id, created_at)`
|
|
3316
|
+
]
|
|
3317
|
+
},
|
|
3318
|
+
{
|
|
3319
|
+
version: 85,
|
|
3320
|
+
name: "agent-tool-events",
|
|
3321
|
+
statements: [
|
|
3322
|
+
`CREATE TABLE IF NOT EXISTS agent_tool_events (
|
|
3323
|
+
id TEXT PRIMARY KEY,
|
|
3324
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
3325
|
+
agent_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
|
3326
|
+
tool_call_id TEXT NOT NULL,
|
|
3327
|
+
tool_name TEXT NOT NULL,
|
|
3328
|
+
assistant_response_id TEXT,
|
|
3329
|
+
provider TEXT,
|
|
3330
|
+
model TEXT,
|
|
3331
|
+
status TEXT NOT NULL,
|
|
3332
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
3333
|
+
args_bytes INTEGER NOT NULL DEFAULT 0,
|
|
3334
|
+
result_text_chars INTEGER NOT NULL DEFAULT 0,
|
|
3335
|
+
result_bytes INTEGER NOT NULL DEFAULT 0,
|
|
3336
|
+
metadata TEXT,
|
|
3337
|
+
created_at TEXT NOT NULL
|
|
3338
|
+
)`,
|
|
3339
|
+
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_project_created ON agent_tool_events(project_id, created_at)`,
|
|
3340
|
+
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_session_created ON agent_tool_events(agent_session_id, created_at)`,
|
|
3341
|
+
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_tool_created ON agent_tool_events(tool_name, created_at)`,
|
|
3342
|
+
`CREATE INDEX IF NOT EXISTS idx_agent_tool_events_status_created ON agent_tool_events(status, created_at)`
|
|
3343
|
+
]
|
|
3344
|
+
},
|
|
3345
|
+
{
|
|
3346
|
+
version: 86,
|
|
3347
|
+
name: "gsc-daily-totals",
|
|
3348
|
+
statements: [
|
|
3349
|
+
`CREATE TABLE IF NOT EXISTS gsc_daily_totals (
|
|
3350
|
+
id TEXT PRIMARY KEY,
|
|
3351
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
3352
|
+
date TEXT NOT NULL,
|
|
3353
|
+
clicks INTEGER NOT NULL,
|
|
3354
|
+
impressions INTEGER NOT NULL,
|
|
3355
|
+
position TEXT NOT NULL,
|
|
3356
|
+
created_at TEXT NOT NULL
|
|
3357
|
+
)`,
|
|
3358
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_gsc_daily_totals_project_date ON gsc_daily_totals(project_id, date)`,
|
|
3359
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_daily_totals_project ON gsc_daily_totals(project_id)`
|
|
3360
|
+
]
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
version: 87,
|
|
3364
|
+
name: "api-key-project-scope",
|
|
3365
|
+
statements: [
|
|
3366
|
+
// Opt-in single-project scoping for API keys. NULL = full-instance access
|
|
3367
|
+
// (the historical default), so this is a no-op for every existing key.
|
|
3368
|
+
// ALTER ADD COLUMN is idempotent (the runner swallows duplicate-column on retry).
|
|
3369
|
+
`ALTER TABLE api_keys ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE CASCADE`,
|
|
3370
|
+
`CREATE INDEX IF NOT EXISTS idx_api_keys_project ON api_keys(project_id)`
|
|
3371
|
+
]
|
|
3214
3372
|
}
|
|
3215
3373
|
];
|
|
3216
3374
|
function rebuildBacklinkTableWithSource(tx, table) {
|
|
@@ -5545,6 +5703,12 @@ function requireScope(request, scope) {
|
|
|
5545
5703
|
if (key.scopes.includes("*") || key.scopes.includes(scope)) return;
|
|
5546
5704
|
throw forbidden(`This action requires the "${scope}" scope on your API key.`);
|
|
5547
5705
|
}
|
|
5706
|
+
function assertProjectScope(request, projectId) {
|
|
5707
|
+
const scoped = request.apiKey?.projectId;
|
|
5708
|
+
if (scoped && scoped !== projectId) {
|
|
5709
|
+
throw forbidden("This API key is scoped to a different project.");
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5548
5712
|
function hashApiKey(key) {
|
|
5549
5713
|
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
5550
5714
|
}
|
|
@@ -5605,10 +5769,20 @@ async function authPlugin(app, opts = {}) {
|
|
|
5605
5769
|
}
|
|
5606
5770
|
app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(apiKeys.id, key.id)).run();
|
|
5607
5771
|
const scopes = Array.isArray(key.scopes) ? key.scopes : [];
|
|
5608
|
-
request.apiKey = { id: key.id, name: key.name, scopes };
|
|
5772
|
+
request.apiKey = { id: key.id, name: key.name, scopes, projectId: key.projectId ?? null };
|
|
5609
5773
|
if (isReadOnlyKey(scopes) && WRITE_METHODS.has(request.method)) {
|
|
5610
5774
|
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
5611
5775
|
}
|
|
5776
|
+
if (key.projectId) {
|
|
5777
|
+
const match = url.match(/\/projects\/([^/?#]+)/);
|
|
5778
|
+
if (match) {
|
|
5779
|
+
const projectName = decodeURIComponent(match[1]);
|
|
5780
|
+
const scoped = app.db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectName)).get();
|
|
5781
|
+
if (!scoped || scoped.id !== key.projectId) {
|
|
5782
|
+
throw forbidden("This API key is scoped to a different project.");
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5612
5786
|
});
|
|
5613
5787
|
}
|
|
5614
5788
|
|
|
@@ -5800,8 +5974,9 @@ async function projectRoutes(app, opts) {
|
|
|
5800
5974
|
const created = app.db.select().from(projects).where(eq3(projects.id, id)).get();
|
|
5801
5975
|
return reply.status(201).send(formatProject(created));
|
|
5802
5976
|
});
|
|
5803
|
-
app.get("/projects", async (
|
|
5804
|
-
const
|
|
5977
|
+
app.get("/projects", async (request, reply) => {
|
|
5978
|
+
const scoped = request.apiKey?.projectId;
|
|
5979
|
+
const rows = scoped ? app.db.select().from(projects).where(eq3(projects.id, scoped)).all() : app.db.select().from(projects).all();
|
|
5805
5980
|
return reply.send(rows.map(formatProject));
|
|
5806
5981
|
});
|
|
5807
5982
|
app.get("/projects/:name", async (request, reply) => {
|
|
@@ -6638,11 +6813,14 @@ async function runRoutes(app, opts) {
|
|
|
6638
6813
|
const filters = [gte(runs.createdAt, since)];
|
|
6639
6814
|
if (!includeProbe) filters.push(notProbeRun());
|
|
6640
6815
|
if (kind) filters.push(eq7(runs.kind, kind));
|
|
6816
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
6817
|
+
if (scopedProjectId) filters.push(eq7(runs.projectId, scopedProjectId));
|
|
6641
6818
|
const rows = app.db.select().from(runs).where(and2(...filters)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(limit).all();
|
|
6642
6819
|
return reply.send(rows.map(formatRun));
|
|
6643
6820
|
});
|
|
6644
6821
|
app.post("/runs", async (request, reply) => {
|
|
6645
|
-
const
|
|
6822
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
6823
|
+
const allProjects = (scopedProjectId ? app.db.select().from(projects).where(eq7(projects.id, scopedProjectId)) : app.db.select().from(projects)).all();
|
|
6646
6824
|
if (allProjects.length === 0) {
|
|
6647
6825
|
return reply.status(207).send([]);
|
|
6648
6826
|
}
|
|
@@ -6708,6 +6886,7 @@ async function runRoutes(app, opts) {
|
|
|
6708
6886
|
app.post("/runs/:id/cancel", async (request, reply) => {
|
|
6709
6887
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
6710
6888
|
if (!run) throw notFound("Run", request.params.id);
|
|
6889
|
+
assertProjectScope(request, run.projectId);
|
|
6711
6890
|
const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
|
|
6712
6891
|
if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
|
|
6713
6892
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -6725,6 +6904,7 @@ async function runRoutes(app, opts) {
|
|
|
6725
6904
|
app.get("/runs/:id", async (request, reply) => {
|
|
6726
6905
|
const run = app.db.select().from(runs).where(eq7(runs.id, request.params.id)).get();
|
|
6727
6906
|
if (!run) throw notFound("Run", request.params.id);
|
|
6907
|
+
assertProjectScope(request, run.projectId);
|
|
6728
6908
|
return reply.send(loadRunDetail(app, run));
|
|
6729
6909
|
});
|
|
6730
6910
|
}
|
|
@@ -7180,6 +7360,13 @@ async function applyRoutes(app, opts) {
|
|
|
7180
7360
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7181
7361
|
const name = config.metadata.name;
|
|
7182
7362
|
const configQueries = resolveConfigSpecQueries(config.spec);
|
|
7363
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
7364
|
+
if (scopedProjectId) {
|
|
7365
|
+
const target = app.db.select({ id: projects.id }).from(projects).where(eq8(projects.name, name)).get();
|
|
7366
|
+
if (!target || target.id !== scopedProjectId) {
|
|
7367
|
+
throw forbidden("This API key is scoped to a single project and cannot apply this config.");
|
|
7368
|
+
}
|
|
7369
|
+
}
|
|
7183
7370
|
let projectId;
|
|
7184
7371
|
let scheduleAction = null;
|
|
7185
7372
|
let aliasesChanged = false;
|
|
@@ -7443,8 +7630,9 @@ async function historyRoutes(app) {
|
|
|
7443
7630
|
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
7444
7631
|
return reply.send(rows.map(formatAuditEntry));
|
|
7445
7632
|
});
|
|
7446
|
-
app.get("/history", async (
|
|
7447
|
-
const
|
|
7633
|
+
app.get("/history", async (request, reply) => {
|
|
7634
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
7635
|
+
const rows = app.db.select().from(auditLog).where(scopedProjectId ? eq9(auditLog.projectId, scopedProjectId) : void 0).orderBy(desc2(auditLog.createdAt)).all();
|
|
7448
7636
|
return reply.send(rows.map(formatAuditEntry));
|
|
7449
7637
|
});
|
|
7450
7638
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -7625,6 +7813,15 @@ async function historyRoutes(app) {
|
|
|
7625
7813
|
if (!run1 || !run2) {
|
|
7626
7814
|
throw validationError("Both run1 and run2 query params are required");
|
|
7627
7815
|
}
|
|
7816
|
+
const requestedRunIds = [.../* @__PURE__ */ new Set([run1, run2])];
|
|
7817
|
+
const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray2(runs.id, requestedRunIds)).all();
|
|
7818
|
+
const runsById = new Map(runRows.map((row) => [row.id, row]));
|
|
7819
|
+
for (const runId of requestedRunIds) {
|
|
7820
|
+
const run = runsById.get(runId);
|
|
7821
|
+
if (!run || run.projectId !== project.id) {
|
|
7822
|
+
throw notFound("Run", runId);
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7628
7825
|
const snaps1 = app.db.select({
|
|
7629
7826
|
queryId: querySnapshots.queryId,
|
|
7630
7827
|
query: queries.query,
|
|
@@ -7736,6 +7933,10 @@ async function analyticsRoutes(app) {
|
|
|
7736
7933
|
}));
|
|
7737
7934
|
const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(eq10(queries.projectId, project.id)).all();
|
|
7738
7935
|
const queryCreatedAt = new Map(projectQueries.map((q) => [q.id, q.createdAt]));
|
|
7936
|
+
const mentionShareCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq10(competitors.projectId, project.id)).all().map((c) => ({
|
|
7937
|
+
domain: c.domain,
|
|
7938
|
+
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
7939
|
+
}));
|
|
7739
7940
|
const overall = computeProviderMetric(allSnapshots);
|
|
7740
7941
|
const byProvider = {};
|
|
7741
7942
|
const providers = new Set(allSnapshots.map((s) => s.provider));
|
|
@@ -7746,7 +7947,7 @@ async function analyticsRoutes(app) {
|
|
|
7746
7947
|
const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
|
|
7747
7948
|
const spanDays = Math.max(1, Math.ceil((latest.getTime() - earliest.getTime()) / 864e5));
|
|
7748
7949
|
const bucketSize = bucketSizeForSpan(spanDays);
|
|
7749
|
-
const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, queryCreatedAt);
|
|
7950
|
+
const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, queryCreatedAt, mentionShareCompetitors);
|
|
7750
7951
|
const trend = computeTrend(buckets, "citationRate");
|
|
7751
7952
|
const mentionTrend = computeTrend(buckets, "mentionRate");
|
|
7752
7953
|
const queryChanges = computeQueryChanges(projectQueries, cutoff);
|
|
@@ -7993,7 +8194,7 @@ function computeProviderMetric(snapshots) {
|
|
|
7993
8194
|
mentionedCount
|
|
7994
8195
|
};
|
|
7995
8196
|
}
|
|
7996
|
-
function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
|
|
8197
|
+
function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt, mentionShareCompetitors = []) {
|
|
7997
8198
|
if (projectRuns.length === 0) return [];
|
|
7998
8199
|
const earliest = new Date(projectRuns[0].createdAt);
|
|
7999
8200
|
const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
|
|
@@ -8030,6 +8231,7 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
|
|
|
8030
8231
|
queryCount,
|
|
8031
8232
|
mentionRate: metric.mentionRate,
|
|
8032
8233
|
mentionedCount: metric.mentionedCount,
|
|
8234
|
+
mentionShare: computeMentionShareBucketMetric(usable, mentionShareCompetitors),
|
|
8033
8235
|
byProvider
|
|
8034
8236
|
});
|
|
8035
8237
|
}
|
|
@@ -8037,6 +8239,26 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
|
|
|
8037
8239
|
}
|
|
8038
8240
|
return buckets;
|
|
8039
8241
|
}
|
|
8242
|
+
function computeMentionShareBucketMetric(snapshots, mentionShareCompetitors) {
|
|
8243
|
+
if (mentionShareCompetitors.length === 0) {
|
|
8244
|
+
return { rate: null, projectMentionSnapshots: 0, competitorMentionSnapshots: 0 };
|
|
8245
|
+
}
|
|
8246
|
+
const result = buildMentionShare(
|
|
8247
|
+
snapshots.map((s) => ({
|
|
8248
|
+
projectMentioned: s.resolvedMentioned,
|
|
8249
|
+
answerText: s.answerText
|
|
8250
|
+
})),
|
|
8251
|
+
{ competitors: mentionShareCompetitors }
|
|
8252
|
+
);
|
|
8253
|
+
const projectMentionSnapshots = result.breakdown.projectMentionSnapshots;
|
|
8254
|
+
const competitorMentionSnapshots = result.breakdown.competitorMentionSnapshots;
|
|
8255
|
+
const denominator = projectMentionSnapshots + competitorMentionSnapshots;
|
|
8256
|
+
return {
|
|
8257
|
+
rate: denominator > 0 ? round4(projectMentionSnapshots / denominator) : null,
|
|
8258
|
+
projectMentionSnapshots,
|
|
8259
|
+
competitorMentionSnapshots
|
|
8260
|
+
};
|
|
8261
|
+
}
|
|
8040
8262
|
function computeQueryChanges(projectQueries, cutoff) {
|
|
8041
8263
|
const byDay = /* @__PURE__ */ new Map();
|
|
8042
8264
|
for (const q of projectQueries) {
|
|
@@ -8338,7 +8560,7 @@ async function intelligenceRoutes(app) {
|
|
|
8338
8560
|
}
|
|
8339
8561
|
|
|
8340
8562
|
// ../api-routes/src/report.ts
|
|
8341
|
-
import { and as
|
|
8563
|
+
import { and as and10, desc as desc7, eq as eq15, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql7 } from "drizzle-orm";
|
|
8342
8564
|
|
|
8343
8565
|
// ../api-routes/src/content.ts
|
|
8344
8566
|
import crypto11 from "crypto";
|
|
@@ -9073,6 +9295,38 @@ function winnabilityClassRank(winnabilityClass) {
|
|
|
9073
9295
|
return winnabilityClass === WinnabilityClasses.ownable ? 0 : 1;
|
|
9074
9296
|
}
|
|
9075
9297
|
|
|
9298
|
+
// ../api-routes/src/gsc-totals.ts
|
|
9299
|
+
import { and as and9, asc as asc2, eq as eq14, sql as sql6 } from "drizzle-orm";
|
|
9300
|
+
function readGscDailyTotals(db, projectId, startDate, endDate) {
|
|
9301
|
+
const rows = db.select({
|
|
9302
|
+
date: gscDailyTotals.date,
|
|
9303
|
+
clicks: gscDailyTotals.clicks,
|
|
9304
|
+
impressions: gscDailyTotals.impressions,
|
|
9305
|
+
position: gscDailyTotals.position
|
|
9306
|
+
}).from(gscDailyTotals).where(
|
|
9307
|
+
and9(
|
|
9308
|
+
eq14(gscDailyTotals.projectId, projectId),
|
|
9309
|
+
sql6`${gscDailyTotals.date} >= ${startDate}`,
|
|
9310
|
+
sql6`${gscDailyTotals.date} <= ${endDate}`
|
|
9311
|
+
)
|
|
9312
|
+
).orderBy(asc2(gscDailyTotals.date)).all();
|
|
9313
|
+
return rows.map((r) => {
|
|
9314
|
+
const position = Number(r.position);
|
|
9315
|
+
return {
|
|
9316
|
+
date: r.date,
|
|
9317
|
+
clicks: r.clicks,
|
|
9318
|
+
impressions: r.impressions,
|
|
9319
|
+
position: Number.isFinite(position) ? position : 0
|
|
9320
|
+
};
|
|
9321
|
+
});
|
|
9322
|
+
}
|
|
9323
|
+
function mergeGscDailyTotalsWithFallback(propertyTotals, dimensionedFallback) {
|
|
9324
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
9325
|
+
for (const row of dimensionedFallback) byDate.set(row.date, row);
|
|
9326
|
+
for (const row of propertyTotals) byDate.set(row.date, row);
|
|
9327
|
+
return [...byDate.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
9328
|
+
}
|
|
9329
|
+
|
|
9076
9330
|
// ../api-routes/src/report-renderer.ts
|
|
9077
9331
|
var COLORS = {
|
|
9078
9332
|
bg: "#09090b",
|
|
@@ -11384,38 +11638,56 @@ function loadSnapshotsForRunIds(db, runIds) {
|
|
|
11384
11638
|
}));
|
|
11385
11639
|
}
|
|
11386
11640
|
function loadQueryLookup(db, projectId) {
|
|
11387
|
-
const rows = db.select().from(queries).where(
|
|
11641
|
+
const rows = db.select().from(queries).where(eq15(queries.projectId, projectId)).all();
|
|
11388
11642
|
const byId = /* @__PURE__ */ new Map();
|
|
11389
11643
|
for (const row of rows) byId.set(row.id, row.query);
|
|
11390
11644
|
return { byId };
|
|
11391
11645
|
}
|
|
11392
11646
|
function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays) {
|
|
11393
|
-
const allRows = db.select().from(gscSearchData).where(
|
|
11394
|
-
|
|
11647
|
+
const allRows = db.select().from(gscSearchData).where(eq15(gscSearchData.projectId, projectId)).all();
|
|
11648
|
+
const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
|
|
11395
11649
|
let maxDate = "";
|
|
11396
11650
|
for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
|
|
11651
|
+
for (const r of allDailyTotals) if (r.date > maxDate) maxDate = r.date;
|
|
11652
|
+
if (!maxDate) return null;
|
|
11397
11653
|
const startDate = windowStartDate(maxDate, windowDays);
|
|
11398
11654
|
const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
let
|
|
11402
|
-
let weightedPositionSum = 0;
|
|
11655
|
+
const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
|
|
11656
|
+
if (rows.length === 0 && dailyTotals.length === 0) return null;
|
|
11657
|
+
let dimensionedClicks = 0;
|
|
11403
11658
|
const queryAgg = /* @__PURE__ */ new Map();
|
|
11404
|
-
const
|
|
11659
|
+
const dimensionedTrendAgg = /* @__PURE__ */ new Map();
|
|
11405
11660
|
for (const r of rows) {
|
|
11406
|
-
|
|
11407
|
-
totalImpressions += r.impressions;
|
|
11408
|
-
weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11661
|
+
dimensionedClicks += r.clicks;
|
|
11409
11662
|
const q = queryAgg.get(r.query) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
|
|
11410
11663
|
q.clicks += r.clicks;
|
|
11411
11664
|
q.impressions += r.impressions;
|
|
11412
11665
|
q.weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11413
11666
|
queryAgg.set(r.query, q);
|
|
11414
|
-
const t =
|
|
11667
|
+
const t = dimensionedTrendAgg.get(r.date) ?? { clicks: 0, impressions: 0, weightedPositionSum: 0 };
|
|
11415
11668
|
t.clicks += r.clicks;
|
|
11416
11669
|
t.impressions += r.impressions;
|
|
11417
|
-
|
|
11670
|
+
t.weightedPositionSum += safeNum(r.position) * r.impressions;
|
|
11671
|
+
dimensionedTrendAgg.set(r.date, t);
|
|
11672
|
+
}
|
|
11673
|
+
const dailySeries = mergeGscDailyTotalsWithFallback(
|
|
11674
|
+
dailyTotals,
|
|
11675
|
+
[...dimensionedTrendAgg.entries()].map(([date, agg]) => ({
|
|
11676
|
+
date,
|
|
11677
|
+
clicks: agg.clicks,
|
|
11678
|
+
impressions: agg.impressions,
|
|
11679
|
+
position: agg.impressions > 0 ? agg.weightedPositionSum / agg.impressions : 0
|
|
11680
|
+
}))
|
|
11681
|
+
);
|
|
11682
|
+
let totalClicks = 0;
|
|
11683
|
+
let totalImpressions = 0;
|
|
11684
|
+
let weightedPositionSum = 0;
|
|
11685
|
+
for (const d of dailySeries) {
|
|
11686
|
+
totalClicks += d.clicks;
|
|
11687
|
+
totalImpressions += d.impressions;
|
|
11688
|
+
weightedPositionSum += d.position * d.impressions;
|
|
11418
11689
|
}
|
|
11690
|
+
const trend = dailySeries.map((d) => ({ date: d.date, clicks: d.clicks, impressions: d.impressions }));
|
|
11419
11691
|
const ctr = totalImpressions > 0 ? totalClicks / totalImpressions : 0;
|
|
11420
11692
|
const avgPosition = totalImpressions > 0 ? weightedPositionSum / totalImpressions : 0;
|
|
11421
11693
|
const topQueries = [...queryAgg.entries()].map(([query, agg]) => ({
|
|
@@ -11438,9 +11710,8 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
11438
11710
|
category,
|
|
11439
11711
|
clicks: agg.clicks,
|
|
11440
11712
|
impressions: agg.impressions,
|
|
11441
|
-
sharePct:
|
|
11713
|
+
sharePct: dimensionedClicks > 0 ? Math.round(agg.clicks / dimensionedClicks * 100) : 0
|
|
11442
11714
|
})).sort((a, b) => b.clicks - a.clicks);
|
|
11443
|
-
const trend = [...trendAgg.entries()].map(([date, agg]) => ({ date, clicks: agg.clicks, impressions: agg.impressions })).sort((a, b) => a.date.localeCompare(b.date));
|
|
11444
11715
|
const periodStart = trend[0]?.date ?? "";
|
|
11445
11716
|
const periodEnd = trend.at(-1)?.date ?? "";
|
|
11446
11717
|
const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
|
|
@@ -11464,13 +11735,13 @@ function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trac
|
|
|
11464
11735
|
function buildGaSection(db, projectId, windowDays) {
|
|
11465
11736
|
const gaWindowKey = GA_WINDOW_SUMMARY_KEYS[windowDays];
|
|
11466
11737
|
const windowSummary = gaWindowKey ? db.select().from(gaTrafficWindowSummaries).where(
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11738
|
+
and10(
|
|
11739
|
+
eq15(gaTrafficWindowSummaries.projectId, projectId),
|
|
11740
|
+
eq15(gaTrafficWindowSummaries.windowKey, gaWindowKey)
|
|
11470
11741
|
)
|
|
11471
11742
|
).limit(1).get() : void 0;
|
|
11472
|
-
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(
|
|
11473
|
-
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(
|
|
11743
|
+
const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq15(gaTrafficSummaries.projectId, projectId)).orderBy(desc7(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
11744
|
+
const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq15(gaTrafficSnapshots.projectId, projectId)).all();
|
|
11474
11745
|
if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
|
|
11475
11746
|
let snapshotMaxDate = "";
|
|
11476
11747
|
for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
|
|
@@ -11529,7 +11800,7 @@ function buildGaSection(db, projectId, windowDays) {
|
|
|
11529
11800
|
};
|
|
11530
11801
|
}
|
|
11531
11802
|
function buildSocialReferrals(db, projectId) {
|
|
11532
|
-
const rows = db.select().from(gaSocialReferrals).where(
|
|
11803
|
+
const rows = db.select().from(gaSocialReferrals).where(eq15(gaSocialReferrals.projectId, projectId)).all();
|
|
11533
11804
|
if (rows.length === 0) return null;
|
|
11534
11805
|
let total = 0;
|
|
11535
11806
|
let organic = 0;
|
|
@@ -11561,7 +11832,7 @@ function buildSocialReferrals(db, projectId) {
|
|
|
11561
11832
|
};
|
|
11562
11833
|
}
|
|
11563
11834
|
function buildAiReferrals(db, projectId) {
|
|
11564
|
-
const rows = db.select().from(gaAiReferrals).where(
|
|
11835
|
+
const rows = db.select().from(gaAiReferrals).where(eq15(gaAiReferrals.projectId, projectId)).all();
|
|
11565
11836
|
if (rows.length === 0) return null;
|
|
11566
11837
|
const dimSessionsByTuple = /* @__PURE__ */ new Map();
|
|
11567
11838
|
for (const r of rows) {
|
|
@@ -11618,7 +11889,7 @@ function buildAiReferrals(db, projectId) {
|
|
|
11618
11889
|
return { totalSessions: total, totalUsers, bySource, trend, topLandingPages };
|
|
11619
11890
|
}
|
|
11620
11891
|
function nonSubresourceReferralPathCondition() {
|
|
11621
|
-
return
|
|
11892
|
+
return sql7`
|
|
11622
11893
|
LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/_next/static/%'
|
|
11623
11894
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/assets/%'
|
|
11624
11895
|
AND LOWER(${aiReferralEventsHourly.landingPathNormalized}) NOT LIKE '/static/%'
|
|
@@ -11642,8 +11913,8 @@ function nonSubresourceReferralPathCondition() {
|
|
|
11642
11913
|
}
|
|
11643
11914
|
function buildServerActivity(db, projectId, windowDays) {
|
|
11644
11915
|
const sourceRows = db.select({ id: trafficSources.id }).from(trafficSources).where(
|
|
11645
|
-
|
|
11646
|
-
|
|
11916
|
+
and10(
|
|
11917
|
+
eq15(trafficSources.projectId, projectId),
|
|
11647
11918
|
ne2(trafficSources.status, TrafficSourceStatuses.archived)
|
|
11648
11919
|
)
|
|
11649
11920
|
).all();
|
|
@@ -11657,19 +11928,19 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11657
11928
|
const priorStart = new Date(priorStartMs).toISOString();
|
|
11658
11929
|
const trendStart = new Date(trendStartMs).toISOString();
|
|
11659
11930
|
const sumVerifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11660
|
-
db.select({ total:
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11931
|
+
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
11932
|
+
and10(
|
|
11933
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11934
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11664
11935
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
11665
11936
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
11666
11937
|
)
|
|
11667
11938
|
).get()?.total ?? 0
|
|
11668
11939
|
);
|
|
11669
11940
|
const sumUnverifiedCrawlers = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11670
|
-
db.select({ total:
|
|
11671
|
-
|
|
11672
|
-
|
|
11941
|
+
db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
11942
|
+
and10(
|
|
11943
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11673
11944
|
ne2(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11674
11945
|
gte2(crawlerEventsHourly.tsHour, windowStartIso),
|
|
11675
11946
|
exclusiveEnd ? lt(crawlerEventsHourly.tsHour, windowEndIso) : lte(crawlerEventsHourly.tsHour, windowEndIso)
|
|
@@ -11677,9 +11948,9 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11677
11948
|
).get()?.total ?? 0
|
|
11678
11949
|
);
|
|
11679
11950
|
const sumReferrals = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11680
|
-
db.select({ total:
|
|
11681
|
-
|
|
11682
|
-
|
|
11951
|
+
db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
11952
|
+
and10(
|
|
11953
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11683
11954
|
nonSubresourceReferralPathCondition(),
|
|
11684
11955
|
gte2(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
11685
11956
|
exclusiveEnd ? lt(aiReferralEventsHourly.tsHour, windowEndIso) : lte(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
@@ -11687,9 +11958,9 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11687
11958
|
).get()?.total ?? 0
|
|
11688
11959
|
);
|
|
11689
11960
|
const sumUserFetches = (windowStartIso, windowEndIso, exclusiveEnd = false) => Number(
|
|
11690
|
-
db.select({ total:
|
|
11691
|
-
|
|
11692
|
-
|
|
11961
|
+
db.select({ total: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
11962
|
+
and10(
|
|
11963
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11693
11964
|
gte2(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
11694
11965
|
exclusiveEnd ? lt(aiUserFetchEventsHourly.tsHour, windowEndIso) : lte(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
11695
11966
|
)
|
|
@@ -11706,31 +11977,31 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11706
11977
|
const crawlerByOperatorRows = db.select({
|
|
11707
11978
|
operator: crawlerEventsHourly.operator,
|
|
11708
11979
|
verificationStatus: crawlerEventsHourly.verificationStatus,
|
|
11709
|
-
hits:
|
|
11980
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11710
11981
|
}).from(crawlerEventsHourly).where(
|
|
11711
|
-
|
|
11712
|
-
|
|
11982
|
+
and10(
|
|
11983
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11713
11984
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
11714
11985
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11715
11986
|
)
|
|
11716
11987
|
).groupBy(crawlerEventsHourly.operator, crawlerEventsHourly.verificationStatus).all();
|
|
11717
11988
|
const crawlerByOperatorPriorRows = db.select({
|
|
11718
11989
|
operator: crawlerEventsHourly.operator,
|
|
11719
|
-
hits:
|
|
11990
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11720
11991
|
}).from(crawlerEventsHourly).where(
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11992
|
+
and10(
|
|
11993
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
11994
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11724
11995
|
gte2(crawlerEventsHourly.tsHour, priorStart),
|
|
11725
11996
|
lt(crawlerEventsHourly.tsHour, headlineStart)
|
|
11726
11997
|
)
|
|
11727
11998
|
).groupBy(crawlerEventsHourly.operator).all();
|
|
11728
11999
|
const referralByOperatorRows = db.select({
|
|
11729
12000
|
operator: aiReferralEventsHourly.operator,
|
|
11730
|
-
hits:
|
|
12001
|
+
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
11731
12002
|
}).from(aiReferralEventsHourly).where(
|
|
11732
|
-
|
|
11733
|
-
|
|
12003
|
+
and10(
|
|
12004
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11734
12005
|
nonSubresourceReferralPathCondition(),
|
|
11735
12006
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11736
12007
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
@@ -11738,10 +12009,10 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11738
12009
|
).groupBy(aiReferralEventsHourly.operator).all();
|
|
11739
12010
|
const userFetchByOperatorRows = db.select({
|
|
11740
12011
|
operator: aiUserFetchEventsHourly.operator,
|
|
11741
|
-
hits:
|
|
12012
|
+
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
11742
12013
|
}).from(aiUserFetchEventsHourly).where(
|
|
11743
|
-
|
|
11744
|
-
|
|
12014
|
+
and10(
|
|
12015
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11745
12016
|
gte2(aiUserFetchEventsHourly.tsHour, headlineStart),
|
|
11746
12017
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
11747
12018
|
)
|
|
@@ -11781,16 +12052,16 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11781
12052
|
);
|
|
11782
12053
|
const topPathsRows = db.select({
|
|
11783
12054
|
path: crawlerEventsHourly.pathNormalized,
|
|
11784
|
-
hits:
|
|
11785
|
-
operators:
|
|
12055
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`,
|
|
12056
|
+
operators: sql7`COUNT(DISTINCT ${crawlerEventsHourly.operator})`
|
|
11786
12057
|
}).from(crawlerEventsHourly).where(
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
12058
|
+
and10(
|
|
12059
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
12060
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11790
12061
|
gte2(crawlerEventsHourly.tsHour, headlineStart),
|
|
11791
12062
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11792
12063
|
)
|
|
11793
|
-
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(
|
|
12064
|
+
).groupBy(crawlerEventsHourly.pathNormalized).orderBy(desc7(sql7`SUM(${crawlerEventsHourly.hits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
11794
12065
|
const topCrawledPaths = topPathsRows.map((r) => ({
|
|
11795
12066
|
path: r.path,
|
|
11796
12067
|
verifiedHits: Number(r.hits),
|
|
@@ -11798,16 +12069,16 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11798
12069
|
}));
|
|
11799
12070
|
const referralProductsRows = db.select({
|
|
11800
12071
|
product: aiReferralEventsHourly.product,
|
|
11801
|
-
arrivals:
|
|
11802
|
-
landingPaths:
|
|
12072
|
+
arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
12073
|
+
landingPaths: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.landingPathNormalized})`
|
|
11803
12074
|
}).from(aiReferralEventsHourly).where(
|
|
11804
|
-
|
|
11805
|
-
|
|
12075
|
+
and10(
|
|
12076
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11806
12077
|
nonSubresourceReferralPathCondition(),
|
|
11807
12078
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11808
12079
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11809
12080
|
)
|
|
11810
|
-
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(
|
|
12081
|
+
).groupBy(aiReferralEventsHourly.product).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).all();
|
|
11811
12082
|
const referralProducts = referralProductsRows.map((r) => ({
|
|
11812
12083
|
product: r.product,
|
|
11813
12084
|
arrivals: Number(r.arrivals),
|
|
@@ -11815,53 +12086,53 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11815
12086
|
}));
|
|
11816
12087
|
const topReferralRows = db.select({
|
|
11817
12088
|
path: aiReferralEventsHourly.landingPathNormalized,
|
|
11818
|
-
arrivals:
|
|
11819
|
-
products:
|
|
12089
|
+
arrivals: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`,
|
|
12090
|
+
products: sql7`COUNT(DISTINCT ${aiReferralEventsHourly.product})`
|
|
11820
12091
|
}).from(aiReferralEventsHourly).where(
|
|
11821
|
-
|
|
11822
|
-
|
|
12092
|
+
and10(
|
|
12093
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11823
12094
|
nonSubresourceReferralPathCondition(),
|
|
11824
12095
|
gte2(aiReferralEventsHourly.tsHour, headlineStart),
|
|
11825
12096
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11826
12097
|
)
|
|
11827
|
-
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(
|
|
12098
|
+
).groupBy(aiReferralEventsHourly.landingPathNormalized).orderBy(desc7(sql7`SUM(${aiReferralEventsHourly.sessionsOrHits})`)).limit(SERVER_ACTIVITY_TOP_PATHS_LIMIT).all();
|
|
11828
12099
|
const topReferralLandingPaths = topReferralRows.map((r) => ({
|
|
11829
12100
|
path: r.path,
|
|
11830
12101
|
arrivals: Number(r.arrivals),
|
|
11831
12102
|
distinctProducts: Number(r.products)
|
|
11832
12103
|
}));
|
|
11833
12104
|
const crawlerTrendRows = db.select({
|
|
11834
|
-
date:
|
|
11835
|
-
hits:
|
|
12105
|
+
date: sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`,
|
|
12106
|
+
hits: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
11836
12107
|
}).from(crawlerEventsHourly).where(
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
12108
|
+
and10(
|
|
12109
|
+
eq15(crawlerEventsHourly.projectId, projectId),
|
|
12110
|
+
eq15(crawlerEventsHourly.verificationStatus, VerificationStatuses.verified),
|
|
11840
12111
|
gte2(crawlerEventsHourly.tsHour, trendStart),
|
|
11841
12112
|
lte(crawlerEventsHourly.tsHour, headlineEnd)
|
|
11842
12113
|
)
|
|
11843
|
-
).groupBy(
|
|
12114
|
+
).groupBy(sql7`SUBSTR(${crawlerEventsHourly.tsHour}, 1, 10)`).all();
|
|
11844
12115
|
const referralTrendRows = db.select({
|
|
11845
|
-
date:
|
|
11846
|
-
hits:
|
|
12116
|
+
date: sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`,
|
|
12117
|
+
hits: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)`
|
|
11847
12118
|
}).from(aiReferralEventsHourly).where(
|
|
11848
|
-
|
|
11849
|
-
|
|
12119
|
+
and10(
|
|
12120
|
+
eq15(aiReferralEventsHourly.projectId, projectId),
|
|
11850
12121
|
nonSubresourceReferralPathCondition(),
|
|
11851
12122
|
gte2(aiReferralEventsHourly.tsHour, trendStart),
|
|
11852
12123
|
lte(aiReferralEventsHourly.tsHour, headlineEnd)
|
|
11853
12124
|
)
|
|
11854
|
-
).groupBy(
|
|
12125
|
+
).groupBy(sql7`SUBSTR(${aiReferralEventsHourly.tsHour}, 1, 10)`).all();
|
|
11855
12126
|
const userFetchTrendRows = db.select({
|
|
11856
|
-
date:
|
|
11857
|
-
hits:
|
|
12127
|
+
date: sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`,
|
|
12128
|
+
hits: sql7`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)`
|
|
11858
12129
|
}).from(aiUserFetchEventsHourly).where(
|
|
11859
|
-
|
|
11860
|
-
|
|
12130
|
+
and10(
|
|
12131
|
+
eq15(aiUserFetchEventsHourly.projectId, projectId),
|
|
11861
12132
|
gte2(aiUserFetchEventsHourly.tsHour, trendStart),
|
|
11862
12133
|
lte(aiUserFetchEventsHourly.tsHour, headlineEnd)
|
|
11863
12134
|
)
|
|
11864
|
-
).groupBy(
|
|
12135
|
+
).groupBy(sql7`SUBSTR(${aiUserFetchEventsHourly.tsHour}, 1, 10)`).all();
|
|
11865
12136
|
const emptyTrendEntry = () => ({ verifiedCrawlerHits: 0, userFetchHits: 0, referralArrivals: 0 });
|
|
11866
12137
|
const dailyTrendMap = /* @__PURE__ */ new Map();
|
|
11867
12138
|
for (const r of crawlerTrendRows) {
|
|
@@ -11912,7 +12183,7 @@ function buildServerActivity(db, projectId, windowDays) {
|
|
|
11912
12183
|
};
|
|
11913
12184
|
}
|
|
11914
12185
|
function buildIndexingHealth(db, projectId) {
|
|
11915
|
-
const gsc = db.select().from(gscCoverageSnapshots).where(
|
|
12186
|
+
const gsc = db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, projectId)).orderBy(desc7(gscCoverageSnapshots.date)).limit(1).get();
|
|
11916
12187
|
if (gsc) {
|
|
11917
12188
|
const total = gsc.indexed + gsc.notIndexed;
|
|
11918
12189
|
return {
|
|
@@ -11925,7 +12196,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
11925
12196
|
indexedPct: total > 0 ? Math.round(gsc.indexed / total * 100) : 0
|
|
11926
12197
|
};
|
|
11927
12198
|
}
|
|
11928
|
-
const bing = db.select().from(bingCoverageSnapshots).where(
|
|
12199
|
+
const bing = db.select().from(bingCoverageSnapshots).where(eq15(bingCoverageSnapshots.projectId, projectId)).orderBy(desc7(bingCoverageSnapshots.date)).limit(1).get();
|
|
11929
12200
|
if (bing) {
|
|
11930
12201
|
const total = bing.indexed + bing.notIndexed + bing.unknown;
|
|
11931
12202
|
return {
|
|
@@ -11941,7 +12212,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
11941
12212
|
return null;
|
|
11942
12213
|
}
|
|
11943
12214
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
11944
|
-
const visibilityRuns = db.select().from(runs).where(
|
|
12215
|
+
const visibilityRuns = db.select().from(runs).where(and10(eq15(runs.projectId, projectId), eq15(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
11945
12216
|
const totalQueries = queryLookup.byId.size;
|
|
11946
12217
|
const points = [];
|
|
11947
12218
|
for (const run of visibilityRuns) {
|
|
@@ -11989,15 +12260,15 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
11989
12260
|
}
|
|
11990
12261
|
function buildInsightList(db, projectId, locationFilter) {
|
|
11991
12262
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
or3(
|
|
12263
|
+
and10(
|
|
12264
|
+
eq15(runs.projectId, projectId),
|
|
12265
|
+
eq15(runs.kind, RunKinds["answer-visibility"]),
|
|
12266
|
+
or3(eq15(runs.status, RunStatuses.completed), eq15(runs.status, RunStatuses.partial)),
|
|
11996
12267
|
notProbeRun()
|
|
11997
12268
|
)
|
|
11998
12269
|
).orderBy(desc7(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
11999
12270
|
if (recentRunIds.length === 0) return [];
|
|
12000
|
-
const rows = db.select().from(insights).where(
|
|
12271
|
+
const rows = db.select().from(insights).where(and10(eq15(insights.projectId, projectId), inArray6(insights.runId, recentRunIds))).orderBy(desc7(insights.createdAt)).all();
|
|
12001
12272
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
12002
12273
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
12003
12274
|
const recommendation = r.recommendation;
|
|
@@ -12549,7 +12820,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
12549
12820
|
const project = resolveProject(db, projectName);
|
|
12550
12821
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
12551
12822
|
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
12552
|
-
const allRuns = db.select().from(runs).where(
|
|
12823
|
+
const allRuns = db.select().from(runs).where(and10(eq15(runs.projectId, project.id), notProbeRun())).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
12553
12824
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
12554
12825
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
12555
12826
|
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
@@ -12558,7 +12829,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
12558
12829
|
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
12559
12830
|
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
12560
12831
|
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
12561
|
-
const competitorRows = db.select().from(competitors).where(
|
|
12832
|
+
const competitorRows = db.select().from(competitors).where(eq15(competitors.projectId, project.id)).all();
|
|
12562
12833
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
12563
12834
|
const ownedDomains = project.ownedDomains;
|
|
12564
12835
|
const projectDomains = [project.canonicalDomain, ...ownedDomains];
|
|
@@ -12785,16 +13056,16 @@ async function reportRoutes(app) {
|
|
|
12785
13056
|
}
|
|
12786
13057
|
|
|
12787
13058
|
// ../api-routes/src/citations.ts
|
|
12788
|
-
import { and as
|
|
13059
|
+
import { and as and11, eq as eq16, inArray as inArray7 } from "drizzle-orm";
|
|
12789
13060
|
async function citationRoutes(app) {
|
|
12790
13061
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
12791
13062
|
const project = resolveProject(app.db, request.params.name);
|
|
12792
13063
|
const configuredProviders = project.providers;
|
|
12793
|
-
const projectQueries = app.db.select().from(queries).where(
|
|
13064
|
+
const projectQueries = app.db.select().from(queries).where(eq16(queries.projectId, project.id)).all();
|
|
12794
13065
|
if (projectQueries.length === 0) {
|
|
12795
13066
|
return reply.send(emptyCitationVisibility("no-queries"));
|
|
12796
13067
|
}
|
|
12797
|
-
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
13068
|
+
const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and11(eq16(runs.projectId, project.id), notProbeRun())).all();
|
|
12798
13069
|
if (projectRuns.length === 0) {
|
|
12799
13070
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
12800
13071
|
}
|
|
@@ -12818,7 +13089,7 @@ async function citationRoutes(app) {
|
|
|
12818
13089
|
queryId: s.queryId,
|
|
12819
13090
|
runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
|
|
12820
13091
|
}));
|
|
12821
|
-
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
13092
|
+
const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq16(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
|
|
12822
13093
|
const response = computeCitationVisibility({
|
|
12823
13094
|
queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
|
|
12824
13095
|
snapshots,
|
|
@@ -12950,7 +13221,7 @@ function normalizeDomain2(domain) {
|
|
|
12950
13221
|
}
|
|
12951
13222
|
|
|
12952
13223
|
// ../api-routes/src/visibility-stats.ts
|
|
12953
|
-
import { and as
|
|
13224
|
+
import { and as and12, desc as desc8, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
12954
13225
|
function round42(value) {
|
|
12955
13226
|
return Math.round(value * 1e4) / 1e4;
|
|
12956
13227
|
}
|
|
@@ -13070,8 +13341,8 @@ async function visibilityStatsRoutes(app) {
|
|
|
13070
13341
|
if (!Number.isInteger(n) || n <= 0) throw validationError('"lastRuns" must be a positive integer');
|
|
13071
13342
|
lastRuns = n;
|
|
13072
13343
|
}
|
|
13073
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
13074
|
-
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(
|
|
13344
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
13345
|
+
let projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and12(eq17(runs.projectId, project.id), eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc8(runs.createdAt)).all().filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial);
|
|
13075
13346
|
if (sinceMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) >= sinceMs);
|
|
13076
13347
|
if (untilMs !== null) projectRuns = projectRuns.filter((r) => Date.parse(r.createdAt) <= untilMs);
|
|
13077
13348
|
if (lastRuns !== null) projectRuns = projectRuns.slice(0, lastRuns);
|
|
@@ -13107,7 +13378,7 @@ async function visibilityStatsRoutes(app) {
|
|
|
13107
13378
|
}
|
|
13108
13379
|
|
|
13109
13380
|
// ../api-routes/src/composites.ts
|
|
13110
|
-
import { eq as
|
|
13381
|
+
import { eq as eq18, and as and13, desc as desc9, sql as sql8, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
|
|
13111
13382
|
var TOP_INSIGHT_LIMIT = 5;
|
|
13112
13383
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
13113
13384
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -13125,7 +13396,7 @@ async function compositeRoutes(app) {
|
|
|
13125
13396
|
const project = resolveProject(app.db, request.params.name);
|
|
13126
13397
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
13127
13398
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
13128
|
-
const allRunsRaw = app.db.select().from(runs).where(
|
|
13399
|
+
const allRunsRaw = app.db.select().from(runs).where(and13(eq18(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
|
|
13129
13400
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
13130
13401
|
const totalRuns = allRuns.length;
|
|
13131
13402
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
@@ -13138,15 +13409,15 @@ async function compositeRoutes(app) {
|
|
|
13138
13409
|
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
13139
13410
|
const latestRunRow = allRuns[0] ?? null;
|
|
13140
13411
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
13141
|
-
const healthRow = app.db.select().from(healthSnapshots).where(
|
|
13412
|
+
const healthRow = app.db.select().from(healthSnapshots).where(eq18(healthSnapshots.projectId, project.id)).orderBy(desc9(healthSnapshots.createdAt)).limit(1).get();
|
|
13142
13413
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
13143
|
-
const insightRows = app.db.select().from(insights).where(
|
|
13414
|
+
const insightRows = app.db.select().from(insights).where(eq18(insights.projectId, project.id)).orderBy(desc9(insights.createdAt)).all();
|
|
13144
13415
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
13145
13416
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
13146
13417
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
13147
13418
|
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
13148
13419
|
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
13149
|
-
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
13420
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq18(queries.projectId, project.id)).all();
|
|
13150
13421
|
const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
|
|
13151
13422
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
|
|
13152
13423
|
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
@@ -13162,7 +13433,7 @@ async function compositeRoutes(app) {
|
|
|
13162
13433
|
trackedPrevious,
|
|
13163
13434
|
previousVisibilityRun?.createdAt ?? null
|
|
13164
13435
|
);
|
|
13165
|
-
const competitorRows = app.db.select().from(competitors).where(
|
|
13436
|
+
const competitorRows = app.db.select().from(competitors).where(eq18(competitors.projectId, project.id)).all();
|
|
13166
13437
|
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
13167
13438
|
for (const snapshots of snapshotsByRun.values()) {
|
|
13168
13439
|
for (const snapshot of snapshots) {
|
|
@@ -13280,25 +13551,25 @@ async function compositeRoutes(app) {
|
|
|
13280
13551
|
citedDomains: querySnapshots.citedDomains,
|
|
13281
13552
|
rawResponse: querySnapshots.rawResponse,
|
|
13282
13553
|
createdAt: querySnapshots.createdAt
|
|
13283
|
-
}).from(querySnapshots).innerJoin(queries,
|
|
13284
|
-
|
|
13285
|
-
|
|
13554
|
+
}).from(querySnapshots).innerJoin(queries, eq18(querySnapshots.queryId, queries.id)).where(
|
|
13555
|
+
and13(
|
|
13556
|
+
eq18(queries.projectId, project.id),
|
|
13286
13557
|
or4(
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13558
|
+
sql8`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
13559
|
+
sql8`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
|
|
13560
|
+
sql8`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
|
|
13290
13561
|
like2(queries.query, pattern)
|
|
13291
13562
|
)
|
|
13292
13563
|
)
|
|
13293
13564
|
).orderBy(desc9(querySnapshots.createdAt)).limit(limit + 1).all());
|
|
13294
13565
|
const insightMatches = app.db.select().from(insights).where(
|
|
13295
|
-
|
|
13296
|
-
|
|
13566
|
+
and13(
|
|
13567
|
+
eq18(insights.projectId, project.id),
|
|
13297
13568
|
or4(
|
|
13298
13569
|
like2(insights.title, pattern),
|
|
13299
13570
|
like2(insights.query, pattern),
|
|
13300
|
-
|
|
13301
|
-
|
|
13571
|
+
sql8`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
|
|
13572
|
+
sql8`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
|
|
13302
13573
|
)
|
|
13303
13574
|
)
|
|
13304
13575
|
).orderBy(desc9(insights.createdAt)).limit(limit + 1).all();
|
|
@@ -13503,17 +13774,17 @@ function buildSuggestedQueriesFromGsc(app, projectId, trackedQueries) {
|
|
|
13503
13774
|
const cutoff = new Date(Date.now() - 28 * 24 * 60 * 60 * 1e3).toISOString().slice(0, 10);
|
|
13504
13775
|
const rows = app.db.select({
|
|
13505
13776
|
query: gscSearchData.query,
|
|
13506
|
-
impressions:
|
|
13507
|
-
clicks:
|
|
13777
|
+
impressions: sql8`COALESCE(SUM(${gscSearchData.impressions}), 0)`,
|
|
13778
|
+
clicks: sql8`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
13508
13779
|
// Weighted average: SUM(position * impressions) / SUM(impressions).
|
|
13509
13780
|
// NULLIF guards the degenerate impressions=0 case (SQLite returns NULL,
|
|
13510
13781
|
// which the JS coerces to 0 — caught by the impression floor anyway).
|
|
13511
|
-
avgPosition:
|
|
13512
|
-
}).from(gscSearchData).where(
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
)).groupBy(gscSearchData.query).orderBy(
|
|
13782
|
+
avgPosition: sql8`COALESCE(SUM(${gscSearchData.position} * ${gscSearchData.impressions}) * 1.0 / NULLIF(SUM(${gscSearchData.impressions}), 0), 0)`
|
|
13783
|
+
}).from(gscSearchData).where(and13(
|
|
13784
|
+
eq18(gscSearchData.projectId, projectId),
|
|
13785
|
+
sql8`${gscSearchData.date} >= ${cutoff}`,
|
|
13786
|
+
sql8`${gscSearchData.impressions} > 0`
|
|
13787
|
+
)).groupBy(gscSearchData.query).orderBy(sql8`SUM(${gscSearchData.impressions}) DESC`).limit(100).all();
|
|
13517
13788
|
const gscRows = rows.map((r) => ({
|
|
13518
13789
|
query: r.query,
|
|
13519
13790
|
impressions: Number(r.impressions),
|
|
@@ -13533,8 +13804,8 @@ function buildIndexCoverageScore(app, projectId) {
|
|
|
13533
13804
|
tooltip,
|
|
13534
13805
|
trend: []
|
|
13535
13806
|
};
|
|
13536
|
-
const gscRow = app.db.select().from(gscCoverageSnapshots).where(
|
|
13537
|
-
const bingRow = app.db.select().from(bingCoverageSnapshots).where(
|
|
13807
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq18(gscCoverageSnapshots.projectId, projectId)).orderBy(desc9(gscCoverageSnapshots.date)).limit(1).get();
|
|
13808
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, projectId)).orderBy(desc9(bingCoverageSnapshots.date)).limit(1).get();
|
|
13538
13809
|
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
13539
13810
|
if (!chosen) return empty;
|
|
13540
13811
|
const total = chosen.indexed + chosen.notIndexed;
|
|
@@ -13560,7 +13831,7 @@ function countGoogleDeindexedUrls(app, projectId) {
|
|
|
13560
13831
|
url: gscUrlInspections.url,
|
|
13561
13832
|
indexingState: gscUrlInspections.indexingState,
|
|
13562
13833
|
inspectedAt: gscUrlInspections.inspectedAt
|
|
13563
|
-
}).from(gscUrlInspections).where(
|
|
13834
|
+
}).from(gscUrlInspections).where(eq18(gscUrlInspections.projectId, projectId)).orderBy(desc9(gscUrlInspections.inspectedAt)).all();
|
|
13564
13835
|
if (rows.length === 0) return 0;
|
|
13565
13836
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
13566
13837
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -17992,7 +18263,7 @@ var canonryLocalRouteCatalog = [
|
|
|
17992
18263
|
method: "post",
|
|
17993
18264
|
path: "/api/v1/projects/{name}/agent/prompt",
|
|
17994
18265
|
summary: "Send a prompt to Aero and stream events back as SSE",
|
|
17995
|
-
description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). The CLI passes "all" to keep write tools available.',
|
|
18266
|
+
description: 'Posts a prompt into the project\'s Aero session and streams `AgentEvent` frames as `text/event-stream`. Each frame is `data: <JSON>\\n\\n`. The server brackets the stream with `{"type":"stream_open"}` and `{"type":"stream_close"}` control frames; `{"type":"error","message":"..."}` surfaces in-stream failures without collapsing the stream. Returns 409 `AGENT_BUSY` if another turn is already in flight for this project. Body field `scope` accepts "all" | "read-only"; omitted defaults to "read-only" (safe dashboard surface). Body field `profile` accepts "default" | "ads-operator"; omitted keeps the default full Canonry operator surface. The CLI passes "all" to keep write tools available.',
|
|
17996
18267
|
tags: ["agent"],
|
|
17997
18268
|
parameters: [nameParameter],
|
|
17998
18269
|
requestBody: {
|
|
@@ -18017,6 +18288,11 @@ var canonryLocalRouteCatalog = [
|
|
|
18017
18288
|
type: "string",
|
|
18018
18289
|
enum: ["all", "read-only"],
|
|
18019
18290
|
description: 'Tool surface scope. Default "read-only". Set "all" to enable write tools.'
|
|
18291
|
+
},
|
|
18292
|
+
profile: {
|
|
18293
|
+
type: "string",
|
|
18294
|
+
enum: ["default", "ads-operator"],
|
|
18295
|
+
description: 'Tool profile. Default "default". Set "ads-operator" to use the narrower ads SaaS operator surface plus the ads context tool.'
|
|
18020
18296
|
}
|
|
18021
18297
|
}
|
|
18022
18298
|
}
|
|
@@ -18202,7 +18478,7 @@ async function settingsRoutes(app, opts) {
|
|
|
18202
18478
|
|
|
18203
18479
|
// ../api-routes/src/keys.ts
|
|
18204
18480
|
import crypto12 from "crypto";
|
|
18205
|
-
import { desc as desc10, eq as
|
|
18481
|
+
import { desc as desc10, eq as eq19 } from "drizzle-orm";
|
|
18206
18482
|
var KEYS_WRITE_SCOPE = "keys.write";
|
|
18207
18483
|
function toApiKeyDto(row) {
|
|
18208
18484
|
const scopes = Array.isArray(row.scopes) ? row.scopes : [];
|
|
@@ -18211,6 +18487,7 @@ function toApiKeyDto(row) {
|
|
|
18211
18487
|
name: row.name,
|
|
18212
18488
|
keyPrefix: row.keyPrefix,
|
|
18213
18489
|
scopes,
|
|
18490
|
+
projectId: row.projectId ?? null,
|
|
18214
18491
|
readOnly: isReadOnlyKey(scopes),
|
|
18215
18492
|
createdAt: row.createdAt,
|
|
18216
18493
|
lastUsedAt: row.lastUsedAt ?? null,
|
|
@@ -18218,8 +18495,9 @@ function toApiKeyDto(row) {
|
|
|
18218
18495
|
};
|
|
18219
18496
|
}
|
|
18220
18497
|
async function keysRoutes(app) {
|
|
18221
|
-
app.get("/keys", async () => {
|
|
18222
|
-
const
|
|
18498
|
+
app.get("/keys", async (request) => {
|
|
18499
|
+
const scopedProjectId = request.apiKey?.projectId;
|
|
18500
|
+
const rows = scopedProjectId ? app.db.select().from(apiKeys).where(eq19(apiKeys.projectId, scopedProjectId)).orderBy(desc10(apiKeys.createdAt)).all() : app.db.select().from(apiKeys).orderBy(desc10(apiKeys.createdAt)).all();
|
|
18223
18501
|
return { keys: rows.map(toApiKeyDto) };
|
|
18224
18502
|
});
|
|
18225
18503
|
app.get("/keys/self", async (request) => {
|
|
@@ -18227,7 +18505,7 @@ async function keysRoutes(app) {
|
|
|
18227
18505
|
if (!id) {
|
|
18228
18506
|
throw notFound("API key", "self");
|
|
18229
18507
|
}
|
|
18230
|
-
const row = app.db.select().from(apiKeys).where(
|
|
18508
|
+
const row = app.db.select().from(apiKeys).where(eq19(apiKeys.id, id)).get();
|
|
18231
18509
|
if (!row) {
|
|
18232
18510
|
throw notFound("API key", id);
|
|
18233
18511
|
}
|
|
@@ -18239,7 +18517,17 @@ async function keysRoutes(app) {
|
|
|
18239
18517
|
if (!parsed.success) {
|
|
18240
18518
|
throw validationError("Invalid API key request", { issues: parsed.error.issues });
|
|
18241
18519
|
}
|
|
18242
|
-
const { name, scopes } = parsed.data;
|
|
18520
|
+
const { name, scopes, projectId } = parsed.data;
|
|
18521
|
+
const requesterProjectId = request.apiKey?.projectId;
|
|
18522
|
+
if (requesterProjectId && projectId !== requesterProjectId) {
|
|
18523
|
+
throw forbidden("A project-scoped key can only mint keys scoped to its own project.");
|
|
18524
|
+
}
|
|
18525
|
+
if (projectId) {
|
|
18526
|
+
const proj = app.db.select({ id: projects.id }).from(projects).where(eq19(projects.id, projectId)).get();
|
|
18527
|
+
if (!proj) {
|
|
18528
|
+
throw notFound("Project", projectId);
|
|
18529
|
+
}
|
|
18530
|
+
}
|
|
18243
18531
|
const raw = `cnry_${crypto12.randomBytes(16).toString("hex")}`;
|
|
18244
18532
|
const keyHash = hashApiKey(raw);
|
|
18245
18533
|
const keyPrefix = raw.slice(0, 9);
|
|
@@ -18253,6 +18541,7 @@ async function keysRoutes(app) {
|
|
|
18253
18541
|
keyHash,
|
|
18254
18542
|
keyPrefix,
|
|
18255
18543
|
scopes: effectiveScopes,
|
|
18544
|
+
projectId: projectId ?? null,
|
|
18256
18545
|
createdAt: now
|
|
18257
18546
|
}).run();
|
|
18258
18547
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
@@ -18260,7 +18549,7 @@ async function keysRoutes(app) {
|
|
|
18260
18549
|
action: "api-key.created",
|
|
18261
18550
|
entityType: "api-key",
|
|
18262
18551
|
entityId: id,
|
|
18263
|
-
diff: { name, keyPrefix, scopes: effectiveScopes }
|
|
18552
|
+
diff: { name, keyPrefix, scopes: effectiveScopes, projectId: projectId ?? null }
|
|
18264
18553
|
}));
|
|
18265
18554
|
});
|
|
18266
18555
|
const dto = {
|
|
@@ -18268,6 +18557,7 @@ async function keysRoutes(app) {
|
|
|
18268
18557
|
name,
|
|
18269
18558
|
keyPrefix,
|
|
18270
18559
|
scopes: effectiveScopes,
|
|
18560
|
+
projectId: projectId ?? null,
|
|
18271
18561
|
readOnly: isReadOnlyKey(effectiveScopes),
|
|
18272
18562
|
createdAt: now,
|
|
18273
18563
|
lastUsedAt: null,
|
|
@@ -18279,19 +18569,23 @@ async function keysRoutes(app) {
|
|
|
18279
18569
|
app.post("/keys/:id/revoke", async (request) => {
|
|
18280
18570
|
requireScope(request, KEYS_WRITE_SCOPE);
|
|
18281
18571
|
const { id } = request.params;
|
|
18282
|
-
const row = app.db.select().from(apiKeys).where(
|
|
18572
|
+
const row = app.db.select().from(apiKeys).where(eq19(apiKeys.id, id)).get();
|
|
18283
18573
|
if (!row) {
|
|
18284
18574
|
throw notFound("API key", id);
|
|
18285
18575
|
}
|
|
18286
18576
|
if (request.apiKey?.id === id) {
|
|
18287
18577
|
throw validationError("Cannot revoke the API key you are currently authenticating with");
|
|
18288
18578
|
}
|
|
18579
|
+
const requesterProjectId = request.apiKey?.projectId;
|
|
18580
|
+
if (requesterProjectId && row.projectId !== requesterProjectId) {
|
|
18581
|
+
throw forbidden("A project-scoped key can only revoke keys scoped to its own project.");
|
|
18582
|
+
}
|
|
18289
18583
|
if (row.revokedAt) {
|
|
18290
18584
|
return toApiKeyDto(row);
|
|
18291
18585
|
}
|
|
18292
18586
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18293
18587
|
app.db.transaction((tx) => {
|
|
18294
|
-
tx.update(apiKeys).set({ revokedAt: now }).where(
|
|
18588
|
+
tx.update(apiKeys).set({ revokedAt: now }).where(eq19(apiKeys.id, id)).run();
|
|
18295
18589
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
18296
18590
|
actor: "api",
|
|
18297
18591
|
action: "api-key.revoked",
|
|
@@ -18364,7 +18658,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
18364
18658
|
|
|
18365
18659
|
// ../api-routes/src/schedules.ts
|
|
18366
18660
|
import crypto13 from "crypto";
|
|
18367
|
-
import { and as
|
|
18661
|
+
import { and as and14, eq as eq20 } from "drizzle-orm";
|
|
18368
18662
|
function parseKindParam(raw) {
|
|
18369
18663
|
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
18370
18664
|
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
@@ -18391,7 +18685,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18391
18685
|
if (!sourceId) {
|
|
18392
18686
|
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
18393
18687
|
}
|
|
18394
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
18688
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq20(trafficSources.id, sourceId)).get();
|
|
18395
18689
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
18396
18690
|
throw notFound("Traffic source", sourceId);
|
|
18397
18691
|
}
|
|
@@ -18436,7 +18730,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18436
18730
|
}
|
|
18437
18731
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18438
18732
|
const enabledBool = enabled !== false;
|
|
18439
|
-
const existing = app.db.select().from(schedules).where(
|
|
18733
|
+
const existing = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18440
18734
|
if (existing) {
|
|
18441
18735
|
app.db.update(schedules).set({
|
|
18442
18736
|
cronExpr,
|
|
@@ -18446,7 +18740,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
18446
18740
|
sourceId: sourceId ?? null,
|
|
18447
18741
|
enabled: enabledBool,
|
|
18448
18742
|
updatedAt: now
|
|
18449
|
-
}).where(
|
|
18743
|
+
}).where(eq20(schedules.id, existing.id)).run();
|
|
18450
18744
|
} else {
|
|
18451
18745
|
app.db.insert(schedules).values({
|
|
18452
18746
|
id: crypto13.randomUUID(),
|
|
@@ -18470,13 +18764,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
18470
18764
|
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
18471
18765
|
});
|
|
18472
18766
|
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
18473
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18767
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18474
18768
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
18475
18769
|
});
|
|
18476
18770
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
18477
18771
|
const project = resolveProject(app.db, request.params.name);
|
|
18478
18772
|
const kind = parseKindParam(request.query?.kind);
|
|
18479
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18773
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18480
18774
|
if (!schedule) {
|
|
18481
18775
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
18482
18776
|
}
|
|
@@ -18485,11 +18779,11 @@ async function scheduleRoutes(app, opts) {
|
|
|
18485
18779
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
18486
18780
|
const project = resolveProject(app.db, request.params.name);
|
|
18487
18781
|
const kind = parseKindParam(request.query?.kind);
|
|
18488
|
-
const schedule = app.db.select().from(schedules).where(
|
|
18782
|
+
const schedule = app.db.select().from(schedules).where(and14(eq20(schedules.projectId, project.id), eq20(schedules.kind, kind))).get();
|
|
18489
18783
|
if (!schedule) {
|
|
18490
18784
|
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
18491
18785
|
}
|
|
18492
|
-
app.db.delete(schedules).where(
|
|
18786
|
+
app.db.delete(schedules).where(eq20(schedules.id, schedule.id)).run();
|
|
18493
18787
|
writeAuditLog(app.db, {
|
|
18494
18788
|
projectId: project.id,
|
|
18495
18789
|
actor: "api",
|
|
@@ -18522,7 +18816,7 @@ function formatSchedule(row) {
|
|
|
18522
18816
|
|
|
18523
18817
|
// ../api-routes/src/notifications.ts
|
|
18524
18818
|
import crypto14 from "crypto";
|
|
18525
|
-
import { eq as
|
|
18819
|
+
import { eq as eq21 } from "drizzle-orm";
|
|
18526
18820
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
|
|
18527
18821
|
async function notificationRoutes(app, opts = {}) {
|
|
18528
18822
|
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
@@ -18562,22 +18856,22 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18562
18856
|
diff: { channel, ...redactNotificationUrl(url), events }
|
|
18563
18857
|
});
|
|
18564
18858
|
return reply.status(201).send({
|
|
18565
|
-
...formatNotification(app.db.select().from(notifications).where(
|
|
18859
|
+
...formatNotification(app.db.select().from(notifications).where(eq21(notifications.id, id)).get()),
|
|
18566
18860
|
webhookSecret
|
|
18567
18861
|
});
|
|
18568
18862
|
});
|
|
18569
18863
|
app.get("/projects/:name/notifications", async (request, reply) => {
|
|
18570
18864
|
const project = resolveProject(app.db, request.params.name);
|
|
18571
|
-
const rows = app.db.select().from(notifications).where(
|
|
18865
|
+
const rows = app.db.select().from(notifications).where(eq21(notifications.projectId, project.id)).all();
|
|
18572
18866
|
return reply.send(rows.map(formatNotification));
|
|
18573
18867
|
});
|
|
18574
18868
|
app.delete("/projects/:name/notifications/:id", async (request, reply) => {
|
|
18575
18869
|
const project = resolveProject(app.db, request.params.name);
|
|
18576
|
-
const notification = app.db.select().from(notifications).where(
|
|
18870
|
+
const notification = app.db.select().from(notifications).where(eq21(notifications.id, request.params.id)).get();
|
|
18577
18871
|
if (!notification || notification.projectId !== project.id) {
|
|
18578
18872
|
throw notFound("Notification", request.params.id);
|
|
18579
18873
|
}
|
|
18580
|
-
app.db.delete(notifications).where(
|
|
18874
|
+
app.db.delete(notifications).where(eq21(notifications.id, notification.id)).run();
|
|
18581
18875
|
writeAuditLog(app.db, {
|
|
18582
18876
|
projectId: project.id,
|
|
18583
18877
|
actor: "api",
|
|
@@ -18589,7 +18883,7 @@ async function notificationRoutes(app, opts = {}) {
|
|
|
18589
18883
|
});
|
|
18590
18884
|
app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
|
|
18591
18885
|
const project = resolveProject(app.db, request.params.name);
|
|
18592
|
-
const notification = app.db.select().from(notifications).where(
|
|
18886
|
+
const notification = app.db.select().from(notifications).where(eq21(notifications.id, request.params.id)).get();
|
|
18593
18887
|
if (!notification || notification.projectId !== project.id) {
|
|
18594
18888
|
throw notFound("Notification", request.params.id);
|
|
18595
18889
|
}
|
|
@@ -18642,7 +18936,7 @@ function formatNotification(row) {
|
|
|
18642
18936
|
|
|
18643
18937
|
// ../api-routes/src/google.ts
|
|
18644
18938
|
import crypto18 from "crypto";
|
|
18645
|
-
import { eq as
|
|
18939
|
+
import { eq as eq22, and as and15, desc as desc11, sql as sql9, inArray as inArray10 } from "drizzle-orm";
|
|
18646
18940
|
|
|
18647
18941
|
// ../api-routes/src/gbp-summary.ts
|
|
18648
18942
|
function computeMetricTotals(rows) {
|
|
@@ -20475,7 +20769,7 @@ async function googleRoutes(app, opts) {
|
|
|
20475
20769
|
if (!projectId) {
|
|
20476
20770
|
return reply.status(400).send("Stale OAuth state \u2014 restart the connect flow.");
|
|
20477
20771
|
}
|
|
20478
|
-
const project = app.db.select().from(projects).where(
|
|
20772
|
+
const project = app.db.select().from(projects).where(eq22(projects.id, projectId)).get();
|
|
20479
20773
|
if (!project) {
|
|
20480
20774
|
return reply.status(400).send("Project no longer exists. Restart the connect flow.");
|
|
20481
20775
|
}
|
|
@@ -20607,22 +20901,22 @@ async function googleRoutes(app, opts) {
|
|
|
20607
20901
|
if (opts.onGscSyncRequested) {
|
|
20608
20902
|
opts.onGscSyncRequested(runId, project.id, { days, full });
|
|
20609
20903
|
}
|
|
20610
|
-
const run = app.db.select().from(runs).where(
|
|
20904
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20611
20905
|
return run;
|
|
20612
20906
|
});
|
|
20613
20907
|
app.get("/projects/:name/google/gsc/performance", async (request) => {
|
|
20614
20908
|
const project = resolveProject(app.db, request.params.name);
|
|
20615
20909
|
const { startDate, endDate, query, page, limit, offset } = request.query;
|
|
20616
20910
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
20617
|
-
const conditions = [
|
|
20618
|
-
if (startDate) conditions.push(
|
|
20619
|
-
else if (cutoffDate) conditions.push(
|
|
20620
|
-
if (endDate) conditions.push(
|
|
20621
|
-
if (query) conditions.push(
|
|
20622
|
-
if (page) conditions.push(
|
|
20911
|
+
const conditions = [eq22(gscSearchData.projectId, project.id)];
|
|
20912
|
+
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
20913
|
+
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20914
|
+
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
20915
|
+
if (query) conditions.push(sql9`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
|
|
20916
|
+
if (page) conditions.push(sql9`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
20623
20917
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
20624
20918
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
20625
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
20919
|
+
const rows = app.db.select().from(gscSearchData).where(and15(...conditions)).orderBy(desc11(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
20626
20920
|
return rows.map((r) => ({
|
|
20627
20921
|
date: r.date,
|
|
20628
20922
|
query: r.query,
|
|
@@ -20639,20 +20933,31 @@ async function googleRoutes(app, opts) {
|
|
|
20639
20933
|
const project = resolveProject(app.db, request.params.name);
|
|
20640
20934
|
const { startDate, endDate } = request.query;
|
|
20641
20935
|
const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
|
|
20642
|
-
const
|
|
20643
|
-
|
|
20644
|
-
|
|
20645
|
-
|
|
20646
|
-
|
|
20936
|
+
const windowStart = startDate ?? cutoffDate ?? "";
|
|
20937
|
+
const windowEnd = endDate ?? "9999-12-31";
|
|
20938
|
+
const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
|
|
20939
|
+
const conditions = [eq22(gscSearchData.projectId, project.id)];
|
|
20940
|
+
if (startDate) conditions.push(sql9`${gscSearchData.date} >= ${startDate}`);
|
|
20941
|
+
else if (cutoffDate) conditions.push(sql9`${gscSearchData.date} >= ${cutoffDate}`);
|
|
20942
|
+
if (endDate) conditions.push(sql9`${gscSearchData.date} <= ${endDate}`);
|
|
20943
|
+
const dimensionedRows = app.db.select({
|
|
20647
20944
|
date: gscSearchData.date,
|
|
20648
|
-
clicks:
|
|
20649
|
-
impressions:
|
|
20650
|
-
}).from(gscSearchData).where(
|
|
20651
|
-
const daily =
|
|
20652
|
-
|
|
20653
|
-
|
|
20654
|
-
|
|
20655
|
-
|
|
20945
|
+
clicks: sql9`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
20946
|
+
impressions: sql9`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
20947
|
+
}).from(gscSearchData).where(and15(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
20948
|
+
const daily = mergeGscDailyTotalsWithFallback(
|
|
20949
|
+
dailyTotals,
|
|
20950
|
+
dimensionedRows.map((r) => ({
|
|
20951
|
+
date: r.date,
|
|
20952
|
+
clicks: r.clicks,
|
|
20953
|
+
impressions: r.impressions,
|
|
20954
|
+
position: 0
|
|
20955
|
+
}))
|
|
20956
|
+
).map((d) => ({
|
|
20957
|
+
date: d.date,
|
|
20958
|
+
clicks: d.clicks,
|
|
20959
|
+
impressions: d.impressions,
|
|
20960
|
+
ctr: d.impressions > 0 ? d.clicks / d.impressions : 0
|
|
20656
20961
|
}));
|
|
20657
20962
|
const totalClicks = daily.reduce((sum, d) => sum + d.clicks, 0);
|
|
20658
20963
|
const totalImpressions = daily.reduce((sum, d) => sum + d.impressions, 0);
|
|
@@ -20730,9 +21035,9 @@ async function googleRoutes(app, opts) {
|
|
|
20730
21035
|
app.get("/projects/:name/google/gsc/inspections", async (request) => {
|
|
20731
21036
|
const project = resolveProject(app.db, request.params.name);
|
|
20732
21037
|
const { url, limit } = request.query;
|
|
20733
|
-
const conditions = [
|
|
20734
|
-
if (url) conditions.push(
|
|
20735
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
21038
|
+
const conditions = [eq22(gscUrlInspections.projectId, project.id)];
|
|
21039
|
+
if (url) conditions.push(eq22(gscUrlInspections.url, url));
|
|
21040
|
+
const rows = app.db.select().from(gscUrlInspections).where(and15(...conditions)).orderBy(desc11(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
20736
21041
|
return rows.map((r) => ({
|
|
20737
21042
|
id: r.id,
|
|
20738
21043
|
url: r.url,
|
|
@@ -20751,7 +21056,7 @@ async function googleRoutes(app, opts) {
|
|
|
20751
21056
|
});
|
|
20752
21057
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
20753
21058
|
const project = resolveProject(app.db, request.params.name);
|
|
20754
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21059
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20755
21060
|
const byUrl = /* @__PURE__ */ new Map();
|
|
20756
21061
|
for (const row of allInspections) {
|
|
20757
21062
|
const existing = byUrl.get(row.url);
|
|
@@ -20779,7 +21084,7 @@ async function googleRoutes(app, opts) {
|
|
|
20779
21084
|
});
|
|
20780
21085
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
20781
21086
|
const project = resolveProject(app.db, request.params.name);
|
|
20782
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21087
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
20783
21088
|
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
20784
21089
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
20785
21090
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
@@ -20828,7 +21133,7 @@ async function googleRoutes(app, opts) {
|
|
|
20828
21133
|
const total = latestByUrl.size;
|
|
20829
21134
|
const indexed = indexedUrls.length;
|
|
20830
21135
|
const notIndexed = notIndexedUrls.length;
|
|
20831
|
-
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(
|
|
21136
|
+
const latestSnapshot = app.db.select({ createdAt: gscCoverageSnapshots.createdAt }).from(gscCoverageSnapshots).where(eq22(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.createdAt)).limit(1).get();
|
|
20832
21137
|
const lastSyncedAt = latestSnapshot?.createdAt ?? null;
|
|
20833
21138
|
const formatRow = (r) => ({
|
|
20834
21139
|
id: r.id,
|
|
@@ -20879,7 +21184,7 @@ async function googleRoutes(app, opts) {
|
|
|
20879
21184
|
const project = resolveProject(app.db, request.params.name);
|
|
20880
21185
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
20881
21186
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
20882
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(
|
|
21187
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq22(gscCoverageSnapshots.projectId, project.id)).orderBy(desc11(gscCoverageSnapshots.date)).limit(limit).all();
|
|
20883
21188
|
return rows.map((r) => ({
|
|
20884
21189
|
date: r.date,
|
|
20885
21190
|
indexed: r.indexed,
|
|
@@ -20948,7 +21253,7 @@ async function googleRoutes(app, opts) {
|
|
|
20948
21253
|
if (opts.onInspectSitemapRequested) {
|
|
20949
21254
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
20950
21255
|
}
|
|
20951
|
-
const run = app.db.select().from(runs).where(
|
|
21256
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20952
21257
|
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
20953
21258
|
});
|
|
20954
21259
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
|
|
@@ -20975,7 +21280,7 @@ async function googleRoutes(app, opts) {
|
|
|
20975
21280
|
if (opts.onInspectSitemapRequested) {
|
|
20976
21281
|
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
|
|
20977
21282
|
}
|
|
20978
|
-
const run = app.db.select().from(runs).where(
|
|
21283
|
+
const run = app.db.select().from(runs).where(eq22(runs.id, runId)).get();
|
|
20979
21284
|
return run;
|
|
20980
21285
|
});
|
|
20981
21286
|
app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
|
|
@@ -21022,7 +21327,7 @@ async function googleRoutes(app, opts) {
|
|
|
21022
21327
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
21023
21328
|
let urlsToNotify = request.body?.urls ?? [];
|
|
21024
21329
|
if (request.body?.allUnindexed) {
|
|
21025
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(
|
|
21330
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, project.id)).orderBy(desc11(gscUrlInspections.inspectedAt)).all();
|
|
21026
21331
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21027
21332
|
for (const row of allInspections) {
|
|
21028
21333
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -21140,7 +21445,7 @@ async function googleRoutes(app, opts) {
|
|
|
21140
21445
|
};
|
|
21141
21446
|
}
|
|
21142
21447
|
function listSelectionResponse(projectId) {
|
|
21143
|
-
const rows = app.db.select().from(gbpLocations).where(
|
|
21448
|
+
const rows = app.db.select().from(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).all();
|
|
21144
21449
|
const dtos = rows.map(rowToDto2);
|
|
21145
21450
|
return {
|
|
21146
21451
|
locations: dtos,
|
|
@@ -21149,15 +21454,15 @@ async function googleRoutes(app, opts) {
|
|
|
21149
21454
|
};
|
|
21150
21455
|
}
|
|
21151
21456
|
function clearGbpProjectData(tx, projectId) {
|
|
21152
|
-
tx.delete(gbpDailyMetrics).where(
|
|
21153
|
-
tx.delete(gbpKeywordImpressions).where(
|
|
21154
|
-
tx.delete(gbpKeywordMonthly).where(
|
|
21155
|
-
tx.delete(gbpPlaceActions).where(
|
|
21156
|
-
tx.delete(gbpLodgingSnapshots).where(
|
|
21157
|
-
tx.delete(gbpLocations).where(
|
|
21457
|
+
tx.delete(gbpDailyMetrics).where(eq22(gbpDailyMetrics.projectId, projectId)).run();
|
|
21458
|
+
tx.delete(gbpKeywordImpressions).where(eq22(gbpKeywordImpressions.projectId, projectId)).run();
|
|
21459
|
+
tx.delete(gbpKeywordMonthly).where(eq22(gbpKeywordMonthly.projectId, projectId)).run();
|
|
21460
|
+
tx.delete(gbpPlaceActions).where(eq22(gbpPlaceActions.projectId, projectId)).run();
|
|
21461
|
+
tx.delete(gbpLodgingSnapshots).where(eq22(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
21462
|
+
tx.delete(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).run();
|
|
21158
21463
|
}
|
|
21159
21464
|
function currentProjectAccount(projectId) {
|
|
21160
|
-
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(
|
|
21465
|
+
const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq22(gbpLocations.projectId, projectId)).limit(1).get();
|
|
21161
21466
|
return row?.accountName ?? null;
|
|
21162
21467
|
}
|
|
21163
21468
|
app.post("/projects/:name/gbp/locations/discover", async (request) => {
|
|
@@ -21227,7 +21532,7 @@ async function googleRoutes(app, opts) {
|
|
|
21227
21532
|
app.db.transaction((tx) => {
|
|
21228
21533
|
if (switching) clearGbpProjectData(tx, project.id);
|
|
21229
21534
|
for (const remote of remoteLocations) {
|
|
21230
|
-
const existing = tx.select().from(gbpLocations).where(
|
|
21535
|
+
const existing = tx.select().from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, remote.name))).get();
|
|
21231
21536
|
const profile = buildLocationProfileFields(remote);
|
|
21232
21537
|
if (existing) {
|
|
21233
21538
|
tx.update(gbpLocations).set({
|
|
@@ -21240,7 +21545,7 @@ async function googleRoutes(app, opts) {
|
|
|
21240
21545
|
mapsUri: remote.metadata?.mapsUri ?? null,
|
|
21241
21546
|
...profile,
|
|
21242
21547
|
updatedAt: now
|
|
21243
|
-
}).where(
|
|
21548
|
+
}).where(eq22(gbpLocations.id, existing.id)).run();
|
|
21244
21549
|
} else {
|
|
21245
21550
|
tx.insert(gbpLocations).values({
|
|
21246
21551
|
id: crypto18.randomUUID(),
|
|
@@ -21324,11 +21629,11 @@ async function googleRoutes(app, opts) {
|
|
|
21324
21629
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
|
|
21325
21630
|
}
|
|
21326
21631
|
const { selected } = parsed.data;
|
|
21327
|
-
const existing = app.db.select().from(gbpLocations).where(
|
|
21632
|
+
const existing = app.db.select().from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.locationName, locationName))).get();
|
|
21328
21633
|
if (!existing) throw notFound("GBP location", locationName);
|
|
21329
21634
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21330
21635
|
app.db.transaction((tx) => {
|
|
21331
|
-
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(
|
|
21636
|
+
tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq22(gbpLocations.id, existing.id)).run();
|
|
21332
21637
|
writeAuditLog(tx, {
|
|
21333
21638
|
projectId: project.id,
|
|
21334
21639
|
actor: "api",
|
|
@@ -21337,7 +21642,7 @@ async function googleRoutes(app, opts) {
|
|
|
21337
21642
|
entityId: locationName
|
|
21338
21643
|
});
|
|
21339
21644
|
});
|
|
21340
|
-
const refreshed = app.db.select().from(gbpLocations).where(
|
|
21645
|
+
const refreshed = app.db.select().from(gbpLocations).where(eq22(gbpLocations.id, existing.id)).get();
|
|
21341
21646
|
return rowToDto2(refreshed);
|
|
21342
21647
|
});
|
|
21343
21648
|
app.delete("/projects/:name/gbp/connection", async (request, reply) => {
|
|
@@ -21381,10 +21686,10 @@ async function googleRoutes(app, opts) {
|
|
|
21381
21686
|
});
|
|
21382
21687
|
app.get("/projects/:name/gbp/metrics", async (request) => {
|
|
21383
21688
|
const project = resolveProject(app.db, request.params.name);
|
|
21384
|
-
const conditions = [
|
|
21385
|
-
if (request.query.locationName) conditions.push(
|
|
21386
|
-
if (request.query.metric) conditions.push(
|
|
21387
|
-
const rows = app.db.select().from(gbpDailyMetrics).where(
|
|
21689
|
+
const conditions = [eq22(gbpDailyMetrics.projectId, project.id)];
|
|
21690
|
+
if (request.query.locationName) conditions.push(eq22(gbpDailyMetrics.locationName, request.query.locationName));
|
|
21691
|
+
if (request.query.metric) conditions.push(eq22(gbpDailyMetrics.metric, request.query.metric));
|
|
21692
|
+
const rows = app.db.select().from(gbpDailyMetrics).where(and15(...conditions)).orderBy(desc11(gbpDailyMetrics.date)).all();
|
|
21388
21693
|
return {
|
|
21389
21694
|
metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
|
|
21390
21695
|
total: rows.length
|
|
@@ -21392,9 +21697,9 @@ async function googleRoutes(app, opts) {
|
|
|
21392
21697
|
});
|
|
21393
21698
|
app.get("/projects/:name/gbp/keywords", async (request) => {
|
|
21394
21699
|
const project = resolveProject(app.db, request.params.name);
|
|
21395
|
-
const conditions = [
|
|
21396
|
-
if (request.query.locationName) conditions.push(
|
|
21397
|
-
const rows = app.db.select().from(gbpKeywordImpressions).where(
|
|
21700
|
+
const conditions = [eq22(gbpKeywordImpressions.projectId, project.id)];
|
|
21701
|
+
if (request.query.locationName) conditions.push(eq22(gbpKeywordImpressions.locationName, request.query.locationName));
|
|
21702
|
+
const rows = app.db.select().from(gbpKeywordImpressions).where(and15(...conditions)).all();
|
|
21398
21703
|
rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
|
|
21399
21704
|
const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
|
|
21400
21705
|
return {
|
|
@@ -21412,9 +21717,9 @@ async function googleRoutes(app, opts) {
|
|
|
21412
21717
|
});
|
|
21413
21718
|
app.get("/projects/:name/gbp/place-actions", async (request) => {
|
|
21414
21719
|
const project = resolveProject(app.db, request.params.name);
|
|
21415
|
-
const conditions = [
|
|
21416
|
-
if (request.query.locationName) conditions.push(
|
|
21417
|
-
const rows = app.db.select().from(gbpPlaceActions).where(
|
|
21720
|
+
const conditions = [eq22(gbpPlaceActions.projectId, project.id)];
|
|
21721
|
+
if (request.query.locationName) conditions.push(eq22(gbpPlaceActions.locationName, request.query.locationName));
|
|
21722
|
+
const rows = app.db.select().from(gbpPlaceActions).where(and15(...conditions)).all();
|
|
21418
21723
|
return {
|
|
21419
21724
|
placeActions: rows.map((r) => ({
|
|
21420
21725
|
locationName: r.locationName,
|
|
@@ -21429,9 +21734,9 @@ async function googleRoutes(app, opts) {
|
|
|
21429
21734
|
});
|
|
21430
21735
|
app.get("/projects/:name/gbp/lodging", async (request) => {
|
|
21431
21736
|
const project = resolveProject(app.db, request.params.name);
|
|
21432
|
-
const conditions = [
|
|
21433
|
-
if (request.query.locationName) conditions.push(
|
|
21434
|
-
const rows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21737
|
+
const conditions = [eq22(gbpLodgingSnapshots.projectId, project.id)];
|
|
21738
|
+
if (request.query.locationName) conditions.push(eq22(gbpLodgingSnapshots.locationName, request.query.locationName));
|
|
21739
|
+
const rows = app.db.select().from(gbpLodgingSnapshots).where(and15(...conditions)).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
21435
21740
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21436
21741
|
for (const row of rows) {
|
|
21437
21742
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21446,9 +21751,9 @@ async function googleRoutes(app, opts) {
|
|
|
21446
21751
|
});
|
|
21447
21752
|
app.get("/projects/:name/gbp/attributes", async (request) => {
|
|
21448
21753
|
const project = resolveProject(app.db, request.params.name);
|
|
21449
|
-
const conditions = [
|
|
21450
|
-
if (request.query.locationName) conditions.push(
|
|
21451
|
-
const rows = app.db.select().from(gbpAttributesSnapshots).where(
|
|
21754
|
+
const conditions = [eq22(gbpAttributesSnapshots.projectId, project.id)];
|
|
21755
|
+
if (request.query.locationName) conditions.push(eq22(gbpAttributesSnapshots.locationName, request.query.locationName));
|
|
21756
|
+
const rows = app.db.select().from(gbpAttributesSnapshots).where(and15(...conditions)).orderBy(desc11(gbpAttributesSnapshots.syncedAt)).all();
|
|
21452
21757
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21453
21758
|
for (const row of rows) {
|
|
21454
21759
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21466,9 +21771,9 @@ async function googleRoutes(app, opts) {
|
|
|
21466
21771
|
});
|
|
21467
21772
|
app.get("/projects/:name/gbp/places", async (request) => {
|
|
21468
21773
|
const project = resolveProject(app.db, request.params.name);
|
|
21469
|
-
const conditions = [
|
|
21470
|
-
if (request.query.locationName) conditions.push(
|
|
21471
|
-
const rows = app.db.select().from(gbpPlaceDetails).where(
|
|
21774
|
+
const conditions = [eq22(gbpPlaceDetails.projectId, project.id)];
|
|
21775
|
+
if (request.query.locationName) conditions.push(eq22(gbpPlaceDetails.locationName, request.query.locationName));
|
|
21776
|
+
const rows = app.db.select().from(gbpPlaceDetails).where(and15(...conditions)).orderBy(desc11(gbpPlaceDetails.syncedAt)).all();
|
|
21472
21777
|
const latestByLocation = /* @__PURE__ */ new Map();
|
|
21473
21778
|
for (const row of rows) {
|
|
21474
21779
|
if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
|
|
@@ -21487,7 +21792,7 @@ async function googleRoutes(app, opts) {
|
|
|
21487
21792
|
app.get("/projects/:name/gbp/summary", async (request) => {
|
|
21488
21793
|
const project = resolveProject(app.db, request.params.name);
|
|
21489
21794
|
const locationName = request.query.locationName ?? null;
|
|
21490
|
-
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(
|
|
21795
|
+
const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), eq22(gbpLocations.selected, true))).all().map((r) => r.n);
|
|
21491
21796
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
21492
21797
|
if (locationNames.length === 0) {
|
|
21493
21798
|
return buildGbpSummary({
|
|
@@ -21501,10 +21806,10 @@ async function googleRoutes(app, opts) {
|
|
|
21501
21806
|
locationProfiles: []
|
|
21502
21807
|
});
|
|
21503
21808
|
}
|
|
21504
|
-
const metricRows = app.db.select().from(gbpDailyMetrics).where(
|
|
21505
|
-
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(
|
|
21506
|
-
const placeActionRows = app.db.select().from(gbpPlaceActions).where(
|
|
21507
|
-
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(
|
|
21809
|
+
const metricRows = app.db.select().from(gbpDailyMetrics).where(and15(eq22(gbpDailyMetrics.projectId, project.id), inArray10(gbpDailyMetrics.locationName, locationNames))).all();
|
|
21810
|
+
const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and15(eq22(gbpKeywordImpressions.projectId, project.id), inArray10(gbpKeywordImpressions.locationName, locationNames))).all();
|
|
21811
|
+
const placeActionRows = app.db.select().from(gbpPlaceActions).where(and15(eq22(gbpPlaceActions.projectId, project.id), inArray10(gbpPlaceActions.locationName, locationNames))).all();
|
|
21812
|
+
const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and15(eq22(gbpLodgingSnapshots.projectId, project.id), inArray10(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc11(gbpLodgingSnapshots.syncedAt)).all();
|
|
21508
21813
|
const latestLodgingByLocation = /* @__PURE__ */ new Map();
|
|
21509
21814
|
for (const row of lodgingRows) {
|
|
21510
21815
|
if (!latestLodgingByLocation.has(row.locationName)) {
|
|
@@ -21518,7 +21823,7 @@ async function googleRoutes(app, opts) {
|
|
|
21518
21823
|
regularHours: gbpLocations.regularHours,
|
|
21519
21824
|
primaryPhone: gbpLocations.primaryPhone,
|
|
21520
21825
|
openStatus: gbpLocations.openStatus
|
|
21521
|
-
}).from(gbpLocations).where(
|
|
21826
|
+
}).from(gbpLocations).where(and15(eq22(gbpLocations.projectId, project.id), inArray10(gbpLocations.locationName, locationNames))).all();
|
|
21522
21827
|
return buildGbpSummary({
|
|
21523
21828
|
locationName,
|
|
21524
21829
|
locationCount: locationNames.length,
|
|
@@ -21534,7 +21839,7 @@ async function googleRoutes(app, opts) {
|
|
|
21534
21839
|
|
|
21535
21840
|
// ../api-routes/src/ads.ts
|
|
21536
21841
|
import crypto19 from "crypto";
|
|
21537
|
-
import { eq as
|
|
21842
|
+
import { eq as eq23, and as and16, asc as asc3, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
|
|
21538
21843
|
function statusDto(row) {
|
|
21539
21844
|
if (!row) return { connected: false };
|
|
21540
21845
|
return {
|
|
@@ -21584,7 +21889,7 @@ async function adsRoutes(app, opts) {
|
|
|
21584
21889
|
createdAt: existingCfg?.createdAt ?? now,
|
|
21585
21890
|
updatedAt: now
|
|
21586
21891
|
});
|
|
21587
|
-
const existingRow = app.db.select().from(adsConnections).where(
|
|
21892
|
+
const existingRow = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21588
21893
|
app.db.transaction((tx) => {
|
|
21589
21894
|
if (existingRow) {
|
|
21590
21895
|
tx.update(adsConnections).set({
|
|
@@ -21594,7 +21899,7 @@ async function adsRoutes(app, opts) {
|
|
|
21594
21899
|
timezone: account.timezone,
|
|
21595
21900
|
status: account.status,
|
|
21596
21901
|
updatedAt: now
|
|
21597
|
-
}).where(
|
|
21902
|
+
}).where(eq23(adsConnections.id, existingRow.id)).run();
|
|
21598
21903
|
} else {
|
|
21599
21904
|
tx.insert(adsConnections).values({
|
|
21600
21905
|
id: crypto19.randomUUID(),
|
|
@@ -21616,16 +21921,16 @@ async function adsRoutes(app, opts) {
|
|
|
21616
21921
|
entityId: account.id
|
|
21617
21922
|
}));
|
|
21618
21923
|
});
|
|
21619
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21924
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21620
21925
|
return statusDto(row);
|
|
21621
21926
|
}
|
|
21622
21927
|
);
|
|
21623
21928
|
app.delete("/projects/:name/ads/connection", async (request) => {
|
|
21624
21929
|
const project = resolveProject(app.db, request.params.name);
|
|
21625
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21930
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21626
21931
|
if (row) {
|
|
21627
21932
|
app.db.transaction((tx) => {
|
|
21628
|
-
tx.delete(adsConnections).where(
|
|
21933
|
+
tx.delete(adsConnections).where(eq23(adsConnections.id, row.id)).run();
|
|
21629
21934
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
21630
21935
|
projectId: project.id,
|
|
21631
21936
|
actor: "api",
|
|
@@ -21641,18 +21946,18 @@ async function adsRoutes(app, opts) {
|
|
|
21641
21946
|
});
|
|
21642
21947
|
app.get("/projects/:name/ads/status", async (request) => {
|
|
21643
21948
|
const project = resolveProject(app.db, request.params.name);
|
|
21644
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21949
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21645
21950
|
return statusDto(row);
|
|
21646
21951
|
});
|
|
21647
21952
|
app.post("/projects/:name/ads/sync", async (request) => {
|
|
21648
21953
|
const project = resolveProject(app.db, request.params.name);
|
|
21649
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21954
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21650
21955
|
if (!row) {
|
|
21651
21956
|
throw validationError('No ads connection for this project. Run "canonry ads connect" first.');
|
|
21652
21957
|
}
|
|
21653
|
-
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
21654
|
-
|
|
21655
|
-
|
|
21958
|
+
const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and16(
|
|
21959
|
+
eq23(runs.projectId, project.id),
|
|
21960
|
+
eq23(runs.kind, RunKinds["ads-sync"]),
|
|
21656
21961
|
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
21657
21962
|
)).get();
|
|
21658
21963
|
if (inFlight) {
|
|
@@ -21674,9 +21979,9 @@ async function adsRoutes(app, opts) {
|
|
|
21674
21979
|
});
|
|
21675
21980
|
app.get("/projects/:name/ads/campaigns", async (request) => {
|
|
21676
21981
|
const project = resolveProject(app.db, request.params.name);
|
|
21677
|
-
const campaignRows = app.db.select().from(adsCampaigns).where(
|
|
21678
|
-
const groupRows = app.db.select().from(adsAdGroups).where(
|
|
21679
|
-
const adRows = app.db.select().from(adsAds).where(
|
|
21982
|
+
const campaignRows = app.db.select().from(adsCampaigns).where(eq23(adsCampaigns.projectId, project.id)).all();
|
|
21983
|
+
const groupRows = app.db.select().from(adsAdGroups).where(eq23(adsAdGroups.projectId, project.id)).all();
|
|
21984
|
+
const adRows = app.db.select().from(adsAds).where(eq23(adsAds.projectId, project.id)).all();
|
|
21680
21985
|
const adsByGroup = /* @__PURE__ */ new Map();
|
|
21681
21986
|
for (const ad of adRows) {
|
|
21682
21987
|
const dto = {
|
|
@@ -21730,12 +22035,12 @@ async function adsRoutes(app, opts) {
|
|
|
21730
22035
|
}
|
|
21731
22036
|
parsedLevel = result.data;
|
|
21732
22037
|
}
|
|
21733
|
-
const conditions = [
|
|
21734
|
-
if (parsedLevel) conditions.push(
|
|
21735
|
-
if (entityId) conditions.push(
|
|
22038
|
+
const conditions = [eq23(adsInsightsDaily.projectId, project.id)];
|
|
22039
|
+
if (parsedLevel) conditions.push(eq23(adsInsightsDaily.level, parsedLevel));
|
|
22040
|
+
if (entityId) conditions.push(eq23(adsInsightsDaily.entityId, entityId));
|
|
21736
22041
|
if (from) conditions.push(gte3(adsInsightsDaily.date, from));
|
|
21737
22042
|
if (to) conditions.push(lte2(adsInsightsDaily.date, to));
|
|
21738
|
-
const rows = app.db.select().from(adsInsightsDaily).where(
|
|
22043
|
+
const rows = app.db.select().from(adsInsightsDaily).where(and16(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
|
|
21739
22044
|
const dtoRows = rows.map((row) => ({
|
|
21740
22045
|
level: row.level,
|
|
21741
22046
|
entityId: row.entityId,
|
|
@@ -21747,19 +22052,19 @@ async function adsRoutes(app, opts) {
|
|
|
21747
22052
|
ctr: adsCtr(row.clicks, row.impressions),
|
|
21748
22053
|
cpcMicros: adsCpcMicros(row.spendMicros, row.clicks)
|
|
21749
22054
|
}));
|
|
21750
|
-
const conn = app.db.select().from(adsConnections).where(
|
|
22055
|
+
const conn = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
21751
22056
|
const response = { rows: dtoRows, currencyCode: conn?.currencyCode ?? null };
|
|
21752
22057
|
return response;
|
|
21753
22058
|
});
|
|
21754
22059
|
app.get("/projects/:name/ads/summary", async (request) => {
|
|
21755
22060
|
const project = resolveProject(app.db, request.params.name);
|
|
21756
|
-
const row = app.db.select().from(adsConnections).where(
|
|
21757
|
-
const campaignCount = app.db.select().from(adsCampaigns).where(
|
|
21758
|
-
const adGroupCount = app.db.select().from(adsAdGroups).where(
|
|
21759
|
-
const adCount = app.db.select().from(adsAds).where(
|
|
21760
|
-
const campaignInsights = app.db.select().from(adsInsightsDaily).where(
|
|
21761
|
-
|
|
21762
|
-
|
|
22061
|
+
const row = app.db.select().from(adsConnections).where(eq23(adsConnections.projectId, project.id)).get();
|
|
22062
|
+
const campaignCount = app.db.select().from(adsCampaigns).where(eq23(adsCampaigns.projectId, project.id)).all().length;
|
|
22063
|
+
const adGroupCount = app.db.select().from(adsAdGroups).where(eq23(adsAdGroups.projectId, project.id)).all().length;
|
|
22064
|
+
const adCount = app.db.select().from(adsAds).where(eq23(adsAds.projectId, project.id)).all().length;
|
|
22065
|
+
const campaignInsights = app.db.select().from(adsInsightsDaily).where(and16(
|
|
22066
|
+
eq23(adsInsightsDaily.projectId, project.id),
|
|
22067
|
+
eq23(adsInsightsDaily.level, "campaign")
|
|
21763
22068
|
)).all();
|
|
21764
22069
|
let impressions = 0;
|
|
21765
22070
|
let clicks = 0;
|
|
@@ -21799,7 +22104,7 @@ async function adsRoutes(app, opts) {
|
|
|
21799
22104
|
|
|
21800
22105
|
// ../api-routes/src/bing.ts
|
|
21801
22106
|
import crypto20 from "crypto";
|
|
21802
|
-
import { eq as
|
|
22107
|
+
import { eq as eq24, and as and17, desc as desc12 } from "drizzle-orm";
|
|
21803
22108
|
|
|
21804
22109
|
// ../integration-bing/src/constants.ts
|
|
21805
22110
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -22173,7 +22478,7 @@ async function bingRoutes(app, opts) {
|
|
|
22173
22478
|
const store = requireConnectionStore();
|
|
22174
22479
|
const project = resolveProject(app.db, request.params.name);
|
|
22175
22480
|
requireConnection(store, project.canonicalDomain);
|
|
22176
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
22481
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
22177
22482
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22178
22483
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
22179
22484
|
for (const row of allInspections) {
|
|
@@ -22262,7 +22567,7 @@ async function bingRoutes(app, opts) {
|
|
|
22262
22567
|
const project = resolveProject(app.db, request.params.name);
|
|
22263
22568
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
22264
22569
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
22265
|
-
const rows = app.db.select().from(bingCoverageSnapshots).where(
|
|
22570
|
+
const rows = app.db.select().from(bingCoverageSnapshots).where(eq24(bingCoverageSnapshots.projectId, project.id)).orderBy(desc12(bingCoverageSnapshots.date)).limit(limit).all();
|
|
22266
22571
|
return rows.map((r) => ({
|
|
22267
22572
|
date: r.date,
|
|
22268
22573
|
indexed: r.indexed,
|
|
@@ -22274,7 +22579,7 @@ async function bingRoutes(app, opts) {
|
|
|
22274
22579
|
requireConnectionStore();
|
|
22275
22580
|
const project = resolveProject(app.db, request.params.name);
|
|
22276
22581
|
const { url, limit } = request.query;
|
|
22277
|
-
const whereClause = url ?
|
|
22582
|
+
const whereClause = url ? and17(eq24(bingUrlInspections.projectId, project.id), eq24(bingUrlInspections.url, url)) : eq24(bingUrlInspections.projectId, project.id);
|
|
22278
22583
|
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();
|
|
22279
22584
|
return filtered.map((r) => ({
|
|
22280
22585
|
id: r.id,
|
|
@@ -22364,7 +22669,7 @@ async function bingRoutes(app, opts) {
|
|
|
22364
22669
|
anchorCount: result.AnchorCount ?? null,
|
|
22365
22670
|
discoveryDate
|
|
22366
22671
|
}).run();
|
|
22367
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
22672
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq24(runs.id, runId)).run();
|
|
22368
22673
|
return {
|
|
22369
22674
|
id,
|
|
22370
22675
|
url,
|
|
@@ -22380,7 +22685,7 @@ async function bingRoutes(app, opts) {
|
|
|
22380
22685
|
} catch (e) {
|
|
22381
22686
|
const msg = e instanceof Error ? e.message : String(e);
|
|
22382
22687
|
bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
|
|
22383
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22688
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
|
|
22384
22689
|
throw e;
|
|
22385
22690
|
}
|
|
22386
22691
|
});
|
|
@@ -22407,7 +22712,7 @@ async function bingRoutes(app, opts) {
|
|
|
22407
22712
|
} else {
|
|
22408
22713
|
bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
|
|
22409
22714
|
}
|
|
22410
|
-
const run = app.db.select().from(runs).where(
|
|
22715
|
+
const run = app.db.select().from(runs).where(eq24(runs.id, runId)).get();
|
|
22411
22716
|
return run;
|
|
22412
22717
|
});
|
|
22413
22718
|
app.post("/projects/:name/bing/request-indexing", async (request) => {
|
|
@@ -22419,7 +22724,7 @@ async function bingRoutes(app, opts) {
|
|
|
22419
22724
|
}
|
|
22420
22725
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
22421
22726
|
if (request.body?.allUnindexed) {
|
|
22422
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(
|
|
22727
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, project.id)).orderBy(desc12(bingUrlInspections.inspectedAt)).all();
|
|
22423
22728
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
22424
22729
|
for (const row of allInspections) {
|
|
22425
22730
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -22506,18 +22811,19 @@ async function bingRoutes(app, opts) {
|
|
|
22506
22811
|
import fs from "fs";
|
|
22507
22812
|
import path from "path";
|
|
22508
22813
|
import os from "os";
|
|
22509
|
-
import { eq as
|
|
22814
|
+
import { eq as eq25, and as and18 } from "drizzle-orm";
|
|
22510
22815
|
function getScreenshotDir() {
|
|
22511
22816
|
return path.join(os.homedir(), ".canonry", "screenshots");
|
|
22512
22817
|
}
|
|
22513
22818
|
async function cdpRoutes(app, opts) {
|
|
22514
22819
|
app.get("/screenshots/:snapshotId", async (request, reply) => {
|
|
22515
22820
|
const { snapshotId } = request.params;
|
|
22516
|
-
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(
|
|
22821
|
+
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath, projectId: runs.projectId }).from(querySnapshots).innerJoin(runs, eq25(querySnapshots.runId, runs.id)).where(eq25(querySnapshots.id, snapshotId)).get();
|
|
22517
22822
|
if (!snapshot?.screenshotPath) {
|
|
22518
22823
|
const err = notFound("Screenshot", snapshotId);
|
|
22519
22824
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
22520
22825
|
}
|
|
22826
|
+
assertProjectScope(request, snapshot.projectId);
|
|
22521
22827
|
const base = path.resolve(getScreenshotDir());
|
|
22522
22828
|
const fullPath = path.resolve(path.join(base, snapshot.screenshotPath));
|
|
22523
22829
|
if (!fullPath.startsWith(base + path.sep) && fullPath !== base) {
|
|
@@ -22580,7 +22886,7 @@ async function cdpRoutes(app, opts) {
|
|
|
22580
22886
|
async (request, reply) => {
|
|
22581
22887
|
const project = resolveProject(app.db, request.params.name);
|
|
22582
22888
|
const { runId } = request.params;
|
|
22583
|
-
const run = app.db.select().from(runs).where(
|
|
22889
|
+
const run = app.db.select().from(runs).where(and18(eq25(runs.id, runId), eq25(runs.projectId, project.id))).get();
|
|
22584
22890
|
if (!run) {
|
|
22585
22891
|
const err = notFound("Run", runId);
|
|
22586
22892
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -22593,8 +22899,8 @@ async function cdpRoutes(app, opts) {
|
|
|
22593
22899
|
citedDomains: querySnapshots.citedDomains,
|
|
22594
22900
|
screenshotPath: querySnapshots.screenshotPath,
|
|
22595
22901
|
rawResponse: querySnapshots.rawResponse
|
|
22596
|
-
}).from(querySnapshots).where(
|
|
22597
|
-
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(
|
|
22902
|
+
}).from(querySnapshots).where(eq25(querySnapshots.runId, runId)).all());
|
|
22903
|
+
const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq25(queries.projectId, project.id)).all();
|
|
22598
22904
|
const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
|
|
22599
22905
|
const byQuery = /* @__PURE__ */ new Map();
|
|
22600
22906
|
for (const snap of snapshots) {
|
|
@@ -22677,7 +22983,7 @@ async function cdpRoutes(app, opts) {
|
|
|
22677
22983
|
|
|
22678
22984
|
// ../api-routes/src/ga.ts
|
|
22679
22985
|
import crypto21 from "crypto";
|
|
22680
|
-
import { eq as
|
|
22986
|
+
import { eq as eq26, desc as desc13, and as and19, sql as sql10 } from "drizzle-orm";
|
|
22681
22987
|
function gaLog(level, action, ctx) {
|
|
22682
22988
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
22683
22989
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -22884,10 +23190,10 @@ async function ga4Routes(app, opts) {
|
|
|
22884
23190
|
if (!saConn && !oauthConn) {
|
|
22885
23191
|
throw notFound("GA4 connection", project.name);
|
|
22886
23192
|
}
|
|
22887
|
-
app.db.delete(gaTrafficSnapshots).where(
|
|
22888
|
-
app.db.delete(gaTrafficSummaries).where(
|
|
22889
|
-
app.db.delete(gaAiReferrals).where(
|
|
22890
|
-
app.db.delete(gaSocialReferrals).where(
|
|
23193
|
+
app.db.delete(gaTrafficSnapshots).where(eq26(gaTrafficSnapshots.projectId, project.id)).run();
|
|
23194
|
+
app.db.delete(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).run();
|
|
23195
|
+
app.db.delete(gaAiReferrals).where(eq26(gaAiReferrals.projectId, project.id)).run();
|
|
23196
|
+
app.db.delete(gaSocialReferrals).where(eq26(gaSocialReferrals.projectId, project.id)).run();
|
|
22891
23197
|
const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
|
|
22892
23198
|
opts.ga4CredentialStore?.deleteConnection(project.name);
|
|
22893
23199
|
opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
|
|
@@ -22908,7 +23214,7 @@ async function ga4Routes(app, opts) {
|
|
|
22908
23214
|
if (!connected) {
|
|
22909
23215
|
return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
|
|
22910
23216
|
}
|
|
22911
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
23217
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
22912
23218
|
return {
|
|
22913
23219
|
connected: true,
|
|
22914
23220
|
propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
|
|
@@ -22972,10 +23278,10 @@ async function ga4Routes(app, opts) {
|
|
|
22972
23278
|
app.db.transaction((tx) => {
|
|
22973
23279
|
if (syncTraffic) {
|
|
22974
23280
|
tx.delete(gaTrafficSnapshots).where(
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
23281
|
+
and19(
|
|
23282
|
+
eq26(gaTrafficSnapshots.projectId, project.id),
|
|
23283
|
+
sql10`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
23284
|
+
sql10`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
22979
23285
|
)
|
|
22980
23286
|
).run();
|
|
22981
23287
|
for (const row of rows) {
|
|
@@ -22996,10 +23302,10 @@ async function ga4Routes(app, opts) {
|
|
|
22996
23302
|
}
|
|
22997
23303
|
if (syncAi) {
|
|
22998
23304
|
tx.delete(gaAiReferrals).where(
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23305
|
+
and19(
|
|
23306
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23307
|
+
sql10`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
23308
|
+
sql10`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
23003
23309
|
)
|
|
23004
23310
|
).run();
|
|
23005
23311
|
for (const row of aiReferrals) {
|
|
@@ -23022,10 +23328,10 @@ async function ga4Routes(app, opts) {
|
|
|
23022
23328
|
}
|
|
23023
23329
|
if (syncSocial) {
|
|
23024
23330
|
tx.delete(gaSocialReferrals).where(
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23028
|
-
|
|
23331
|
+
and19(
|
|
23332
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23333
|
+
sql10`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
23334
|
+
sql10`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
23029
23335
|
)
|
|
23030
23336
|
).run();
|
|
23031
23337
|
for (const row of socialReferrals) {
|
|
@@ -23044,7 +23350,7 @@ async function ga4Routes(app, opts) {
|
|
|
23044
23350
|
}
|
|
23045
23351
|
}
|
|
23046
23352
|
if (syncSummary) {
|
|
23047
|
-
tx.delete(gaTrafficSummaries).where(
|
|
23353
|
+
tx.delete(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).run();
|
|
23048
23354
|
tx.insert(gaTrafficSummaries).values({
|
|
23049
23355
|
id: crypto21.randomUUID(),
|
|
23050
23356
|
projectId: project.id,
|
|
@@ -23056,7 +23362,7 @@ async function ga4Routes(app, opts) {
|
|
|
23056
23362
|
syncedAt: now,
|
|
23057
23363
|
syncRunId: runId
|
|
23058
23364
|
}).run();
|
|
23059
|
-
tx.delete(gaTrafficWindowSummaries).where(
|
|
23365
|
+
tx.delete(gaTrafficWindowSummaries).where(eq26(gaTrafficWindowSummaries.projectId, project.id)).run();
|
|
23060
23366
|
for (const ws of windowSummaries) {
|
|
23061
23367
|
tx.insert(gaTrafficWindowSummaries).values({
|
|
23062
23368
|
id: crypto21.randomUUID(),
|
|
@@ -23074,7 +23380,7 @@ async function ga4Routes(app, opts) {
|
|
|
23074
23380
|
}
|
|
23075
23381
|
}
|
|
23076
23382
|
});
|
|
23077
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(
|
|
23383
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq26(runs.id, runId)).run();
|
|
23078
23384
|
const syncedComponents = only ? [
|
|
23079
23385
|
...syncTraffic ? ["traffic"] : [],
|
|
23080
23386
|
...syncSummary ? ["summary"] : [],
|
|
@@ -23103,7 +23409,7 @@ async function ga4Routes(app, opts) {
|
|
|
23103
23409
|
} catch (e) {
|
|
23104
23410
|
const msg = e instanceof Error ? e.message : String(e);
|
|
23105
23411
|
gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
|
|
23106
|
-
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
23412
|
+
app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(runs.id, runId)).run();
|
|
23107
23413
|
throw e;
|
|
23108
23414
|
}
|
|
23109
23415
|
});
|
|
@@ -23114,66 +23420,66 @@ async function ga4Routes(app, opts) {
|
|
|
23114
23420
|
const window = parseWindow(request.query.window);
|
|
23115
23421
|
const cutoff = windowCutoff(window);
|
|
23116
23422
|
const cutoffDate = cutoff?.slice(0, 10) ?? null;
|
|
23117
|
-
const snapshotConditions = [
|
|
23118
|
-
if (cutoffDate) snapshotConditions.push(
|
|
23119
|
-
const aiConditions = [
|
|
23120
|
-
if (cutoffDate) aiConditions.push(
|
|
23121
|
-
const socialConditions = [
|
|
23122
|
-
if (cutoffDate) socialConditions.push(
|
|
23423
|
+
const snapshotConditions = [eq26(gaTrafficSnapshots.projectId, project.id)];
|
|
23424
|
+
if (cutoffDate) snapshotConditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
23425
|
+
const aiConditions = [eq26(gaAiReferrals.projectId, project.id)];
|
|
23426
|
+
if (cutoffDate) aiConditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
23427
|
+
const socialConditions = [eq26(gaSocialReferrals.projectId, project.id)];
|
|
23428
|
+
if (cutoffDate) socialConditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
23123
23429
|
const windowSummaryRow = cutoffDate ? app.db.select({
|
|
23124
23430
|
totalSessions: gaTrafficWindowSummaries.totalSessions,
|
|
23125
23431
|
totalOrganicSessions: gaTrafficWindowSummaries.totalOrganicSessions,
|
|
23126
23432
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
23127
23433
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
23128
23434
|
}).from(gaTrafficWindowSummaries).where(
|
|
23129
|
-
|
|
23130
|
-
|
|
23131
|
-
|
|
23435
|
+
and19(
|
|
23436
|
+
eq26(gaTrafficWindowSummaries.projectId, project.id),
|
|
23437
|
+
eq26(gaTrafficWindowSummaries.windowKey, window)
|
|
23132
23438
|
)
|
|
23133
23439
|
).get() : null;
|
|
23134
23440
|
const snapshotTotalsRow = cutoffDate && !windowSummaryRow ? app.db.select({
|
|
23135
|
-
totalSessions:
|
|
23136
|
-
totalOrganicSessions:
|
|
23137
|
-
totalUsers:
|
|
23138
|
-
}).from(gaTrafficSnapshots).where(
|
|
23441
|
+
totalSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
23442
|
+
totalOrganicSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
23443
|
+
totalUsers: sql10`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
23444
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).get() : null;
|
|
23139
23445
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
23140
23446
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
23141
23447
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
23142
23448
|
totalUsers: gaTrafficSummaries.totalUsers
|
|
23143
|
-
}).from(gaTrafficSummaries).where(
|
|
23449
|
+
}).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).get();
|
|
23144
23450
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
23145
|
-
totalDirectSessions:
|
|
23146
|
-
}).from(gaTrafficSnapshots).where(
|
|
23451
|
+
totalDirectSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
23452
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).get();
|
|
23147
23453
|
const summaryMeta = app.db.select({
|
|
23148
23454
|
periodStart: gaTrafficSummaries.periodStart,
|
|
23149
23455
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
23150
|
-
}).from(gaTrafficSummaries).where(
|
|
23456
|
+
}).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).get();
|
|
23151
23457
|
const rows = app.db.select({
|
|
23152
|
-
landingPage:
|
|
23153
|
-
sessions:
|
|
23154
|
-
organicSessions:
|
|
23155
|
-
directSessions:
|
|
23156
|
-
users:
|
|
23157
|
-
}).from(gaTrafficSnapshots).where(
|
|
23458
|
+
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
23459
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23460
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23461
|
+
directSessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
23462
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23463
|
+
}).from(gaTrafficSnapshots).where(and19(...snapshotConditions)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
23158
23464
|
const aiReferralRows = app.db.select({
|
|
23159
23465
|
source: gaAiReferrals.source,
|
|
23160
23466
|
medium: gaAiReferrals.medium,
|
|
23161
23467
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23162
|
-
sessions:
|
|
23163
|
-
users:
|
|
23164
|
-
}).from(gaAiReferrals).where(
|
|
23468
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23469
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23470
|
+
}).from(gaAiReferrals).where(and19(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
23165
23471
|
const aiReferralLandingPageRows = app.db.select({
|
|
23166
23472
|
source: gaAiReferrals.source,
|
|
23167
23473
|
medium: gaAiReferrals.medium,
|
|
23168
23474
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23169
|
-
landingPage:
|
|
23170
|
-
sessions:
|
|
23171
|
-
users:
|
|
23172
|
-
}).from(gaAiReferrals).where(
|
|
23475
|
+
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23476
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23477
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23478
|
+
}).from(gaAiReferrals).where(and19(...aiConditions)).groupBy(
|
|
23173
23479
|
gaAiReferrals.source,
|
|
23174
23480
|
gaAiReferrals.medium,
|
|
23175
23481
|
gaAiReferrals.sourceDimension,
|
|
23176
|
-
|
|
23482
|
+
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23177
23483
|
).all();
|
|
23178
23484
|
const aiReferrals = pickWinningDimension(
|
|
23179
23485
|
aiReferralRows,
|
|
@@ -23184,10 +23490,10 @@ async function ga4Routes(app, opts) {
|
|
|
23184
23490
|
(r) => `${r.source}\0${r.medium}\0${r.landingPage}`
|
|
23185
23491
|
);
|
|
23186
23492
|
const aiDeduped = app.db.select({
|
|
23187
|
-
sessions:
|
|
23188
|
-
users:
|
|
23493
|
+
sessions: sql10`COALESCE(SUM(max_sessions), 0)`,
|
|
23494
|
+
users: sql10`COALESCE(SUM(max_users), 0)`
|
|
23189
23495
|
}).from(
|
|
23190
|
-
|
|
23496
|
+
sql10`(
|
|
23191
23497
|
SELECT date, source, medium,
|
|
23192
23498
|
MAX(dimension_sessions) AS max_sessions,
|
|
23193
23499
|
MAX(dimension_users) AS max_users
|
|
@@ -23196,7 +23502,7 @@ async function ga4Routes(app, opts) {
|
|
|
23196
23502
|
SUM(sessions) AS dimension_sessions,
|
|
23197
23503
|
SUM(users) AS dimension_users
|
|
23198
23504
|
FROM ga_ai_referrals
|
|
23199
|
-
WHERE project_id = ${project.id}${cutoffDate ?
|
|
23505
|
+
WHERE project_id = ${project.id}${cutoffDate ? sql10` AND date >= ${cutoffDate}` : sql10``}
|
|
23200
23506
|
GROUP BY date, source, medium, source_dimension
|
|
23201
23507
|
)
|
|
23202
23508
|
GROUP BY date, source, medium
|
|
@@ -23204,9 +23510,9 @@ async function ga4Routes(app, opts) {
|
|
|
23204
23510
|
).get();
|
|
23205
23511
|
const aiBySessionRows = app.db.select({
|
|
23206
23512
|
channelGroup: gaAiReferrals.channelGroup,
|
|
23207
|
-
sessions:
|
|
23208
|
-
users:
|
|
23209
|
-
}).from(gaAiReferrals).where(
|
|
23513
|
+
sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
23514
|
+
users: sql10`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
23515
|
+
}).from(gaAiReferrals).where(and19(...aiConditions, eq26(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
23210
23516
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
23211
23517
|
let aiBySessionUsers = 0;
|
|
23212
23518
|
for (const row of aiBySessionRows) {
|
|
@@ -23218,14 +23524,14 @@ async function ga4Routes(app, opts) {
|
|
|
23218
23524
|
source: gaSocialReferrals.source,
|
|
23219
23525
|
medium: gaSocialReferrals.medium,
|
|
23220
23526
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
23221
|
-
sessions:
|
|
23222
|
-
users:
|
|
23223
|
-
}).from(gaSocialReferrals).where(
|
|
23527
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
23528
|
+
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
23529
|
+
}).from(gaSocialReferrals).where(and19(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql10`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
23224
23530
|
const socialTotals = app.db.select({
|
|
23225
|
-
sessions:
|
|
23226
|
-
users:
|
|
23227
|
-
}).from(gaSocialReferrals).where(
|
|
23228
|
-
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(
|
|
23531
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`,
|
|
23532
|
+
users: sql10`SUM(${gaSocialReferrals.users})`
|
|
23533
|
+
}).from(gaSocialReferrals).where(and19(...socialConditions)).get();
|
|
23534
|
+
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq26(gaTrafficSummaries.projectId, project.id)).orderBy(desc13(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
23229
23535
|
const total = summaryRow?.totalSessions ?? 0;
|
|
23230
23536
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
23231
23537
|
const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
|
|
@@ -23305,22 +23611,22 @@ async function ga4Routes(app, opts) {
|
|
|
23305
23611
|
const project = resolveProject(app.db, request.params.name);
|
|
23306
23612
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23307
23613
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23308
|
-
const conditions = [
|
|
23309
|
-
if (cutoffDate) conditions.push(
|
|
23614
|
+
const conditions = [eq26(gaAiReferrals.projectId, project.id)];
|
|
23615
|
+
if (cutoffDate) conditions.push(sql10`${gaAiReferrals.date} >= ${cutoffDate}`);
|
|
23310
23616
|
const rows = app.db.select({
|
|
23311
23617
|
date: gaAiReferrals.date,
|
|
23312
23618
|
source: gaAiReferrals.source,
|
|
23313
23619
|
medium: gaAiReferrals.medium,
|
|
23314
|
-
landingPage:
|
|
23620
|
+
landingPage: sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
23315
23621
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
23316
|
-
sessions:
|
|
23317
|
-
users:
|
|
23318
|
-
}).from(gaAiReferrals).where(
|
|
23622
|
+
sessions: sql10`SUM(${gaAiReferrals.sessions})`,
|
|
23623
|
+
users: sql10`SUM(${gaAiReferrals.users})`
|
|
23624
|
+
}).from(gaAiReferrals).where(and19(...conditions)).groupBy(
|
|
23319
23625
|
gaAiReferrals.date,
|
|
23320
23626
|
gaAiReferrals.source,
|
|
23321
23627
|
gaAiReferrals.medium,
|
|
23322
23628
|
gaAiReferrals.sourceDimension,
|
|
23323
|
-
|
|
23629
|
+
sql10`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
|
|
23324
23630
|
).orderBy(gaAiReferrals.date).all();
|
|
23325
23631
|
return rows;
|
|
23326
23632
|
});
|
|
@@ -23328,8 +23634,8 @@ async function ga4Routes(app, opts) {
|
|
|
23328
23634
|
const project = resolveProject(app.db, request.params.name);
|
|
23329
23635
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23330
23636
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23331
|
-
const conditions = [
|
|
23332
|
-
if (cutoffDate) conditions.push(
|
|
23637
|
+
const conditions = [eq26(gaSocialReferrals.projectId, project.id)];
|
|
23638
|
+
if (cutoffDate) conditions.push(sql10`${gaSocialReferrals.date} >= ${cutoffDate}`);
|
|
23333
23639
|
const rows = app.db.select({
|
|
23334
23640
|
date: gaSocialReferrals.date,
|
|
23335
23641
|
source: gaSocialReferrals.source,
|
|
@@ -23337,7 +23643,7 @@ async function ga4Routes(app, opts) {
|
|
|
23337
23643
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
23338
23644
|
sessions: gaSocialReferrals.sessions,
|
|
23339
23645
|
users: gaSocialReferrals.users
|
|
23340
|
-
}).from(gaSocialReferrals).where(
|
|
23646
|
+
}).from(gaSocialReferrals).where(and19(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
23341
23647
|
return rows;
|
|
23342
23648
|
});
|
|
23343
23649
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -23350,10 +23656,10 @@ async function ga4Routes(app, opts) {
|
|
|
23350
23656
|
d.setDate(d.getDate() - n);
|
|
23351
23657
|
return fmt(d);
|
|
23352
23658
|
};
|
|
23353
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
23354
|
-
|
|
23355
|
-
|
|
23356
|
-
|
|
23659
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and19(
|
|
23660
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23661
|
+
sql10`${gaSocialReferrals.date} >= ${from}`,
|
|
23662
|
+
sql10`${gaSocialReferrals.date} < ${to}`
|
|
23357
23663
|
)).get();
|
|
23358
23664
|
const current7d = sumSocial(daysAgo(7), fmt(today));
|
|
23359
23665
|
const prev7d = sumSocial(daysAgo(14), daysAgo(7));
|
|
@@ -23362,19 +23668,19 @@ async function ga4Routes(app, opts) {
|
|
|
23362
23668
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
23363
23669
|
const sourceCurrent = app.db.select({
|
|
23364
23670
|
source: gaSocialReferrals.source,
|
|
23365
|
-
sessions:
|
|
23366
|
-
}).from(gaSocialReferrals).where(
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23671
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
23672
|
+
}).from(gaSocialReferrals).where(and19(
|
|
23673
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23674
|
+
sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`,
|
|
23675
|
+
sql10`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
23370
23676
|
)).groupBy(gaSocialReferrals.source).all();
|
|
23371
23677
|
const sourcePrev = app.db.select({
|
|
23372
23678
|
source: gaSocialReferrals.source,
|
|
23373
|
-
sessions:
|
|
23374
|
-
}).from(gaSocialReferrals).where(
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23679
|
+
sessions: sql10`SUM(${gaSocialReferrals.sessions})`
|
|
23680
|
+
}).from(gaSocialReferrals).where(and19(
|
|
23681
|
+
eq26(gaSocialReferrals.projectId, project.id),
|
|
23682
|
+
sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`,
|
|
23683
|
+
sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`
|
|
23378
23684
|
)).groupBy(gaSocialReferrals.source).all();
|
|
23379
23685
|
const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
|
|
23380
23686
|
let biggestMover = null;
|
|
@@ -23413,16 +23719,16 @@ async function ga4Routes(app, opts) {
|
|
|
23413
23719
|
return fmt(d);
|
|
23414
23720
|
};
|
|
23415
23721
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
23416
|
-
const sumTotal = (from, to) => app.db.select({ sessions:
|
|
23417
|
-
const sumOrganic = (from, to) => app.db.select({ sessions:
|
|
23418
|
-
const sumDirect = (from, to) => app.db.select({ sessions:
|
|
23419
|
-
const sumAi = (from, to) => app.db.select({ sessions:
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23423
|
-
|
|
23722
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23723
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23724
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and19(eq26(gaTrafficSnapshots.projectId, project.id), sql10`${gaTrafficSnapshots.date} >= ${from}`, sql10`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
23725
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23726
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23727
|
+
sql10`${gaAiReferrals.date} >= ${from}`,
|
|
23728
|
+
sql10`${gaAiReferrals.date} < ${to}`,
|
|
23729
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23424
23730
|
)).get();
|
|
23425
|
-
const sumSocial = (from, to) => app.db.select({ sessions:
|
|
23731
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql10`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${from}`, sql10`${gaSocialReferrals.date} < ${to}`)).get();
|
|
23426
23732
|
const todayStr = fmt(today);
|
|
23427
23733
|
const buildTrend = (sum) => {
|
|
23428
23734
|
const c7 = sum(daysAgo(7), todayStr)?.sessions ?? 0;
|
|
@@ -23431,17 +23737,17 @@ async function ga4Routes(app, opts) {
|
|
|
23431
23737
|
const p30 = sum(daysAgo(60), daysAgo(30))?.sessions ?? 0;
|
|
23432
23738
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
23433
23739
|
};
|
|
23434
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23740
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23741
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23742
|
+
sql10`${gaAiReferrals.date} >= ${daysAgo(7)}`,
|
|
23743
|
+
sql10`${gaAiReferrals.date} < ${todayStr}`,
|
|
23744
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23439
23745
|
)).groupBy(gaAiReferrals.source).all();
|
|
23440
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions:
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23746
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql10`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and19(
|
|
23747
|
+
eq26(gaAiReferrals.projectId, project.id),
|
|
23748
|
+
sql10`${gaAiReferrals.date} >= ${daysAgo(14)}`,
|
|
23749
|
+
sql10`${gaAiReferrals.date} < ${daysAgo(7)}`,
|
|
23750
|
+
eq26(gaAiReferrals.sourceDimension, "session")
|
|
23445
23751
|
)).groupBy(gaAiReferrals.source).all();
|
|
23446
23752
|
const findBiggestMover = (current, prev) => {
|
|
23447
23753
|
const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
|
|
@@ -23457,8 +23763,8 @@ async function ga4Routes(app, opts) {
|
|
|
23457
23763
|
}
|
|
23458
23764
|
return mover;
|
|
23459
23765
|
};
|
|
23460
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
23461
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions:
|
|
23766
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(7)}`, sql10`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
23767
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql10`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and19(eq26(gaSocialReferrals.projectId, project.id), sql10`${gaSocialReferrals.date} >= ${daysAgo(14)}`, sql10`${gaSocialReferrals.date} < ${daysAgo(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
23462
23768
|
return {
|
|
23463
23769
|
total: buildTrend(sumTotal),
|
|
23464
23770
|
organic: buildTrend(sumOrganic),
|
|
@@ -23473,14 +23779,14 @@ async function ga4Routes(app, opts) {
|
|
|
23473
23779
|
const project = resolveProject(app.db, request.params.name);
|
|
23474
23780
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23475
23781
|
const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
|
|
23476
|
-
const conditions = [
|
|
23477
|
-
if (cutoffDate) conditions.push(
|
|
23782
|
+
const conditions = [eq26(gaTrafficSnapshots.projectId, project.id)];
|
|
23783
|
+
if (cutoffDate) conditions.push(sql10`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
|
|
23478
23784
|
const rows = app.db.select({
|
|
23479
23785
|
date: gaTrafficSnapshots.date,
|
|
23480
|
-
sessions:
|
|
23481
|
-
organicSessions:
|
|
23482
|
-
users:
|
|
23483
|
-
}).from(gaTrafficSnapshots).where(
|
|
23786
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23787
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23788
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23789
|
+
}).from(gaTrafficSnapshots).where(and19(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
23484
23790
|
return rows.map((r) => ({
|
|
23485
23791
|
date: r.date,
|
|
23486
23792
|
sessions: r.sessions ?? 0,
|
|
@@ -23492,11 +23798,11 @@ async function ga4Routes(app, opts) {
|
|
|
23492
23798
|
const project = resolveProject(app.db, request.params.name);
|
|
23493
23799
|
requireGa4Connection(opts, project.name, project.canonicalDomain);
|
|
23494
23800
|
const trafficPages = app.db.select({
|
|
23495
|
-
landingPage:
|
|
23496
|
-
sessions:
|
|
23497
|
-
organicSessions:
|
|
23498
|
-
users:
|
|
23499
|
-
}).from(gaTrafficSnapshots).where(
|
|
23801
|
+
landingPage: sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
|
|
23802
|
+
sessions: sql10`SUM(${gaTrafficSnapshots.sessions})`,
|
|
23803
|
+
organicSessions: sql10`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
23804
|
+
users: sql10`SUM(${gaTrafficSnapshots.users})`
|
|
23805
|
+
}).from(gaTrafficSnapshots).where(eq26(gaTrafficSnapshots.projectId, project.id)).groupBy(sql10`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql10`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
|
|
23500
23806
|
return {
|
|
23501
23807
|
pages: trafficPages.map((r) => ({
|
|
23502
23808
|
landingPage: r.landingPage,
|
|
@@ -25144,7 +25450,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
25144
25450
|
|
|
25145
25451
|
// ../api-routes/src/backlinks.ts
|
|
25146
25452
|
import crypto23 from "crypto";
|
|
25147
|
-
import { and as
|
|
25453
|
+
import { and as and21, asc as asc4, desc as desc14, eq as eq27, sql as sql11 } from "drizzle-orm";
|
|
25148
25454
|
|
|
25149
25455
|
// ../integration-commoncrawl/src/constants.ts
|
|
25150
25456
|
import os2 from "os";
|
|
@@ -25434,7 +25740,7 @@ async function queryBacklinks(opts) {
|
|
|
25434
25740
|
const reversed = opts.targets.map(reverseDomain);
|
|
25435
25741
|
const targetList = reversed.map(quote).join(", ");
|
|
25436
25742
|
const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
|
|
25437
|
-
const
|
|
25743
|
+
const sql14 = `
|
|
25438
25744
|
WITH vertices AS (
|
|
25439
25745
|
SELECT * FROM read_csv(
|
|
25440
25746
|
${quote(opts.vertexPath)},
|
|
@@ -25470,7 +25776,7 @@ async function queryBacklinks(opts) {
|
|
|
25470
25776
|
const conn = await instance.connect();
|
|
25471
25777
|
let rows;
|
|
25472
25778
|
try {
|
|
25473
|
-
const reader = await conn.runAndReadAll(
|
|
25779
|
+
const reader = await conn.runAndReadAll(sql14);
|
|
25474
25780
|
rows = reader.getRowObjects();
|
|
25475
25781
|
} finally {
|
|
25476
25782
|
conn.disconnectSync?.();
|
|
@@ -25547,7 +25853,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
25547
25853
|
}
|
|
25548
25854
|
|
|
25549
25855
|
// ../api-routes/src/backlinks-filter.ts
|
|
25550
|
-
import { and as
|
|
25856
|
+
import { and as and20, ne as ne3, notLike } from "drizzle-orm";
|
|
25551
25857
|
var BACKLINK_FILTER_PATTERNS = [
|
|
25552
25858
|
"*.google.com",
|
|
25553
25859
|
"*.googleusercontent.com",
|
|
@@ -25570,7 +25876,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
25570
25876
|
conditions.push(ne3(backlinkDomains.linkingDomain, pattern));
|
|
25571
25877
|
}
|
|
25572
25878
|
}
|
|
25573
|
-
const combined =
|
|
25879
|
+
const combined = and20(...conditions);
|
|
25574
25880
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
25575
25881
|
return combined;
|
|
25576
25882
|
}
|
|
@@ -25641,11 +25947,11 @@ function mapRunRow(row) {
|
|
|
25641
25947
|
}
|
|
25642
25948
|
function latestSummaryForProject(db, projectId, source, release) {
|
|
25643
25949
|
const conditions = [
|
|
25644
|
-
|
|
25645
|
-
|
|
25950
|
+
eq27(backlinkSummaries.projectId, projectId),
|
|
25951
|
+
eq27(backlinkSummaries.source, source)
|
|
25646
25952
|
];
|
|
25647
|
-
if (release) conditions.push(
|
|
25648
|
-
return db.select().from(backlinkSummaries).where(
|
|
25953
|
+
if (release) conditions.push(eq27(backlinkSummaries.release, release));
|
|
25954
|
+
return db.select().from(backlinkSummaries).where(and21(...conditions)).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
25649
25955
|
}
|
|
25650
25956
|
function parseExcludeCrawlers(value) {
|
|
25651
25957
|
if (!value) return false;
|
|
@@ -25653,19 +25959,19 @@ function parseExcludeCrawlers(value) {
|
|
|
25653
25959
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
25654
25960
|
}
|
|
25655
25961
|
function computeFilteredSummary(db, base) {
|
|
25656
|
-
const baseDomainCondition =
|
|
25657
|
-
|
|
25658
|
-
|
|
25659
|
-
|
|
25962
|
+
const baseDomainCondition = and21(
|
|
25963
|
+
eq27(backlinkDomains.projectId, base.projectId),
|
|
25964
|
+
eq27(backlinkDomains.source, base.source),
|
|
25965
|
+
eq27(backlinkDomains.release, base.release)
|
|
25660
25966
|
);
|
|
25661
|
-
const filteredCondition =
|
|
25967
|
+
const filteredCondition = and21(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
25662
25968
|
const unfilteredAgg = db.select({
|
|
25663
|
-
count:
|
|
25664
|
-
total:
|
|
25969
|
+
count: sql11`count(*)`,
|
|
25970
|
+
total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
25665
25971
|
}).from(backlinkDomains).where(baseDomainCondition).get();
|
|
25666
25972
|
const filteredAgg = db.select({
|
|
25667
|
-
count:
|
|
25668
|
-
total:
|
|
25973
|
+
count: sql11`count(*)`,
|
|
25974
|
+
total: sql11`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
25669
25975
|
}).from(backlinkDomains).where(filteredCondition).get();
|
|
25670
25976
|
const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc14(backlinkDomains.numHosts)).limit(10).all();
|
|
25671
25977
|
const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
|
|
@@ -25688,13 +25994,13 @@ function computeFilteredSummary(db, base) {
|
|
|
25688
25994
|
};
|
|
25689
25995
|
}
|
|
25690
25996
|
function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
|
|
25691
|
-
const summary = db.select().from(backlinkSummaries).where(
|
|
25997
|
+
const summary = db.select().from(backlinkSummaries).where(and21(eq27(backlinkSummaries.projectId, projectId), eq27(backlinkSummaries.source, source))).orderBy(desc14(backlinkSummaries.queriedAt)).limit(1).get();
|
|
25692
25998
|
let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
|
|
25693
25999
|
if (summary && excludeCrawlers) {
|
|
25694
|
-
const filtered = db.select({ count:
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
26000
|
+
const filtered = db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(and21(
|
|
26001
|
+
eq27(backlinkDomains.projectId, projectId),
|
|
26002
|
+
eq27(backlinkDomains.source, source),
|
|
26003
|
+
eq27(backlinkDomains.release, summary.release),
|
|
25698
26004
|
backlinkCrawlerExclusionClause()
|
|
25699
26005
|
)).get();
|
|
25700
26006
|
totalLinkingDomains = Number(filtered?.count ?? 0);
|
|
@@ -25709,7 +26015,7 @@ function buildSourceAvailability(db, projectId, source, connected, excludeCrawle
|
|
|
25709
26015
|
};
|
|
25710
26016
|
}
|
|
25711
26017
|
function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
|
|
25712
|
-
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
26018
|
+
const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
25713
26019
|
const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
|
|
25714
26020
|
const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
|
|
25715
26021
|
const sources = [
|
|
@@ -25765,7 +26071,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25765
26071
|
"@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
|
|
25766
26072
|
);
|
|
25767
26073
|
}
|
|
25768
|
-
const existing = app.db.select().from(ccReleaseSyncs).where(
|
|
26074
|
+
const existing = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, release)).get();
|
|
25769
26075
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
25770
26076
|
if (existing) {
|
|
25771
26077
|
if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
|
|
@@ -25776,9 +26082,9 @@ async function backlinksRoutes(app, opts) {
|
|
|
25776
26082
|
phaseDetail: null,
|
|
25777
26083
|
error: null,
|
|
25778
26084
|
updatedAt: now
|
|
25779
|
-
}).where(
|
|
26085
|
+
}).where(eq27(ccReleaseSyncs.id, existing.id)).run();
|
|
25780
26086
|
opts.onReleaseSyncRequested(existing.id, release);
|
|
25781
|
-
const refreshed = app.db.select().from(ccReleaseSyncs).where(
|
|
26087
|
+
const refreshed = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.id, existing.id)).get();
|
|
25782
26088
|
return reply.status(200).send(mapSyncRow(refreshed));
|
|
25783
26089
|
}
|
|
25784
26090
|
const id = crypto23.randomUUID();
|
|
@@ -25790,7 +26096,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25790
26096
|
updatedAt: now
|
|
25791
26097
|
}).run();
|
|
25792
26098
|
opts.onReleaseSyncRequested(id, release);
|
|
25793
|
-
const inserted = app.db.select().from(ccReleaseSyncs).where(
|
|
26099
|
+
const inserted = app.db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.id, id)).get();
|
|
25794
26100
|
return reply.status(201).send(mapSyncRow(inserted));
|
|
25795
26101
|
});
|
|
25796
26102
|
app.get("/backlinks/syncs/latest", async (_request, reply) => {
|
|
@@ -25848,7 +26154,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25848
26154
|
createdAt: now
|
|
25849
26155
|
}).run();
|
|
25850
26156
|
opts.onBacklinkExtractRequested(runId, project.id, release);
|
|
25851
|
-
const run = app.db.select().from(runs).where(
|
|
26157
|
+
const run = app.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
25852
26158
|
return reply.status(201).send(mapRunRow(run));
|
|
25853
26159
|
});
|
|
25854
26160
|
app.get(
|
|
@@ -25874,13 +26180,13 @@ async function backlinksRoutes(app, opts) {
|
|
|
25874
26180
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
25875
26181
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
25876
26182
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
25877
|
-
const baseDomainCondition =
|
|
25878
|
-
|
|
25879
|
-
|
|
25880
|
-
|
|
26183
|
+
const baseDomainCondition = and21(
|
|
26184
|
+
eq27(backlinkDomains.projectId, project.id),
|
|
26185
|
+
eq27(backlinkDomains.source, source),
|
|
26186
|
+
eq27(backlinkDomains.release, targetRelease)
|
|
25881
26187
|
);
|
|
25882
|
-
const domainCondition = excludeCrawlers ?
|
|
25883
|
-
const totalRow = app.db.select({ count:
|
|
26188
|
+
const domainCondition = excludeCrawlers ? and21(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
26189
|
+
const totalRow = app.db.select({ count: sql11`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
25884
26190
|
const rows = app.db.select({
|
|
25885
26191
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
25886
26192
|
numHosts: backlinkDomains.numHosts,
|
|
@@ -25903,7 +26209,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25903
26209
|
async (request, reply) => {
|
|
25904
26210
|
const project = resolveProject(app.db, request.params.name);
|
|
25905
26211
|
const source = parseSourceParam(request.query.source);
|
|
25906
|
-
const rows = app.db.select().from(backlinkSummaries).where(
|
|
26212
|
+
const rows = app.db.select().from(backlinkSummaries).where(and21(eq27(backlinkSummaries.projectId, project.id), eq27(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
|
|
25907
26213
|
const response = rows.map((r) => ({
|
|
25908
26214
|
release: r.release,
|
|
25909
26215
|
totalLinkingDomains: r.totalLinkingDomains,
|
|
@@ -25950,7 +26256,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25950
26256
|
createdAt: now
|
|
25951
26257
|
}).run();
|
|
25952
26258
|
opts.onBingBacklinkSyncRequested(runId, project.id);
|
|
25953
|
-
const run = app.db.select().from(runs).where(
|
|
26259
|
+
const run = app.db.select().from(runs).where(eq27(runs.id, runId)).get();
|
|
25954
26260
|
return reply.status(201).send(mapRunRow(run));
|
|
25955
26261
|
}
|
|
25956
26262
|
);
|
|
@@ -25959,7 +26265,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
25959
26265
|
// ../api-routes/src/traffic.ts
|
|
25960
26266
|
import crypto25 from "crypto";
|
|
25961
26267
|
import { Agent as UndiciAgent } from "undici";
|
|
25962
|
-
import { and as
|
|
26268
|
+
import { and as and22, desc as desc15, eq as eq28, gte as gte4, lte as lte3, sql as sql12 } from "drizzle-orm";
|
|
25963
26269
|
|
|
25964
26270
|
// ../integration-cloud-run/src/auth.ts
|
|
25965
26271
|
import crypto24 from "crypto";
|
|
@@ -29161,14 +29467,18 @@ function trimOrNull(value) {
|
|
|
29161
29467
|
function buildEventId2(event) {
|
|
29162
29468
|
return `wordpress:${event.observed_at}:${event.id}`;
|
|
29163
29469
|
}
|
|
29164
|
-
function normalizeWordpressTrafficEvent(event) {
|
|
29470
|
+
function normalizeWordpressTrafficEvent(event, site) {
|
|
29165
29471
|
if (!event.observed_at) return null;
|
|
29166
29472
|
if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
|
|
29167
|
-
|
|
29473
|
+
if (!event.path) return null;
|
|
29474
|
+
const path8 = event.path.trim();
|
|
29168
29475
|
if (!path8) return null;
|
|
29169
29476
|
const queryString = trimOrNull(event.query_string);
|
|
29170
29477
|
const host = trimOrNull(event.host);
|
|
29171
29478
|
const requestUrl = host ? `https://${host}${path8}${queryString ? `?${queryString}` : ""}` : `${path8}${queryString ? `?${queryString}` : ""}`;
|
|
29479
|
+
const labels = {};
|
|
29480
|
+
if (host) labels.host = host;
|
|
29481
|
+
if (site?.anonymous_id) labels.anonymousId = site.anonymous_id;
|
|
29172
29482
|
return {
|
|
29173
29483
|
sourceType: TrafficSourceTypes.wordpress,
|
|
29174
29484
|
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
@@ -29189,7 +29499,7 @@ function normalizeWordpressTrafficEvent(event) {
|
|
|
29189
29499
|
responseSizeBytes: null,
|
|
29190
29500
|
providerResource: {
|
|
29191
29501
|
type: "wordpress_site",
|
|
29192
|
-
labels
|
|
29502
|
+
labels
|
|
29193
29503
|
},
|
|
29194
29504
|
providerLabels: {}
|
|
29195
29505
|
};
|
|
@@ -29294,7 +29604,7 @@ async function listWordpressTrafficEvents(options) {
|
|
|
29294
29604
|
const entries = body.events ?? [];
|
|
29295
29605
|
rawEntryCount += entries.length;
|
|
29296
29606
|
for (const entry of entries) {
|
|
29297
|
-
const normalized = normalizeWordpressTrafficEvent(entry);
|
|
29607
|
+
const normalized = normalizeWordpressTrafficEvent(entry, body.site);
|
|
29298
29608
|
if (normalized) {
|
|
29299
29609
|
events.push(normalized);
|
|
29300
29610
|
} else {
|
|
@@ -29776,8 +30086,8 @@ async function runBackfillTask(options) {
|
|
|
29776
30086
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
29777
30087
|
try {
|
|
29778
30088
|
app.db.transaction((tx) => {
|
|
29779
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
29780
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
30089
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq28(runs.id, runId)).run();
|
|
30090
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
29781
30091
|
});
|
|
29782
30092
|
} catch {
|
|
29783
30093
|
}
|
|
@@ -29792,7 +30102,7 @@ async function runBackfillTask(options) {
|
|
|
29792
30102
|
if (allEvents.length === 0) {
|
|
29793
30103
|
const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
29794
30104
|
try {
|
|
29795
|
-
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(
|
|
30105
|
+
app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: finishedAt2 }).where(eq28(runs.id, runId)).run();
|
|
29796
30106
|
} catch {
|
|
29797
30107
|
}
|
|
29798
30108
|
return;
|
|
@@ -29814,29 +30124,29 @@ async function runBackfillTask(options) {
|
|
|
29814
30124
|
try {
|
|
29815
30125
|
app.db.transaction((tx) => {
|
|
29816
30126
|
tx.delete(crawlerEventsHourly).where(
|
|
29817
|
-
|
|
29818
|
-
|
|
30127
|
+
and22(
|
|
30128
|
+
eq28(crawlerEventsHourly.sourceId, sourceRow.id),
|
|
29819
30129
|
gte4(crawlerEventsHourly.tsHour, windowStartIso),
|
|
29820
30130
|
lte3(crawlerEventsHourly.tsHour, windowEndIso)
|
|
29821
30131
|
)
|
|
29822
30132
|
).run();
|
|
29823
30133
|
tx.delete(aiUserFetchEventsHourly).where(
|
|
29824
|
-
|
|
29825
|
-
|
|
30134
|
+
and22(
|
|
30135
|
+
eq28(aiUserFetchEventsHourly.sourceId, sourceRow.id),
|
|
29826
30136
|
gte4(aiUserFetchEventsHourly.tsHour, windowStartIso),
|
|
29827
30137
|
lte3(aiUserFetchEventsHourly.tsHour, windowEndIso)
|
|
29828
30138
|
)
|
|
29829
30139
|
).run();
|
|
29830
30140
|
tx.delete(aiReferralEventsHourly).where(
|
|
29831
|
-
|
|
29832
|
-
|
|
30141
|
+
and22(
|
|
30142
|
+
eq28(aiReferralEventsHourly.sourceId, sourceRow.id),
|
|
29833
30143
|
gte4(aiReferralEventsHourly.tsHour, windowStartIso),
|
|
29834
30144
|
lte3(aiReferralEventsHourly.tsHour, windowEndIso)
|
|
29835
30145
|
)
|
|
29836
30146
|
).run();
|
|
29837
30147
|
tx.delete(rawEventSamples).where(
|
|
29838
|
-
|
|
29839
|
-
|
|
30148
|
+
and22(
|
|
30149
|
+
eq28(rawEventSamples.sourceId, sourceRow.id),
|
|
29840
30150
|
gte4(rawEventSamples.ts, windowStartIso),
|
|
29841
30151
|
lte3(rawEventSamples.ts, windowEndIso)
|
|
29842
30152
|
)
|
|
@@ -29925,8 +30235,8 @@ async function runBackfillTask(options) {
|
|
|
29925
30235
|
lastError: null,
|
|
29926
30236
|
lastEventIds: newRingBuffer,
|
|
29927
30237
|
updatedAt: finishedAt
|
|
29928
|
-
}).where(
|
|
29929
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
30238
|
+
}).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
30239
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq28(runs.id, runId)).run();
|
|
29930
30240
|
});
|
|
29931
30241
|
} catch (e) {
|
|
29932
30242
|
markFailed(`Backfill rollup write failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -30008,7 +30318,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30008
30318
|
createdAt: existing?.createdAt ?? now,
|
|
30009
30319
|
updatedAt: now
|
|
30010
30320
|
});
|
|
30011
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30321
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes["cloud-run"] && row.status !== TrafficSourceStatuses.archived);
|
|
30012
30322
|
const config = {
|
|
30013
30323
|
gcpProjectId,
|
|
30014
30324
|
serviceName: serviceName ?? null,
|
|
@@ -30024,8 +30334,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30024
30334
|
lastError: null,
|
|
30025
30335
|
configJson: config,
|
|
30026
30336
|
updatedAt: now
|
|
30027
|
-
}).where(
|
|
30028
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30337
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30338
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30029
30339
|
} else {
|
|
30030
30340
|
const newId = crypto25.randomUUID();
|
|
30031
30341
|
app.db.insert(trafficSources).values({
|
|
@@ -30042,7 +30352,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30042
30352
|
createdAt: now,
|
|
30043
30353
|
updatedAt: now
|
|
30044
30354
|
}).run();
|
|
30045
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30355
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30046
30356
|
}
|
|
30047
30357
|
writeAuditLog(app.db, {
|
|
30048
30358
|
projectId: project.id,
|
|
@@ -30094,7 +30404,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30094
30404
|
createdAt: existing?.createdAt ?? now,
|
|
30095
30405
|
updatedAt: now
|
|
30096
30406
|
});
|
|
30097
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30407
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.wordpress && row.status !== TrafficSourceStatuses.archived);
|
|
30098
30408
|
const config = { baseUrl, username };
|
|
30099
30409
|
const fallbackName = displayName ?? `WordPress \xB7 ${new URL(baseUrl).host}`;
|
|
30100
30410
|
let sourceRow;
|
|
@@ -30105,8 +30415,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30105
30415
|
lastError: null,
|
|
30106
30416
|
configJson: config,
|
|
30107
30417
|
updatedAt: now
|
|
30108
|
-
}).where(
|
|
30109
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30418
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30419
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30110
30420
|
} else {
|
|
30111
30421
|
const newId = crypto25.randomUUID();
|
|
30112
30422
|
app.db.insert(trafficSources).values({
|
|
@@ -30123,7 +30433,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30123
30433
|
createdAt: now,
|
|
30124
30434
|
updatedAt: now
|
|
30125
30435
|
}).run();
|
|
30126
|
-
sourceRow = app.db.select().from(trafficSources).where(
|
|
30436
|
+
sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30127
30437
|
}
|
|
30128
30438
|
writeAuditLog(app.db, {
|
|
30129
30439
|
projectId: project.id,
|
|
@@ -30177,7 +30487,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30177
30487
|
createdAt: existing?.createdAt ?? now,
|
|
30178
30488
|
updatedAt: now
|
|
30179
30489
|
});
|
|
30180
|
-
const activeSource = app.db.select().from(trafficSources).where(
|
|
30490
|
+
const activeSource = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).all().find((row) => row.sourceType === TrafficSourceTypes.vercel && row.status !== TrafficSourceStatuses.archived);
|
|
30181
30491
|
const config = { projectId, teamId, environment };
|
|
30182
30492
|
const fallbackName = displayName ?? `Vercel \xB7 ${projectId}`;
|
|
30183
30493
|
const { sourceRow, scheduleCreated } = app.db.transaction((tx) => {
|
|
@@ -30189,8 +30499,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30189
30499
|
lastError: null,
|
|
30190
30500
|
configJson: config,
|
|
30191
30501
|
updatedAt: now
|
|
30192
|
-
}).where(
|
|
30193
|
-
row = tx.select().from(trafficSources).where(
|
|
30502
|
+
}).where(eq28(trafficSources.id, activeSource.id)).run();
|
|
30503
|
+
row = tx.select().from(trafficSources).where(eq28(trafficSources.id, activeSource.id)).get();
|
|
30194
30504
|
} else {
|
|
30195
30505
|
const newId = crypto25.randomUUID();
|
|
30196
30506
|
tx.insert(trafficSources).values({
|
|
@@ -30215,12 +30525,12 @@ async function trafficRoutes(app, opts) {
|
|
|
30215
30525
|
createdAt: now,
|
|
30216
30526
|
updatedAt: now
|
|
30217
30527
|
}).run();
|
|
30218
|
-
row = tx.select().from(trafficSources).where(
|
|
30528
|
+
row = tx.select().from(trafficSources).where(eq28(trafficSources.id, newId)).get();
|
|
30219
30529
|
}
|
|
30220
30530
|
const existingSchedule = tx.select().from(schedules).where(
|
|
30221
|
-
|
|
30222
|
-
|
|
30223
|
-
|
|
30531
|
+
and22(
|
|
30532
|
+
eq28(schedules.projectId, project.id),
|
|
30533
|
+
eq28(schedules.kind, SchedulableRunKinds["traffic-sync"])
|
|
30224
30534
|
)
|
|
30225
30535
|
).get();
|
|
30226
30536
|
let created = false;
|
|
@@ -30269,7 +30579,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30269
30579
|
});
|
|
30270
30580
|
app.post("/projects/:name/traffic/sources/:id/sync", async (request) => {
|
|
30271
30581
|
const project = resolveProject(app.db, request.params.name);
|
|
30272
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
30582
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
30273
30583
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30274
30584
|
throw notFound("Traffic source", request.params.id);
|
|
30275
30585
|
}
|
|
@@ -30295,8 +30605,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30295
30605
|
const markFailed = (msg, errorCode) => {
|
|
30296
30606
|
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30297
30607
|
app.db.transaction((tx) => {
|
|
30298
|
-
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(
|
|
30299
|
-
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(
|
|
30608
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq28(runs.id, runId)).run();
|
|
30609
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
30300
30610
|
});
|
|
30301
30611
|
try {
|
|
30302
30612
|
opts.onTrafficSynced?.({
|
|
@@ -30377,7 +30687,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30377
30687
|
}
|
|
30378
30688
|
const credential = credentialStore.getConnection(project.name);
|
|
30379
30689
|
if (!credential) {
|
|
30380
|
-
app.db.delete(runs).where(
|
|
30690
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30381
30691
|
throw validationError(
|
|
30382
30692
|
`No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
|
|
30383
30693
|
);
|
|
@@ -30426,12 +30736,12 @@ async function trafficRoutes(app, opts) {
|
|
|
30426
30736
|
auditAction = "traffic.vercel.synced";
|
|
30427
30737
|
const credentialStore = opts.vercelTrafficCredentialStore;
|
|
30428
30738
|
if (!credentialStore) {
|
|
30429
|
-
app.db.delete(runs).where(
|
|
30739
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30430
30740
|
throw validationError("Vercel traffic credential storage is not configured for this deployment");
|
|
30431
30741
|
}
|
|
30432
30742
|
const credential = credentialStore.getConnection(project.name);
|
|
30433
30743
|
if (!credential) {
|
|
30434
|
-
app.db.delete(runs).where(
|
|
30744
|
+
app.db.delete(runs).where(eq28(runs.id, runId)).run();
|
|
30435
30745
|
throw validationError(
|
|
30436
30746
|
`No Vercel credential found for project "${project.name}". Run "canonry traffic connect vercel" first.`
|
|
30437
30747
|
);
|
|
@@ -30531,7 +30841,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30531
30841
|
let aiReferralHitsCount = 0;
|
|
30532
30842
|
let unknownHitsCount = 0;
|
|
30533
30843
|
app.db.transaction((tx) => {
|
|
30534
|
-
const latestRow = tx.select().from(trafficSources).where(
|
|
30844
|
+
const latestRow = tx.select().from(trafficSources).where(eq28(trafficSources.id, sourceRow.id)).get();
|
|
30535
30845
|
const previousIds = latestRow.lastEventIds ?? [];
|
|
30536
30846
|
const seenEventIds = new Set(previousIds);
|
|
30537
30847
|
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
@@ -30579,7 +30889,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30579
30889
|
crawlerEventsHourly.status
|
|
30580
30890
|
],
|
|
30581
30891
|
set: {
|
|
30582
|
-
hits:
|
|
30892
|
+
hits: sql12`${crawlerEventsHourly.hits} + ${bucket.hits}`,
|
|
30583
30893
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
30584
30894
|
updatedAt: finishedAt
|
|
30585
30895
|
}
|
|
@@ -30612,7 +30922,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30612
30922
|
aiUserFetchEventsHourly.status
|
|
30613
30923
|
],
|
|
30614
30924
|
set: {
|
|
30615
|
-
hits:
|
|
30925
|
+
hits: sql12`${aiUserFetchEventsHourly.hits} + ${bucket.hits}`,
|
|
30616
30926
|
sampledUserAgent: bucket.sampledUserAgent,
|
|
30617
30927
|
updatedAt: finishedAt
|
|
30618
30928
|
}
|
|
@@ -30647,7 +30957,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30647
30957
|
aiReferralEventsHourly.status
|
|
30648
30958
|
],
|
|
30649
30959
|
set: {
|
|
30650
|
-
sessionsOrHits:
|
|
30960
|
+
sessionsOrHits: sql12`${aiReferralEventsHourly.sessionsOrHits} + ${bucket.hits}`,
|
|
30651
30961
|
updatedAt: finishedAt
|
|
30652
30962
|
}
|
|
30653
30963
|
}).run();
|
|
@@ -30699,8 +31009,8 @@ async function trafficRoutes(app, opts) {
|
|
|
30699
31009
|
if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
|
|
30700
31010
|
sourceUpdate.lastCursor = nextCursor ?? null;
|
|
30701
31011
|
}
|
|
30702
|
-
tx.update(trafficSources).set(sourceUpdate).where(
|
|
30703
|
-
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(
|
|
31012
|
+
tx.update(trafficSources).set(sourceUpdate).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
31013
|
+
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq28(runs.id, runId)).run();
|
|
30704
31014
|
});
|
|
30705
31015
|
writeAuditLog(app.db, {
|
|
30706
31016
|
projectId: project.id,
|
|
@@ -30752,7 +31062,7 @@ async function trafficRoutes(app, opts) {
|
|
|
30752
31062
|
});
|
|
30753
31063
|
app.post("/projects/:name/traffic/sources/:id/backfill", async (request) => {
|
|
30754
31064
|
const project = resolveProject(app.db, request.params.name);
|
|
30755
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31065
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
30756
31066
|
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
30757
31067
|
throw notFound("Traffic source", request.params.id);
|
|
30758
31068
|
}
|
|
@@ -30938,10 +31248,10 @@ async function trafficRoutes(app, opts) {
|
|
|
30938
31248
|
function buildSourceDetail(projectId, row, since) {
|
|
30939
31249
|
const crawlerPathRows = app.db.select({
|
|
30940
31250
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
30941
|
-
hits:
|
|
31251
|
+
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
30942
31252
|
}).from(crawlerEventsHourly).where(
|
|
30943
|
-
|
|
30944
|
-
|
|
31253
|
+
and22(
|
|
31254
|
+
eq28(crawlerEventsHourly.sourceId, row.id),
|
|
30945
31255
|
gte4(crawlerEventsHourly.tsHour, since)
|
|
30946
31256
|
)
|
|
30947
31257
|
).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
@@ -30949,29 +31259,29 @@ async function trafficRoutes(app, opts) {
|
|
|
30949
31259
|
crawlerPathRows.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
30950
31260
|
);
|
|
30951
31261
|
const crawlerTotal = crawlerSegments.content + crawlerSegments.sitemap + crawlerSegments.robots + crawlerSegments.asset + crawlerSegments.other;
|
|
30952
|
-
const aiUserFetchTotals = app.db.select({ total:
|
|
30953
|
-
|
|
30954
|
-
|
|
31262
|
+
const aiUserFetchTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
31263
|
+
and22(
|
|
31264
|
+
eq28(aiUserFetchEventsHourly.sourceId, row.id),
|
|
30955
31265
|
gte4(aiUserFetchEventsHourly.tsHour, since)
|
|
30956
31266
|
)
|
|
30957
31267
|
).get();
|
|
30958
|
-
const aiTotals = app.db.select({ total:
|
|
30959
|
-
|
|
30960
|
-
|
|
31268
|
+
const aiTotals = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
31269
|
+
and22(
|
|
31270
|
+
eq28(aiReferralEventsHourly.sourceId, row.id),
|
|
30961
31271
|
gte4(aiReferralEventsHourly.tsHour, since)
|
|
30962
31272
|
)
|
|
30963
31273
|
).get();
|
|
30964
|
-
const sampleTotals = app.db.select({ total:
|
|
30965
|
-
|
|
30966
|
-
|
|
31274
|
+
const sampleTotals = app.db.select({ total: sql12`COUNT(*)` }).from(rawEventSamples).where(
|
|
31275
|
+
and22(
|
|
31276
|
+
eq28(rawEventSamples.sourceId, row.id),
|
|
30967
31277
|
gte4(rawEventSamples.ts, since)
|
|
30968
31278
|
)
|
|
30969
31279
|
).get();
|
|
30970
31280
|
const latestRun = app.db.select().from(runs).where(
|
|
30971
|
-
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
31281
|
+
and22(
|
|
31282
|
+
eq28(runs.projectId, projectId),
|
|
31283
|
+
eq28(runs.kind, RunKinds["traffic-sync"]),
|
|
31284
|
+
eq28(runs.sourceId, row.id)
|
|
30975
31285
|
)
|
|
30976
31286
|
).orderBy(desc15(runs.startedAt)).limit(1).get();
|
|
30977
31287
|
return {
|
|
@@ -31002,7 +31312,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31002
31312
|
"`advanceToNow` must be `true`. There is no implicit reset."
|
|
31003
31313
|
);
|
|
31004
31314
|
}
|
|
31005
|
-
const sourceRow = app.db.select().from(trafficSources).where(
|
|
31315
|
+
const sourceRow = app.db.select().from(trafficSources).where(and22(eq28(trafficSources.projectId, project.id), eq28(trafficSources.id, request.params.id))).get();
|
|
31006
31316
|
if (!sourceRow) {
|
|
31007
31317
|
throw notFound("traffic source", request.params.id);
|
|
31008
31318
|
}
|
|
@@ -31019,7 +31329,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31019
31329
|
status: TrafficSourceStatuses.connected,
|
|
31020
31330
|
lastError: null,
|
|
31021
31331
|
updatedAt: now
|
|
31022
|
-
}).where(
|
|
31332
|
+
}).where(eq28(trafficSources.id, sourceRow.id)).run();
|
|
31023
31333
|
writeAuditLog(tx, auditFromRequest(request, {
|
|
31024
31334
|
projectId: project.id,
|
|
31025
31335
|
actor: "api",
|
|
@@ -31027,20 +31337,20 @@ async function trafficRoutes(app, opts) {
|
|
|
31027
31337
|
entityType: "traffic_source",
|
|
31028
31338
|
entityId: sourceRow.id
|
|
31029
31339
|
}));
|
|
31030
|
-
updatedRow = tx.select().from(trafficSources).where(
|
|
31340
|
+
updatedRow = tx.select().from(trafficSources).where(eq28(trafficSources.id, sourceRow.id)).get();
|
|
31031
31341
|
});
|
|
31032
31342
|
return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
|
|
31033
31343
|
});
|
|
31034
31344
|
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
31035
31345
|
const project = resolveProject(app.db, request.params.name);
|
|
31036
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
31346
|
+
const rows = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
31037
31347
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
31038
31348
|
const response = { sources };
|
|
31039
31349
|
return response;
|
|
31040
31350
|
});
|
|
31041
31351
|
app.get("/projects/:name/traffic/status", async (request) => {
|
|
31042
31352
|
const project = resolveProject(app.db, request.params.name);
|
|
31043
|
-
const rows = app.db.select().from(trafficSources).where(
|
|
31353
|
+
const rows = app.db.select().from(trafficSources).where(eq28(trafficSources.projectId, project.id)).orderBy(desc15(trafficSources.createdAt)).all();
|
|
31044
31354
|
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
31045
31355
|
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
31046
31356
|
const response = { sources };
|
|
@@ -31050,7 +31360,7 @@ async function trafficRoutes(app, opts) {
|
|
|
31050
31360
|
"/projects/:name/traffic/sources/:id",
|
|
31051
31361
|
async (request) => {
|
|
31052
31362
|
const project = resolveProject(app.db, request.params.name);
|
|
31053
|
-
const row = app.db.select().from(trafficSources).where(
|
|
31363
|
+
const row = app.db.select().from(trafficSources).where(eq28(trafficSources.id, request.params.id)).get();
|
|
31054
31364
|
if (!row || row.projectId !== project.id) {
|
|
31055
31365
|
throw notFound("Traffic source", request.params.id);
|
|
31056
31366
|
}
|
|
@@ -31102,15 +31412,15 @@ async function trafficRoutes(app, opts) {
|
|
|
31102
31412
|
let aiReferralTotal = 0;
|
|
31103
31413
|
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
31104
31414
|
const crawlerFilters = [
|
|
31105
|
-
|
|
31415
|
+
eq28(crawlerEventsHourly.projectId, project.id),
|
|
31106
31416
|
gte4(crawlerEventsHourly.tsHour, sinceIso),
|
|
31107
31417
|
lte3(crawlerEventsHourly.tsHour, untilIso)
|
|
31108
31418
|
];
|
|
31109
|
-
if (sourceIdParam) crawlerFilters.push(
|
|
31110
|
-
const crawlerWhere =
|
|
31419
|
+
if (sourceIdParam) crawlerFilters.push(eq28(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
31420
|
+
const crawlerWhere = and22(...crawlerFilters);
|
|
31111
31421
|
const pathTotals = app.db.select({
|
|
31112
31422
|
pathNormalized: crawlerEventsHourly.pathNormalized,
|
|
31113
|
-
hits:
|
|
31423
|
+
hits: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)`
|
|
31114
31424
|
}).from(crawlerEventsHourly).where(crawlerWhere).groupBy(crawlerEventsHourly.pathNormalized).all();
|
|
31115
31425
|
crawlerSegments = segmentCrawlerHits(
|
|
31116
31426
|
pathTotals.map((r) => ({ pathNormalized: r.pathNormalized, hits: Number(r.hits) }))
|
|
@@ -31134,13 +31444,13 @@ async function trafficRoutes(app, opts) {
|
|
|
31134
31444
|
}
|
|
31135
31445
|
if (kind === "all" || kind === TrafficEventKinds["ai-user-fetch"]) {
|
|
31136
31446
|
const userFetchFilters = [
|
|
31137
|
-
|
|
31447
|
+
eq28(aiUserFetchEventsHourly.projectId, project.id),
|
|
31138
31448
|
gte4(aiUserFetchEventsHourly.tsHour, sinceIso),
|
|
31139
31449
|
lte3(aiUserFetchEventsHourly.tsHour, untilIso)
|
|
31140
31450
|
];
|
|
31141
|
-
if (sourceIdParam) userFetchFilters.push(
|
|
31142
|
-
const userFetchWhere =
|
|
31143
|
-
const total = app.db.select({ total:
|
|
31451
|
+
if (sourceIdParam) userFetchFilters.push(eq28(aiUserFetchEventsHourly.sourceId, sourceIdParam));
|
|
31452
|
+
const userFetchWhere = and22(...userFetchFilters);
|
|
31453
|
+
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(userFetchWhere).get();
|
|
31144
31454
|
aiUserFetchTotal = Number(total?.total ?? 0);
|
|
31145
31455
|
const rows = app.db.select().from(aiUserFetchEventsHourly).where(userFetchWhere).orderBy(desc15(aiUserFetchEventsHourly.tsHour)).limit(limit).all();
|
|
31146
31456
|
for (const r of rows) {
|
|
@@ -31159,13 +31469,13 @@ async function trafficRoutes(app, opts) {
|
|
|
31159
31469
|
}
|
|
31160
31470
|
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
31161
31471
|
const aiFilters = [
|
|
31162
|
-
|
|
31472
|
+
eq28(aiReferralEventsHourly.projectId, project.id),
|
|
31163
31473
|
gte4(aiReferralEventsHourly.tsHour, sinceIso),
|
|
31164
31474
|
lte3(aiReferralEventsHourly.tsHour, untilIso)
|
|
31165
31475
|
];
|
|
31166
|
-
if (sourceIdParam) aiFilters.push(
|
|
31167
|
-
const aiWhere =
|
|
31168
|
-
const total = app.db.select({ total:
|
|
31476
|
+
if (sourceIdParam) aiFilters.push(eq28(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
31477
|
+
const aiWhere = and22(...aiFilters);
|
|
31478
|
+
const total = app.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
31169
31479
|
aiReferralTotal = Number(total?.total ?? 0);
|
|
31170
31480
|
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc15(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
31171
31481
|
for (const r of rows) {
|
|
@@ -31371,7 +31681,7 @@ function readInstalledManifest(skillDir) {
|
|
|
31371
31681
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
|
|
31372
31682
|
|
|
31373
31683
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
31374
|
-
import { and as
|
|
31684
|
+
import { and as and23, eq as eq29 } from "drizzle-orm";
|
|
31375
31685
|
function skippedNoProject() {
|
|
31376
31686
|
return {
|
|
31377
31687
|
status: CheckStatuses.skipped,
|
|
@@ -31387,8 +31697,8 @@ var BACKLINKS_CHECKS = [
|
|
|
31387
31697
|
title: "Backlinks source connected",
|
|
31388
31698
|
run: (ctx) => {
|
|
31389
31699
|
if (!ctx.project) return skippedNoProject();
|
|
31390
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
31391
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
31700
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq29(projects.id, ctx.project.id)).get();
|
|
31701
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
31392
31702
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
31393
31703
|
const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
|
|
31394
31704
|
const connected = [];
|
|
@@ -31403,9 +31713,9 @@ var BACKLINKS_CHECKS = [
|
|
|
31403
31713
|
details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
|
|
31404
31714
|
};
|
|
31405
31715
|
}
|
|
31406
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
31407
|
-
|
|
31408
|
-
|
|
31716
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and23(
|
|
31717
|
+
eq29(backlinkSummaries.projectId, ctx.project.id),
|
|
31718
|
+
eq29(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
31409
31719
|
)).limit(1).get() : false;
|
|
31410
31720
|
return {
|
|
31411
31721
|
status: CheckStatuses.ok,
|
|
@@ -31565,7 +31875,7 @@ var BING_AUTH_CHECKS = [
|
|
|
31565
31875
|
];
|
|
31566
31876
|
|
|
31567
31877
|
// ../api-routes/src/doctor/checks/content.ts
|
|
31568
|
-
import { eq as
|
|
31878
|
+
import { eq as eq30 } from "drizzle-orm";
|
|
31569
31879
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
31570
31880
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
31571
31881
|
function skippedNoProject2() {
|
|
@@ -31578,7 +31888,7 @@ function skippedNoProject2() {
|
|
|
31578
31888
|
}
|
|
31579
31889
|
function loadProject(ctx) {
|
|
31580
31890
|
if (!ctx.project) return null;
|
|
31581
|
-
return ctx.db.select().from(projects).where(
|
|
31891
|
+
return ctx.db.select().from(projects).where(eq30(projects.id, ctx.project.id)).get() ?? null;
|
|
31582
31892
|
}
|
|
31583
31893
|
function percent(value) {
|
|
31584
31894
|
return Math.round(value * 100);
|
|
@@ -31670,7 +31980,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
31670
31980
|
);
|
|
31671
31981
|
|
|
31672
31982
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
31673
|
-
import { eq as
|
|
31983
|
+
import { eq as eq31 } from "drizzle-orm";
|
|
31674
31984
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
31675
31985
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
31676
31986
|
var adsConnectionCheck = {
|
|
@@ -31687,7 +31997,7 @@ var adsConnectionCheck = {
|
|
|
31687
31997
|
remediation: null
|
|
31688
31998
|
};
|
|
31689
31999
|
}
|
|
31690
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
32000
|
+
const row = ctx.db.select().from(adsConnections).where(eq31(adsConnections.projectId, ctx.project.id)).get();
|
|
31691
32001
|
if (!row) {
|
|
31692
32002
|
return {
|
|
31693
32003
|
status: CheckStatuses.skipped,
|
|
@@ -31737,7 +32047,7 @@ var adsRecentSyncCheck = {
|
|
|
31737
32047
|
remediation: null
|
|
31738
32048
|
};
|
|
31739
32049
|
}
|
|
31740
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
32050
|
+
const row = ctx.db.select().from(adsConnections).where(eq31(adsConnections.projectId, ctx.project.id)).get();
|
|
31741
32051
|
if (!row) {
|
|
31742
32052
|
return {
|
|
31743
32053
|
status: CheckStatuses.skipped,
|
|
@@ -31928,7 +32238,7 @@ var ga4ConnectionCheck = {
|
|
|
31928
32238
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
31929
32239
|
|
|
31930
32240
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
31931
|
-
import { and as
|
|
32241
|
+
import { and as and24, eq as eq32 } from "drizzle-orm";
|
|
31932
32242
|
var RECENT_SYNC_WARN_DAYS2 = 7;
|
|
31933
32243
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
31934
32244
|
function skippedNoProject3() {
|
|
@@ -32161,7 +32471,7 @@ var recentSyncCheck = {
|
|
|
32161
32471
|
title: "GBP recent sync",
|
|
32162
32472
|
run: (ctx) => {
|
|
32163
32473
|
if (!ctx.project) return skippedNoProject3();
|
|
32164
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
32474
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and24(eq32(gbpLocations.projectId, ctx.project.id), eq32(gbpLocations.selected, true))).all();
|
|
32165
32475
|
if (selected.length === 0) {
|
|
32166
32476
|
return {
|
|
32167
32477
|
status: CheckStatuses.skipped,
|
|
@@ -32221,7 +32531,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
32221
32531
|
);
|
|
32222
32532
|
|
|
32223
32533
|
// ../api-routes/src/doctor/checks/places.ts
|
|
32224
|
-
import { eq as
|
|
32534
|
+
import { eq as eq33 } from "drizzle-orm";
|
|
32225
32535
|
var apiKeyCheck = {
|
|
32226
32536
|
id: "gbp.places.api-key",
|
|
32227
32537
|
category: CheckCategories.auth,
|
|
@@ -32266,7 +32576,7 @@ var apiKeyCheck = {
|
|
|
32266
32576
|
details: { tier: cfg.tier }
|
|
32267
32577
|
};
|
|
32268
32578
|
}
|
|
32269
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
32579
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq33(gbpLocations.projectId, ctx.project.id)).all();
|
|
32270
32580
|
const selected = rows.filter((r) => r.selected);
|
|
32271
32581
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
32272
32582
|
const details = {
|
|
@@ -32763,7 +33073,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
32763
33073
|
];
|
|
32764
33074
|
|
|
32765
33075
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
32766
|
-
import { and as
|
|
33076
|
+
import { and as and25, eq as eq34, gte as gte5, ne as ne4, sql as sql13 } from "drizzle-orm";
|
|
32767
33077
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
32768
33078
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
32769
33079
|
function skippedNoProject5() {
|
|
@@ -32777,8 +33087,8 @@ function skippedNoProject5() {
|
|
|
32777
33087
|
function loadProbes(ctx) {
|
|
32778
33088
|
if (!ctx.project) return [];
|
|
32779
33089
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
32780
|
-
|
|
32781
|
-
|
|
33090
|
+
and25(
|
|
33091
|
+
eq34(trafficSources.projectId, ctx.project.id),
|
|
32782
33092
|
ne4(trafficSources.status, TrafficSourceStatuses.archived)
|
|
32783
33093
|
)
|
|
32784
33094
|
).all();
|
|
@@ -32857,17 +33167,17 @@ var recentDataCheck = {
|
|
|
32857
33167
|
const warnCutoff = new Date(now.getTime() - RECENT_DATA_WARN_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32858
33168
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
32859
33169
|
const recentCrawlers = Number(
|
|
32860
|
-
ctx.db.select({ total:
|
|
32861
|
-
|
|
32862
|
-
|
|
33170
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33171
|
+
and25(
|
|
33172
|
+
eq34(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32863
33173
|
gte5(crawlerEventsHourly.tsHour, warnCutoff)
|
|
32864
33174
|
)
|
|
32865
33175
|
).get()?.total ?? 0
|
|
32866
33176
|
);
|
|
32867
33177
|
const recentReferrals = Number(
|
|
32868
|
-
ctx.db.select({ total:
|
|
32869
|
-
|
|
32870
|
-
|
|
33178
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33179
|
+
and25(
|
|
33180
|
+
eq34(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32871
33181
|
gte5(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
32872
33182
|
)
|
|
32873
33183
|
).get()?.total ?? 0
|
|
@@ -32881,17 +33191,17 @@ var recentDataCheck = {
|
|
|
32881
33191
|
};
|
|
32882
33192
|
}
|
|
32883
33193
|
const olderCrawlers = Number(
|
|
32884
|
-
ctx.db.select({ total:
|
|
32885
|
-
|
|
32886
|
-
|
|
33194
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
33195
|
+
and25(
|
|
33196
|
+
eq34(crawlerEventsHourly.projectId, ctx.project.id),
|
|
32887
33197
|
gte5(crawlerEventsHourly.tsHour, failCutoff)
|
|
32888
33198
|
)
|
|
32889
33199
|
).get()?.total ?? 0
|
|
32890
33200
|
);
|
|
32891
33201
|
const olderReferrals = Number(
|
|
32892
|
-
ctx.db.select({ total:
|
|
32893
|
-
|
|
32894
|
-
|
|
33202
|
+
ctx.db.select({ total: sql13`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
33203
|
+
and25(
|
|
33204
|
+
eq34(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
32895
33205
|
gte5(aiReferralEventsHourly.tsHour, failCutoff)
|
|
32896
33206
|
)
|
|
32897
33207
|
).get()?.total ?? 0
|
|
@@ -33307,7 +33617,7 @@ async function doctorRoutes(app, opts) {
|
|
|
33307
33617
|
|
|
33308
33618
|
// ../api-routes/src/discovery/routes.ts
|
|
33309
33619
|
import crypto27 from "crypto";
|
|
33310
|
-
import { and as
|
|
33620
|
+
import { and as and26, desc as desc16, eq as eq35, gte as gte6, inArray as inArray12 } from "drizzle-orm";
|
|
33311
33621
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
33312
33622
|
async function discoveryRoutes(app, opts) {
|
|
33313
33623
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -33339,9 +33649,9 @@ async function discoveryRoutes(app, opts) {
|
|
|
33339
33649
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
33340
33650
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
33341
33651
|
const decision = app.db.transaction((tx) => {
|
|
33342
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
33343
|
-
|
|
33344
|
-
|
|
33652
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and26(
|
|
33653
|
+
eq35(discoverySessions.projectId, project.id),
|
|
33654
|
+
eq35(discoverySessions.icpDescription, icpDescription),
|
|
33345
33655
|
inArray12(discoverySessions.status, [
|
|
33346
33656
|
DiscoverySessionStatuses.queued,
|
|
33347
33657
|
DiscoverySessionStatuses.seeding,
|
|
@@ -33411,7 +33721,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33411
33721
|
const project = resolveProject(app.db, request.params.name);
|
|
33412
33722
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
33413
33723
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
33414
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
33724
|
+
const rows = app.db.select().from(discoverySessions).where(eq35(discoverySessions.projectId, project.id)).orderBy(desc16(discoverySessions.createdAt)).limit(limit).all();
|
|
33415
33725
|
return reply.send(rows.map(serializeSession));
|
|
33416
33726
|
}
|
|
33417
33727
|
);
|
|
@@ -33419,11 +33729,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
33419
33729
|
"/projects/:name/discover/sessions/:id",
|
|
33420
33730
|
async (request, reply) => {
|
|
33421
33731
|
const project = resolveProject(app.db, request.params.name);
|
|
33422
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33732
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33423
33733
|
if (!session || session.projectId !== project.id) {
|
|
33424
33734
|
throw notFound("Discovery session", request.params.id);
|
|
33425
33735
|
}
|
|
33426
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33736
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33427
33737
|
const detail = {
|
|
33428
33738
|
...serializeSession(session),
|
|
33429
33739
|
probes: probeRows.map(serializeProbe)
|
|
@@ -33435,7 +33745,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33435
33745
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
33436
33746
|
async (request, reply) => {
|
|
33437
33747
|
const project = resolveProject(app.db, request.params.name);
|
|
33438
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33748
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33439
33749
|
if (!session || session.projectId !== project.id) {
|
|
33440
33750
|
throw notFound("Discovery session", request.params.id);
|
|
33441
33751
|
}
|
|
@@ -33443,7 +33753,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33443
33753
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
33444
33754
|
const applyAnchor = request.query.anchor !== "false";
|
|
33445
33755
|
const provider = session.seedProvider ?? "gemini";
|
|
33446
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33756
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33447
33757
|
const extract = opts.harvestSearchQueries;
|
|
33448
33758
|
const probesWithQueries = probeRows.map((row) => {
|
|
33449
33759
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -33454,7 +33764,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33454
33764
|
return { searchQueries: [] };
|
|
33455
33765
|
}
|
|
33456
33766
|
});
|
|
33457
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
33767
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq35(queries.projectId, project.id)).all().map((r) => r.query);
|
|
33458
33768
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
33459
33769
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
33460
33770
|
effectiveDomains(project)
|
|
@@ -33501,12 +33811,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
33501
33811
|
"/projects/:name/discover/sessions/:id/promote",
|
|
33502
33812
|
async (request, reply) => {
|
|
33503
33813
|
const project = resolveProject(app.db, request.params.name);
|
|
33504
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33814
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33505
33815
|
if (!session || session.projectId !== project.id) {
|
|
33506
33816
|
throw notFound("Discovery session", request.params.id);
|
|
33507
33817
|
}
|
|
33508
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33509
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33818
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33819
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
33510
33820
|
const seenCompetitors = new Set(existingCompetitors);
|
|
33511
33821
|
const cited = /* @__PURE__ */ new Set();
|
|
33512
33822
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -33535,7 +33845,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33535
33845
|
);
|
|
33536
33846
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
33537
33847
|
const project = resolveProject(app.db, request.params.name);
|
|
33538
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
33848
|
+
const session = app.db.select().from(discoverySessions).where(eq35(discoverySessions.id, request.params.id)).get();
|
|
33539
33849
|
if (!session || session.projectId !== project.id) {
|
|
33540
33850
|
throw notFound("Discovery session", request.params.id);
|
|
33541
33851
|
}
|
|
@@ -33558,7 +33868,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33558
33868
|
const bucketSet = new Set(buckets);
|
|
33559
33869
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
33560
33870
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
33561
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
33871
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq35(discoveryProbes.sessionId, session.id)).all();
|
|
33562
33872
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
33563
33873
|
for (const probe of probeRows) {
|
|
33564
33874
|
if (!probe.bucket) continue;
|
|
@@ -33566,7 +33876,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33566
33876
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
33567
33877
|
}
|
|
33568
33878
|
const existingQueries = new Set(
|
|
33569
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
33879
|
+
app.db.select({ query: queries.query }).from(queries).where(eq35(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
33570
33880
|
);
|
|
33571
33881
|
const promotedQueries = [];
|
|
33572
33882
|
const skippedQueries = [];
|
|
@@ -33582,7 +33892,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
33582
33892
|
const skippedCompetitors = [];
|
|
33583
33893
|
if (includeCompetitors) {
|
|
33584
33894
|
const existingCompetitors = new Set(
|
|
33585
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
33895
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
33586
33896
|
);
|
|
33587
33897
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
33588
33898
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -33689,7 +33999,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
33689
33999
|
|
|
33690
34000
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
33691
34001
|
import crypto28 from "crypto";
|
|
33692
|
-
import { eq as
|
|
34002
|
+
import { eq as eq36 } from "drizzle-orm";
|
|
33693
34003
|
var DEFAULT_MAX_PROBES = 100;
|
|
33694
34004
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
33695
34005
|
function classifyProbeBucket(input) {
|
|
@@ -33743,7 +34053,7 @@ async function executeDiscovery(opts) {
|
|
|
33743
34053
|
status: DiscoverySessionStatuses.seeding,
|
|
33744
34054
|
dedupThreshold,
|
|
33745
34055
|
startedAt
|
|
33746
|
-
}).where(
|
|
34056
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33747
34057
|
const seedResult = await opts.deps.seed({
|
|
33748
34058
|
project: opts.project,
|
|
33749
34059
|
icpDescription: opts.icpDescription,
|
|
@@ -33769,7 +34079,7 @@ async function executeDiscovery(opts) {
|
|
|
33769
34079
|
seedCountRaw,
|
|
33770
34080
|
seedCount,
|
|
33771
34081
|
warning
|
|
33772
|
-
}).where(
|
|
34082
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33773
34083
|
const probeRows = [];
|
|
33774
34084
|
const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
|
|
33775
34085
|
for (const query of probedCanonicals) {
|
|
@@ -33810,7 +34120,7 @@ async function executeDiscovery(opts) {
|
|
|
33810
34120
|
wastedCount: buckets["wasted-surface"],
|
|
33811
34121
|
competitorMap,
|
|
33812
34122
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33813
|
-
}).where(
|
|
34123
|
+
}).where(eq36(discoverySessions.id, opts.sessionId)).run();
|
|
33814
34124
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
33815
34125
|
return {
|
|
33816
34126
|
buckets,
|
|
@@ -33850,7 +34160,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
33850
34160
|
status: DiscoverySessionStatuses.failed,
|
|
33851
34161
|
error,
|
|
33852
34162
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
33853
|
-
}).where(
|
|
34163
|
+
}).where(eq36(discoverySessions.id, sessionId)).run();
|
|
33854
34164
|
}
|
|
33855
34165
|
function dedupeStrings(input) {
|
|
33856
34166
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -33868,7 +34178,7 @@ function dedupeStrings(input) {
|
|
|
33868
34178
|
|
|
33869
34179
|
// ../api-routes/src/technical-aeo.ts
|
|
33870
34180
|
import crypto29 from "crypto";
|
|
33871
|
-
import { and as
|
|
34181
|
+
import { and as and27, asc as asc5, count, desc as desc17, eq as eq37, inArray as inArray13 } from "drizzle-orm";
|
|
33872
34182
|
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
33873
34183
|
function emptyScore(projectName) {
|
|
33874
34184
|
return {
|
|
@@ -33900,9 +34210,9 @@ function parsePositiveInt(value, fallback, max) {
|
|
|
33900
34210
|
async function technicalAeoRoutes(app, opts) {
|
|
33901
34211
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
33902
34212
|
const project = resolveProject(app.db, request.params.name);
|
|
33903
|
-
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33904
|
-
|
|
33905
|
-
|
|
34213
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34214
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34215
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
33906
34216
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33907
34217
|
notProbeRun()
|
|
33908
34218
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
@@ -33935,9 +34245,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33935
34245
|
});
|
|
33936
34246
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
33937
34247
|
const project = resolveProject(app.db, request.params.name);
|
|
33938
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
33939
|
-
|
|
33940
|
-
|
|
34248
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34249
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34250
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
33941
34251
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33942
34252
|
notProbeRun()
|
|
33943
34253
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
@@ -33945,14 +34255,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33945
34255
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
33946
34256
|
}
|
|
33947
34257
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
33948
|
-
const conds = [
|
|
33949
|
-
if (statusFilter) conds.push(
|
|
33950
|
-
const where =
|
|
34258
|
+
const conds = [eq37(siteAuditPages.runId, latest.runId)];
|
|
34259
|
+
if (statusFilter) conds.push(eq37(siteAuditPages.status, statusFilter));
|
|
34260
|
+
const where = and27(...conds);
|
|
33951
34261
|
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
33952
34262
|
const total = totalRow?.value ?? 0;
|
|
33953
34263
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
33954
34264
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
33955
|
-
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ?
|
|
34265
|
+
const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc5(siteAuditPages.url) : asc5(siteAuditPages.overallScore);
|
|
33956
34266
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
33957
34267
|
const pages = rows.map((row) => ({
|
|
33958
34268
|
url: row.url,
|
|
@@ -33971,9 +34281,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33971
34281
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
33972
34282
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
33973
34283
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
33974
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
33975
|
-
|
|
33976
|
-
|
|
34284
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq37(siteAuditSnapshots.runId, runs.id)).where(and27(
|
|
34285
|
+
eq37(siteAuditSnapshots.projectId, project.id),
|
|
34286
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
33977
34287
|
inArray13(runs.status, SURFACEABLE_STATUSES),
|
|
33978
34288
|
notProbeRun()
|
|
33979
34289
|
)).orderBy(desc17(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
@@ -33985,9 +34295,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
33985
34295
|
if (!parsed.success) {
|
|
33986
34296
|
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
33987
34297
|
}
|
|
33988
|
-
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
33989
|
-
|
|
33990
|
-
|
|
34298
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and27(
|
|
34299
|
+
eq37(runs.projectId, project.id),
|
|
34300
|
+
eq37(runs.kind, RunKinds["site-audit"]),
|
|
33991
34301
|
inArray13(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
33992
34302
|
)).get();
|
|
33993
34303
|
if (existing) {
|
|
@@ -34578,9 +34888,9 @@ var IntelligenceService = class {
|
|
|
34578
34888
|
*/
|
|
34579
34889
|
analyzeAndPersist(runId, projectId) {
|
|
34580
34890
|
const recentRuns = this.db.select().from(runs).where(
|
|
34581
|
-
|
|
34582
|
-
|
|
34583
|
-
or5(
|
|
34891
|
+
and28(
|
|
34892
|
+
eq38(runs.projectId, projectId),
|
|
34893
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
34584
34894
|
// Defensive: RunCoordinator already skips probes before this is
|
|
34585
34895
|
// called, but if a future call site invokes analyzeAndPersist
|
|
34586
34896
|
// directly for a probe, probes still must not pollute the
|
|
@@ -34662,7 +34972,7 @@ var IntelligenceService = class {
|
|
|
34662
34972
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
34663
34973
|
*/
|
|
34664
34974
|
analyzeAndPersistGbp(runId, projectId) {
|
|
34665
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
34975
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq38(runs.id, runId)).get();
|
|
34666
34976
|
if (!runRow) {
|
|
34667
34977
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
34668
34978
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -34670,9 +34980,9 @@ var IntelligenceService = class {
|
|
|
34670
34980
|
}
|
|
34671
34981
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
34672
34982
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
34673
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
34674
|
-
|
|
34675
|
-
|
|
34983
|
+
const selected = this.db.select().from(gbpLocations).where(and28(
|
|
34984
|
+
eq38(gbpLocations.projectId, projectId),
|
|
34985
|
+
eq38(gbpLocations.selected, true),
|
|
34676
34986
|
gte7(gbpLocations.syncedAt, windowStart),
|
|
34677
34987
|
lte4(gbpLocations.syncedAt, windowEnd)
|
|
34678
34988
|
)).all();
|
|
@@ -34707,12 +35017,12 @@ var IntelligenceService = class {
|
|
|
34707
35017
|
}
|
|
34708
35018
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
34709
35019
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
34710
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
34711
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
34712
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
34713
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
35020
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and28(eq38(gbpDailyMetrics.projectId, projectId), eq38(gbpDailyMetrics.locationName, locationName))).all();
|
|
35021
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and28(eq38(gbpPlaceActions.projectId, projectId), eq38(gbpPlaceActions.locationName, locationName))).all();
|
|
35022
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and28(eq38(gbpLodgingSnapshots.projectId, projectId), eq38(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc18(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
35023
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and28(eq38(gbpLocations.projectId, projectId), eq38(gbpLocations.locationName, locationName))).get();
|
|
34714
35024
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
34715
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
35025
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and28(eq38(gbpPlaceDetails.projectId, projectId), eq38(gbpPlaceDetails.locationName, locationName))).orderBy(desc18(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
34716
35026
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
34717
35027
|
const summary = buildGbpSummary({
|
|
34718
35028
|
locationName,
|
|
@@ -34748,7 +35058,7 @@ var IntelligenceService = class {
|
|
|
34748
35058
|
/** Build the month-over-month keyword series for a location from the
|
|
34749
35059
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
34750
35060
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
34751
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
35061
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and28(eq38(gbpKeywordMonthly.projectId, projectId), eq38(gbpKeywordMonthly.locationName, locationName))).all();
|
|
34752
35062
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
34753
35063
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
34754
35064
|
const recentMonth = months[0] ?? null;
|
|
@@ -34779,7 +35089,7 @@ var IntelligenceService = class {
|
|
|
34779
35089
|
*/
|
|
34780
35090
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
34781
35091
|
const covered = new Set(coveredLocationNames);
|
|
34782
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
35092
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and28(eq38(insights.projectId, projectId), eq38(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
34783
35093
|
const staleIds = [];
|
|
34784
35094
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
34785
35095
|
for (const row of existing) {
|
|
@@ -34790,7 +35100,7 @@ var IntelligenceService = class {
|
|
|
34790
35100
|
}
|
|
34791
35101
|
this.db.transaction((tx) => {
|
|
34792
35102
|
for (const id of staleIds) {
|
|
34793
|
-
tx.delete(insights).where(
|
|
35103
|
+
tx.delete(insights).where(eq38(insights.id, id)).run();
|
|
34794
35104
|
}
|
|
34795
35105
|
for (const insight of gbpInsights) {
|
|
34796
35106
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -34868,7 +35178,7 @@ var IntelligenceService = class {
|
|
|
34868
35178
|
* create per run + aggregate). DB is left untouched.
|
|
34869
35179
|
*/
|
|
34870
35180
|
backfill(projectName, opts, onProgress) {
|
|
34871
|
-
const project = this.db.select().from(projects).where(
|
|
35181
|
+
const project = this.db.select().from(projects).where(eq38(projects.name, projectName)).get();
|
|
34872
35182
|
if (!project) {
|
|
34873
35183
|
throw new Error(`Project "${projectName}" not found`);
|
|
34874
35184
|
}
|
|
@@ -34881,13 +35191,13 @@ var IntelligenceService = class {
|
|
|
34881
35191
|
sinceTimestamp = parsed;
|
|
34882
35192
|
}
|
|
34883
35193
|
const allRuns = this.db.select().from(runs).where(
|
|
34884
|
-
|
|
34885
|
-
|
|
34886
|
-
or5(
|
|
35194
|
+
and28(
|
|
35195
|
+
eq38(runs.projectId, project.id),
|
|
35196
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
34887
35197
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
34888
35198
|
ne5(runs.trigger, RunTriggers.probe)
|
|
34889
35199
|
)
|
|
34890
|
-
).orderBy(
|
|
35200
|
+
).orderBy(asc6(runs.finishedAt)).all();
|
|
34891
35201
|
let startIdx = 0;
|
|
34892
35202
|
let endIdx = allRuns.length;
|
|
34893
35203
|
if (opts?.fromRunId) {
|
|
@@ -34962,7 +35272,7 @@ var IntelligenceService = class {
|
|
|
34962
35272
|
return { processed, skipped, totalInsights };
|
|
34963
35273
|
}
|
|
34964
35274
|
loadTrackedCompetitors(projectId) {
|
|
34965
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
35275
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq38(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
34966
35276
|
}
|
|
34967
35277
|
/**
|
|
34968
35278
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -34983,15 +35293,15 @@ var IntelligenceService = class {
|
|
|
34983
35293
|
}
|
|
34984
35294
|
persistResult(result, runId, projectId) {
|
|
34985
35295
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
34986
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
35296
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq38(insights.runId, runId)).all();
|
|
34987
35297
|
for (const row of existingInsights) {
|
|
34988
35298
|
if (row.dismissed) {
|
|
34989
35299
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
34990
35300
|
}
|
|
34991
35301
|
}
|
|
34992
35302
|
this.db.transaction((tx) => {
|
|
34993
|
-
tx.delete(insights).where(
|
|
34994
|
-
tx.delete(healthSnapshots).where(
|
|
35303
|
+
tx.delete(insights).where(eq38(insights.runId, runId)).run();
|
|
35304
|
+
tx.delete(healthSnapshots).where(eq38(healthSnapshots.runId, runId)).run();
|
|
34995
35305
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34996
35306
|
for (const insight of result.insights) {
|
|
34997
35307
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -35044,24 +35354,24 @@ var IntelligenceService = class {
|
|
|
35044
35354
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
35045
35355
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
35046
35356
|
if (regressions.length === 0) return rawInsights;
|
|
35047
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
35357
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq38(gscSearchData.projectId, projectId)).all();
|
|
35048
35358
|
const gscConnected = gscRows.length > 0;
|
|
35049
35359
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
35050
35360
|
for (const row of gscRows) {
|
|
35051
35361
|
const key = row.query.toLowerCase();
|
|
35052
35362
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
35053
35363
|
}
|
|
35054
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
35364
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq38(projects.id, projectId)).get();
|
|
35055
35365
|
const locationCount = Math.max(
|
|
35056
35366
|
1,
|
|
35057
35367
|
(projectRow?.locations ?? []).length
|
|
35058
35368
|
);
|
|
35059
35369
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
35060
35370
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
35061
|
-
|
|
35062
|
-
|
|
35063
|
-
|
|
35064
|
-
or5(
|
|
35371
|
+
and28(
|
|
35372
|
+
eq38(runs.projectId, projectId),
|
|
35373
|
+
eq38(runs.kind, RunKinds["answer-visibility"]),
|
|
35374
|
+
or5(eq38(runs.status, "completed"), eq38(runs.status, "partial")),
|
|
35065
35375
|
// Defensive — see top of file.
|
|
35066
35376
|
ne5(runs.trigger, RunTriggers.probe)
|
|
35067
35377
|
)
|
|
@@ -35081,7 +35391,7 @@ var IntelligenceService = class {
|
|
|
35081
35391
|
const haveHistory = recentRunIds.length > 0;
|
|
35082
35392
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
35083
35393
|
if (haveHistory) {
|
|
35084
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
35394
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and28(eq38(insights.type, "regression"), inArray14(insights.runId, recentRunIds))).all();
|
|
35085
35395
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
35086
35396
|
for (const row of priorRows) {
|
|
35087
35397
|
if (!row.runId) continue;
|
|
@@ -35110,7 +35420,7 @@ var IntelligenceService = class {
|
|
|
35110
35420
|
});
|
|
35111
35421
|
}
|
|
35112
35422
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
35113
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
35423
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq38(projects.id, projectId)).get();
|
|
35114
35424
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
35115
35425
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
35116
35426
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -35127,7 +35437,7 @@ var IntelligenceService = class {
|
|
|
35127
35437
|
citedDomains: querySnapshots.citedDomains,
|
|
35128
35438
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
35129
35439
|
snapshotLocation: querySnapshots.location
|
|
35130
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
35440
|
+
}).from(querySnapshots).leftJoin(queries, eq38(querySnapshots.queryId, queries.id)).where(eq38(querySnapshots.runId, runId)).all();
|
|
35131
35441
|
const snapshots = [];
|
|
35132
35442
|
let orphanCount = 0;
|
|
35133
35443
|
for (const r of rows) {
|
|
@@ -35180,6 +35490,7 @@ export {
|
|
|
35180
35490
|
schedules,
|
|
35181
35491
|
notifications,
|
|
35182
35492
|
gscSearchData,
|
|
35493
|
+
gscDailyTotals,
|
|
35183
35494
|
gscUrlInspections,
|
|
35184
35495
|
gscCoverageSnapshots,
|
|
35185
35496
|
siteAuditSnapshots,
|
|
@@ -35195,6 +35506,8 @@ export {
|
|
|
35195
35506
|
backlinkDomains,
|
|
35196
35507
|
backlinkSummaries,
|
|
35197
35508
|
agentMemory,
|
|
35509
|
+
llmUsageEvents,
|
|
35510
|
+
agentToolEvents,
|
|
35198
35511
|
crawlerEventsHourly,
|
|
35199
35512
|
aiUserFetchEventsHourly,
|
|
35200
35513
|
rawEventSamples,
|