@ainyc/canonry 4.15.0 → 4.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -223
- package/assets/assets/index-4fWsYFLp.css +1 -0
- package/assets/assets/index-C5-Gvl6o.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-C32VL5BB.js → chunk-6TWKC3DP.js} +147 -12
- package/dist/{chunk-7HBZCGRL.js → chunk-PAZCY4FF.js} +99 -4
- package/dist/{chunk-6QTH5NS5.js → chunk-Q2OED5JQ.js} +75 -1
- package/dist/{chunk-DLSQXNUN.js → chunk-ZGHD3IAV.js} +485 -150
- package/dist/cli.js +248 -45
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-BCKXIKIL.js → intelligence-service-X3PQLBUV.js} +2 -2
- package/dist/mcp.js +9 -3
- package/package.json +7 -7
- package/assets/assets/index-B6Mi9Fd1.js +0 -302
- package/assets/assets/index-D0EPNRDs.css +0 -1
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6TWKC3DP.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
schedules,
|
|
67
67
|
trafficSources,
|
|
68
68
|
usageCounters
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-PAZCY4FF.js";
|
|
70
70
|
import {
|
|
71
71
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
72
72
|
AGENT_PROVIDER_IDS,
|
|
@@ -81,7 +81,9 @@ import {
|
|
|
81
81
|
RunKinds,
|
|
82
82
|
RunStatuses,
|
|
83
83
|
RunTriggers,
|
|
84
|
+
SchedulableRunKinds,
|
|
84
85
|
TrafficEventConfidences,
|
|
86
|
+
TrafficEventKinds,
|
|
85
87
|
TrafficEvidenceKinds,
|
|
86
88
|
TrafficSourceAuthModes,
|
|
87
89
|
TrafficSourceStatuses,
|
|
@@ -143,6 +145,7 @@ import {
|
|
|
143
145
|
runInProgress,
|
|
144
146
|
runNotCancellable,
|
|
145
147
|
runTriggerRequestSchema,
|
|
148
|
+
schedulableRunKindSchema,
|
|
146
149
|
scheduleUpsertRequestSchema,
|
|
147
150
|
serializeRunError,
|
|
148
151
|
snapshotRequestSchema,
|
|
@@ -152,7 +155,7 @@ import {
|
|
|
152
155
|
visibilityStateFromAnswerMentioned,
|
|
153
156
|
windowCutoff,
|
|
154
157
|
wordpressEnvSchema
|
|
155
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-Q2OED5JQ.js";
|
|
156
159
|
|
|
157
160
|
// src/telemetry.ts
|
|
158
161
|
import crypto from "crypto";
|
|
@@ -1435,7 +1438,7 @@ function loadRunDetail(app, run) {
|
|
|
1435
1438
|
|
|
1436
1439
|
// ../api-routes/src/apply.ts
|
|
1437
1440
|
import crypto10 from "crypto";
|
|
1438
|
-
import { eq as eq8 } from "drizzle-orm";
|
|
1441
|
+
import { and as and2, eq as eq8 } from "drizzle-orm";
|
|
1439
1442
|
|
|
1440
1443
|
// ../api-routes/src/schedule-utils.ts
|
|
1441
1444
|
var DAY_MAP = {
|
|
@@ -1840,8 +1843,9 @@ async function applyRoutes(app, opts) {
|
|
|
1840
1843
|
entityType: "competitor",
|
|
1841
1844
|
diff: { competitors: normalizedCompetitors }
|
|
1842
1845
|
});
|
|
1846
|
+
const AV_KIND = SchedulableRunKinds["answer-visibility"];
|
|
1843
1847
|
if (resolvedSchedule) {
|
|
1844
|
-
const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
|
|
1848
|
+
const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1845
1849
|
if (existingSched) {
|
|
1846
1850
|
tx.update(schedules).set({
|
|
1847
1851
|
cronExpr: resolvedSchedule.cronExpr,
|
|
@@ -1855,6 +1859,7 @@ async function applyRoutes(app, opts) {
|
|
|
1855
1859
|
tx.insert(schedules).values({
|
|
1856
1860
|
id: crypto10.randomUUID(),
|
|
1857
1861
|
projectId,
|
|
1862
|
+
kind: AV_KIND,
|
|
1858
1863
|
cronExpr: resolvedSchedule.cronExpr,
|
|
1859
1864
|
preset: resolvedSchedule.preset,
|
|
1860
1865
|
timezone: resolvedSchedule.timezone,
|
|
@@ -1866,9 +1871,9 @@ async function applyRoutes(app, opts) {
|
|
|
1866
1871
|
}
|
|
1867
1872
|
scheduleAction = "upsert";
|
|
1868
1873
|
} else if (deleteSchedule) {
|
|
1869
|
-
const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
|
|
1874
|
+
const existingSched = tx.select().from(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).get();
|
|
1870
1875
|
if (existingSched) {
|
|
1871
|
-
tx.delete(schedules).where(eq8(schedules.projectId, projectId)).run();
|
|
1876
|
+
tx.delete(schedules).where(and2(eq8(schedules.projectId, projectId), eq8(schedules.kind, AV_KIND))).run();
|
|
1872
1877
|
scheduleAction = "delete";
|
|
1873
1878
|
}
|
|
1874
1879
|
}
|
|
@@ -1896,7 +1901,7 @@ async function applyRoutes(app, opts) {
|
|
|
1896
1901
|
}
|
|
1897
1902
|
});
|
|
1898
1903
|
if (scheduleAction) {
|
|
1899
|
-
opts?.onScheduleUpdated?.(scheduleAction, projectId);
|
|
1904
|
+
opts?.onScheduleUpdated?.(scheduleAction, projectId, SchedulableRunKinds["answer-visibility"]);
|
|
1900
1905
|
}
|
|
1901
1906
|
if (!hasNotifications) {
|
|
1902
1907
|
opts?.onProjectUpserted?.(projectId, config.metadata.name);
|
|
@@ -2558,7 +2563,7 @@ function buildCategoryCounts(counts) {
|
|
|
2558
2563
|
}
|
|
2559
2564
|
|
|
2560
2565
|
// ../api-routes/src/intelligence.ts
|
|
2561
|
-
import { eq as eq11, desc as desc4, and as
|
|
2566
|
+
import { eq as eq11, desc as desc4, and as and3 } from "drizzle-orm";
|
|
2562
2567
|
function emptyHealthSnapshot(projectId) {
|
|
2563
2568
|
return {
|
|
2564
2569
|
id: `no-data:${projectId}`,
|
|
@@ -2609,7 +2614,7 @@ async function intelligenceRoutes(app) {
|
|
|
2609
2614
|
if (request.query.runId) {
|
|
2610
2615
|
conditions.push(eq11(insights.runId, request.query.runId));
|
|
2611
2616
|
}
|
|
2612
|
-
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] :
|
|
2617
|
+
const rows = app.db.select().from(insights).where(conditions.length === 1 ? conditions[0] : and3(...conditions)).orderBy(desc4(insights.createdAt)).all();
|
|
2613
2618
|
const showDismissed = request.query.dismissed === "true";
|
|
2614
2619
|
const result = rows.filter((r) => showDismissed || !r.dismissed).map(mapInsightRow);
|
|
2615
2620
|
return reply.send(result);
|
|
@@ -2649,7 +2654,7 @@ async function intelligenceRoutes(app) {
|
|
|
2649
2654
|
}
|
|
2650
2655
|
|
|
2651
2656
|
// ../api-routes/src/report.ts
|
|
2652
|
-
import { and as
|
|
2657
|
+
import { and as and5, desc as desc6, eq as eq13, inArray as inArray4, or as or2 } from "drizzle-orm";
|
|
2653
2658
|
|
|
2654
2659
|
// ../api-routes/src/report-renderer.ts
|
|
2655
2660
|
var COLORS = {
|
|
@@ -4724,7 +4729,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4724
4729
|
}
|
|
4725
4730
|
|
|
4726
4731
|
// ../api-routes/src/content-data.ts
|
|
4727
|
-
import { and as
|
|
4732
|
+
import { and as and4, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
|
|
4728
4733
|
var RECENT_RUNS_WINDOW = 5;
|
|
4729
4734
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
4730
4735
|
const projectId = project.id;
|
|
@@ -4848,7 +4853,7 @@ function listCompetitorDomains(db, projectId) {
|
|
|
4848
4853
|
}
|
|
4849
4854
|
function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter) {
|
|
4850
4855
|
const rows = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
4851
|
-
|
|
4856
|
+
and4(
|
|
4852
4857
|
eq12(runs.projectId, projectId),
|
|
4853
4858
|
eq12(runs.kind, RunKinds["answer-visibility"]),
|
|
4854
4859
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
@@ -5192,7 +5197,7 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
|
|
|
5192
5197
|
}
|
|
5193
5198
|
function buildGaSection(db, projectId) {
|
|
5194
5199
|
const windowSummary = db.select().from(gaTrafficWindowSummaries).where(
|
|
5195
|
-
|
|
5200
|
+
and5(
|
|
5196
5201
|
eq13(gaTrafficWindowSummaries.projectId, projectId),
|
|
5197
5202
|
eq13(gaTrafficWindowSummaries.windowKey, "30d")
|
|
5198
5203
|
)
|
|
@@ -5375,7 +5380,7 @@ function buildIndexingHealth(db, projectId) {
|
|
|
5375
5380
|
return null;
|
|
5376
5381
|
}
|
|
5377
5382
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
5378
|
-
const visibilityRuns = db.select().from(runs).where(
|
|
5383
|
+
const visibilityRuns = db.select().from(runs).where(and5(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
5379
5384
|
const totalQueries = queryLookup.byId.size;
|
|
5380
5385
|
const points = [];
|
|
5381
5386
|
for (const run of visibilityRuns) {
|
|
@@ -5421,14 +5426,14 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
5421
5426
|
}
|
|
5422
5427
|
function buildInsightList(db, projectId, locationFilter) {
|
|
5423
5428
|
const recentRunIds = db.select({ id: runs.id, location: runs.location }).from(runs).where(
|
|
5424
|
-
|
|
5429
|
+
and5(
|
|
5425
5430
|
eq13(runs.projectId, projectId),
|
|
5426
5431
|
eq13(runs.kind, RunKinds["answer-visibility"]),
|
|
5427
5432
|
or2(eq13(runs.status, RunStatuses.completed), eq13(runs.status, RunStatuses.partial))
|
|
5428
5433
|
)
|
|
5429
5434
|
).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
5430
5435
|
if (recentRunIds.length === 0) return [];
|
|
5431
|
-
const rows = db.select().from(insights).where(
|
|
5436
|
+
const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray4(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
|
|
5432
5437
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
5433
5438
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
5434
5439
|
const recommendation = parseJsonColumn(r.recommendation, null);
|
|
@@ -6350,7 +6355,7 @@ function normalizeDomain2(domain) {
|
|
|
6350
6355
|
}
|
|
6351
6356
|
|
|
6352
6357
|
// ../api-routes/src/composites.ts
|
|
6353
|
-
import { eq as eq15, and as
|
|
6358
|
+
import { eq as eq15, and as and6, desc as desc7, sql as sql3, like, or as or3, inArray as inArray6 } from "drizzle-orm";
|
|
6354
6359
|
var TOP_INSIGHT_LIMIT = 5;
|
|
6355
6360
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
6356
6361
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -6453,7 +6458,7 @@ async function compositeRoutes(app) {
|
|
|
6453
6458
|
rawResponse: querySnapshots.rawResponse,
|
|
6454
6459
|
createdAt: querySnapshots.createdAt
|
|
6455
6460
|
}).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
|
|
6456
|
-
|
|
6461
|
+
and6(
|
|
6457
6462
|
eq15(queries.projectId, project.id),
|
|
6458
6463
|
or3(
|
|
6459
6464
|
sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
|
|
@@ -6464,7 +6469,7 @@ async function compositeRoutes(app) {
|
|
|
6464
6469
|
)
|
|
6465
6470
|
).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
|
|
6466
6471
|
const insightMatches = app.db.select().from(insights).where(
|
|
6467
|
-
|
|
6472
|
+
and6(
|
|
6468
6473
|
eq15(insights.projectId, project.id),
|
|
6469
6474
|
or3(
|
|
6470
6475
|
like(insights.title, pattern),
|
|
@@ -7036,6 +7041,12 @@ var locationQueryParameter = {
|
|
|
7036
7041
|
description: "Filter by location label. Use an empty value to request locationless results.",
|
|
7037
7042
|
schema: stringSchema
|
|
7038
7043
|
};
|
|
7044
|
+
var scheduleKindQueryParameter = {
|
|
7045
|
+
name: "kind",
|
|
7046
|
+
in: "query",
|
|
7047
|
+
description: 'Schedulable run kind. Defaults to "answer-visibility" for backward compatibility.',
|
|
7048
|
+
schema: { type: "string", enum: ["answer-visibility", "traffic-sync"] }
|
|
7049
|
+
};
|
|
7039
7050
|
var reportAudienceQueryParameter = {
|
|
7040
7051
|
name: "audience",
|
|
7041
7052
|
in: "query",
|
|
@@ -7893,7 +7904,7 @@ var routeCatalog = [
|
|
|
7893
7904
|
path: "/api/v1/projects/{name}/schedule",
|
|
7894
7905
|
summary: "Create or update a schedule",
|
|
7895
7906
|
tags: ["schedules"],
|
|
7896
|
-
parameters: [nameParameter],
|
|
7907
|
+
parameters: [nameParameter, scheduleKindQueryParameter],
|
|
7897
7908
|
requestBody: {
|
|
7898
7909
|
required: true,
|
|
7899
7910
|
content: {
|
|
@@ -7901,11 +7912,13 @@ var routeCatalog = [
|
|
|
7901
7912
|
schema: {
|
|
7902
7913
|
type: "object",
|
|
7903
7914
|
properties: {
|
|
7915
|
+
kind: { type: "string", enum: ["answer-visibility", "traffic-sync"] },
|
|
7904
7916
|
preset: stringSchema,
|
|
7905
7917
|
cron: stringSchema,
|
|
7906
7918
|
timezone: stringSchema,
|
|
7907
7919
|
providers: stringArraySchema,
|
|
7908
|
-
enabled: booleanSchema
|
|
7920
|
+
enabled: booleanSchema,
|
|
7921
|
+
sourceId: stringSchema
|
|
7909
7922
|
}
|
|
7910
7923
|
}
|
|
7911
7924
|
}
|
|
@@ -7913,7 +7926,8 @@ var routeCatalog = [
|
|
|
7913
7926
|
},
|
|
7914
7927
|
responses: {
|
|
7915
7928
|
200: { description: "Schedule updated." },
|
|
7916
|
-
201: { description: "Schedule created." }
|
|
7929
|
+
201: { description: "Schedule created." },
|
|
7930
|
+
400: { description: "Invalid payload (e.g. sourceId missing for kind=traffic-sync, or providers set for kind=traffic-sync)." }
|
|
7917
7931
|
}
|
|
7918
7932
|
},
|
|
7919
7933
|
{
|
|
@@ -7921,7 +7935,7 @@ var routeCatalog = [
|
|
|
7921
7935
|
path: "/api/v1/projects/{name}/schedule",
|
|
7922
7936
|
summary: "Get a schedule",
|
|
7923
7937
|
tags: ["schedules"],
|
|
7924
|
-
parameters: [nameParameter],
|
|
7938
|
+
parameters: [nameParameter, scheduleKindQueryParameter],
|
|
7925
7939
|
responses: {
|
|
7926
7940
|
200: { description: "Schedule returned." },
|
|
7927
7941
|
404: { description: "Schedule not found." }
|
|
@@ -7932,7 +7946,7 @@ var routeCatalog = [
|
|
|
7932
7946
|
path: "/api/v1/projects/{name}/schedule",
|
|
7933
7947
|
summary: "Delete a schedule",
|
|
7934
7948
|
tags: ["schedules"],
|
|
7935
|
-
parameters: [nameParameter],
|
|
7949
|
+
parameters: [nameParameter, scheduleKindQueryParameter],
|
|
7936
7950
|
responses: {
|
|
7937
7951
|
204: { description: "Schedule deleted." },
|
|
7938
7952
|
404: { description: "Schedule not found." }
|
|
@@ -9661,8 +9675,66 @@ var routeCatalog = [
|
|
|
9661
9675
|
},
|
|
9662
9676
|
responses: {
|
|
9663
9677
|
200: { description: "Sync summary returned." },
|
|
9664
|
-
400: { description: "Invalid sync request
|
|
9665
|
-
404: { description: "Project or traffic source not found." }
|
|
9678
|
+
400: { description: "Invalid sync request or missing credentials." },
|
|
9679
|
+
404: { description: "Project or traffic source not found." },
|
|
9680
|
+
502: { description: "Upstream Cloud Run pull or auth-token resolution failed." }
|
|
9681
|
+
}
|
|
9682
|
+
},
|
|
9683
|
+
{
|
|
9684
|
+
method: "get",
|
|
9685
|
+
path: "/api/v1/projects/{name}/traffic/sources",
|
|
9686
|
+
summary: "List non-archived traffic sources for a project",
|
|
9687
|
+
tags: ["traffic"],
|
|
9688
|
+
parameters: [nameParameter],
|
|
9689
|
+
responses: {
|
|
9690
|
+
200: { description: "Source list returned." },
|
|
9691
|
+
404: { description: "Project not found." }
|
|
9692
|
+
}
|
|
9693
|
+
},
|
|
9694
|
+
{
|
|
9695
|
+
method: "get",
|
|
9696
|
+
path: "/api/v1/projects/{name}/traffic/status",
|
|
9697
|
+
summary: "List non-archived traffic sources with last-24h totals and the latest sync run for each",
|
|
9698
|
+
description: "Single-call composite for the `canonry traffic status` view: same shape as `GET /traffic/sources/{id}` but returned as `{ sources: TrafficSourceDetailDto[] }` for every non-archived source. Lets agents and the dashboard avoid an N+1 fan-out.",
|
|
9699
|
+
tags: ["traffic"],
|
|
9700
|
+
parameters: [nameParameter],
|
|
9701
|
+
responses: {
|
|
9702
|
+
200: { description: "Status returned." },
|
|
9703
|
+
404: { description: "Project not found." }
|
|
9704
|
+
}
|
|
9705
|
+
},
|
|
9706
|
+
{
|
|
9707
|
+
method: "get",
|
|
9708
|
+
path: "/api/v1/projects/{name}/traffic/sources/{id}",
|
|
9709
|
+
summary: "Get a single traffic source with last-24h totals and the latest sync run",
|
|
9710
|
+
tags: ["traffic"],
|
|
9711
|
+
parameters: [
|
|
9712
|
+
nameParameter,
|
|
9713
|
+
{ name: "id", in: "path", required: true, description: "Traffic source ID.", schema: stringSchema }
|
|
9714
|
+
],
|
|
9715
|
+
responses: {
|
|
9716
|
+
200: { description: "Source detail returned." },
|
|
9717
|
+
404: { description: "Project or source not found." }
|
|
9718
|
+
}
|
|
9719
|
+
},
|
|
9720
|
+
{
|
|
9721
|
+
method: "get",
|
|
9722
|
+
path: "/api/v1/projects/{name}/traffic/events",
|
|
9723
|
+
summary: "List rolled-up crawler and AI-referral hits within a window",
|
|
9724
|
+
description: "Returns hourly rollup rows from `crawler_events_hourly` and `ai_referral_events_hourly`. Defaults to the last 24h. Totals reflect the full window; the `events` array is capped by `limit` (default 500, max 5000).",
|
|
9725
|
+
tags: ["traffic"],
|
|
9726
|
+
parameters: [
|
|
9727
|
+
nameParameter,
|
|
9728
|
+
{ name: "since", in: "query", description: "ISO-8601 window start (defaults to 24h ago).", schema: stringSchema },
|
|
9729
|
+
{ name: "until", in: "query", description: "ISO-8601 window end (defaults to now).", schema: stringSchema },
|
|
9730
|
+
{ name: "kind", in: "query", description: 'Filter to "crawler", "ai-referral", or "all" (default).', schema: stringSchema },
|
|
9731
|
+
{ name: "limit", in: "query", description: "Max rows per kind in the events array (default 500, max 5000).", schema: stringSchema },
|
|
9732
|
+
{ name: "sourceId", in: "query", description: "Restrict to a single traffic source.", schema: stringSchema }
|
|
9733
|
+
],
|
|
9734
|
+
responses: {
|
|
9735
|
+
200: { description: "Events returned with windowed totals." },
|
|
9736
|
+
400: { description: "Invalid query parameters." },
|
|
9737
|
+
404: { description: "Project not found." }
|
|
9666
9738
|
}
|
|
9667
9739
|
}
|
|
9668
9740
|
];
|
|
@@ -10030,7 +10102,15 @@ async function telemetryRoutes(app, opts) {
|
|
|
10030
10102
|
|
|
10031
10103
|
// ../api-routes/src/schedules.ts
|
|
10032
10104
|
import crypto11 from "crypto";
|
|
10033
|
-
import { eq as eq16 } from "drizzle-orm";
|
|
10105
|
+
import { and as and7, eq as eq16 } from "drizzle-orm";
|
|
10106
|
+
function parseKindParam(raw) {
|
|
10107
|
+
if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
|
|
10108
|
+
const parsed = schedulableRunKindSchema.safeParse(raw);
|
|
10109
|
+
if (!parsed.success) {
|
|
10110
|
+
throw validationError(`Invalid kind "${String(raw)}". Must be one of: ${Object.values(SchedulableRunKinds).join(", ")}`);
|
|
10111
|
+
}
|
|
10112
|
+
return parsed.data;
|
|
10113
|
+
}
|
|
10034
10114
|
async function scheduleRoutes(app, opts) {
|
|
10035
10115
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
10036
10116
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -10043,7 +10123,22 @@ async function scheduleRoutes(app, opts) {
|
|
|
10043
10123
|
}))
|
|
10044
10124
|
});
|
|
10045
10125
|
}
|
|
10046
|
-
const
|
|
10126
|
+
const kind = parsedBody.data.kind ?? parseKindParam(request.query?.kind);
|
|
10127
|
+
const { preset, cron: cron2, timezone, providers, enabled, sourceId } = parsedBody.data;
|
|
10128
|
+
if (kind === SchedulableRunKinds["traffic-sync"]) {
|
|
10129
|
+
if (!sourceId) {
|
|
10130
|
+
throw validationError('"sourceId" is required when kind is "traffic-sync"');
|
|
10131
|
+
}
|
|
10132
|
+
const sourceRow = app.db.select().from(trafficSources).where(eq16(trafficSources.id, sourceId)).get();
|
|
10133
|
+
if (!sourceRow || sourceRow.projectId !== project.id) {
|
|
10134
|
+
throw notFound("Traffic source", sourceId);
|
|
10135
|
+
}
|
|
10136
|
+
if (providers && providers.length > 0) {
|
|
10137
|
+
throw validationError('"providers" is not valid for kind "traffic-sync"');
|
|
10138
|
+
}
|
|
10139
|
+
} else if (sourceId) {
|
|
10140
|
+
throw validationError(`"sourceId" is only valid when kind is "traffic-sync"`);
|
|
10141
|
+
}
|
|
10047
10142
|
const validNames = opts.validProviderNames ?? [];
|
|
10048
10143
|
if (validNames.length && providers?.length) {
|
|
10049
10144
|
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
@@ -10073,13 +10168,14 @@ async function scheduleRoutes(app, opts) {
|
|
|
10073
10168
|
}
|
|
10074
10169
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10075
10170
|
const enabledInt = enabled === false ? 0 : 1;
|
|
10076
|
-
const existing = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
|
|
10171
|
+
const existing = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10077
10172
|
if (existing) {
|
|
10078
10173
|
app.db.update(schedules).set({
|
|
10079
10174
|
cronExpr,
|
|
10080
10175
|
preset: preset ?? null,
|
|
10081
10176
|
timezone,
|
|
10082
|
-
providers: JSON.stringify(providers),
|
|
10177
|
+
providers: JSON.stringify(providers ?? []),
|
|
10178
|
+
sourceId: sourceId ?? null,
|
|
10083
10179
|
enabled: enabledInt,
|
|
10084
10180
|
updatedAt: now
|
|
10085
10181
|
}).where(eq16(schedules.id, existing.id)).run();
|
|
@@ -10087,11 +10183,13 @@ async function scheduleRoutes(app, opts) {
|
|
|
10087
10183
|
app.db.insert(schedules).values({
|
|
10088
10184
|
id: crypto11.randomUUID(),
|
|
10089
10185
|
projectId: project.id,
|
|
10186
|
+
kind,
|
|
10090
10187
|
cronExpr,
|
|
10091
10188
|
preset: preset ?? null,
|
|
10092
10189
|
timezone,
|
|
10093
10190
|
enabled: enabledInt,
|
|
10094
|
-
providers: JSON.stringify(providers),
|
|
10191
|
+
providers: JSON.stringify(providers ?? []),
|
|
10192
|
+
sourceId: sourceId ?? null,
|
|
10095
10193
|
createdAt: now,
|
|
10096
10194
|
updatedAt: now
|
|
10097
10195
|
}).run();
|
|
@@ -10101,25 +10199,27 @@ async function scheduleRoutes(app, opts) {
|
|
|
10101
10199
|
actor: "api",
|
|
10102
10200
|
action: existing ? "schedule.updated" : "schedule.created",
|
|
10103
10201
|
entityType: "schedule",
|
|
10104
|
-
diff: { cronExpr, preset, timezone, providers }
|
|
10202
|
+
diff: { kind, cronExpr, preset, timezone, providers, sourceId }
|
|
10105
10203
|
});
|
|
10106
|
-
opts.onScheduleUpdated?.("upsert", project.id);
|
|
10107
|
-
const schedule = app.db.select().from(schedules).where(eq16(schedules.projectId, project.id)).get();
|
|
10204
|
+
opts.onScheduleUpdated?.("upsert", project.id, kind);
|
|
10205
|
+
const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10108
10206
|
return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
|
|
10109
10207
|
});
|
|
10110
10208
|
app.get("/projects/:name/schedule", async (request, reply) => {
|
|
10111
10209
|
const project = resolveProject(app.db, request.params.name);
|
|
10112
|
-
const
|
|
10210
|
+
const kind = parseKindParam(request.query?.kind);
|
|
10211
|
+
const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10113
10212
|
if (!schedule) {
|
|
10114
|
-
throw notFound("Schedule", request.params.name);
|
|
10213
|
+
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10115
10214
|
}
|
|
10116
10215
|
return reply.send(formatSchedule(schedule));
|
|
10117
10216
|
});
|
|
10118
10217
|
app.delete("/projects/:name/schedule", async (request, reply) => {
|
|
10119
10218
|
const project = resolveProject(app.db, request.params.name);
|
|
10120
|
-
const
|
|
10219
|
+
const kind = parseKindParam(request.query?.kind);
|
|
10220
|
+
const schedule = app.db.select().from(schedules).where(and7(eq16(schedules.projectId, project.id), eq16(schedules.kind, kind))).get();
|
|
10121
10221
|
if (!schedule) {
|
|
10122
|
-
throw notFound("Schedule", request.params.name);
|
|
10222
|
+
throw notFound("Schedule", `${request.params.name} (kind=${kind})`);
|
|
10123
10223
|
}
|
|
10124
10224
|
app.db.delete(schedules).where(eq16(schedules.id, schedule.id)).run();
|
|
10125
10225
|
writeAuditLog(app.db, {
|
|
@@ -10127,9 +10227,10 @@ async function scheduleRoutes(app, opts) {
|
|
|
10127
10227
|
actor: "api",
|
|
10128
10228
|
action: "schedule.deleted",
|
|
10129
10229
|
entityType: "schedule",
|
|
10130
|
-
entityId: schedule.id
|
|
10230
|
+
entityId: schedule.id,
|
|
10231
|
+
diff: { kind }
|
|
10131
10232
|
});
|
|
10132
|
-
opts.onScheduleUpdated?.("delete", project.id);
|
|
10233
|
+
opts.onScheduleUpdated?.("delete", project.id, kind);
|
|
10133
10234
|
return reply.status(204).send();
|
|
10134
10235
|
});
|
|
10135
10236
|
}
|
|
@@ -10137,11 +10238,13 @@ function formatSchedule(row) {
|
|
|
10137
10238
|
return {
|
|
10138
10239
|
id: row.id,
|
|
10139
10240
|
projectId: row.projectId,
|
|
10241
|
+
kind: row.kind,
|
|
10140
10242
|
cronExpr: row.cronExpr,
|
|
10141
10243
|
preset: row.preset,
|
|
10142
10244
|
timezone: row.timezone,
|
|
10143
10245
|
enabled: row.enabled === 1,
|
|
10144
10246
|
providers: parseJsonColumn(row.providers, []),
|
|
10247
|
+
sourceId: row.sourceId,
|
|
10145
10248
|
lastRunAt: row.lastRunAt,
|
|
10146
10249
|
nextRunAt: row.nextRunAt,
|
|
10147
10250
|
createdAt: row.createdAt,
|
|
@@ -10270,7 +10373,7 @@ function formatNotification(row) {
|
|
|
10270
10373
|
|
|
10271
10374
|
// ../api-routes/src/google.ts
|
|
10272
10375
|
import crypto14 from "crypto";
|
|
10273
|
-
import { eq as eq18, and as
|
|
10376
|
+
import { eq as eq18, and as and8, desc as desc8, sql as sql4 } from "drizzle-orm";
|
|
10274
10377
|
|
|
10275
10378
|
// ../integration-google/src/constants.ts
|
|
10276
10379
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -11485,7 +11588,7 @@ async function googleRoutes(app, opts) {
|
|
|
11485
11588
|
if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
|
|
11486
11589
|
if (query) conditions.push(sql4`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
11487
11590
|
if (page) conditions.push(sql4`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
11488
|
-
const rows = app.db.select().from(gscSearchData).where(
|
|
11591
|
+
const rows = app.db.select().from(gscSearchData).where(and8(...conditions)).orderBy(desc8(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
11489
11592
|
return rows.map((r) => ({
|
|
11490
11593
|
date: r.date,
|
|
11491
11594
|
query: r.query,
|
|
@@ -11559,7 +11662,7 @@ async function googleRoutes(app, opts) {
|
|
|
11559
11662
|
const { url, limit } = request.query;
|
|
11560
11663
|
const conditions = [eq18(gscUrlInspections.projectId, project.id)];
|
|
11561
11664
|
if (url) conditions.push(eq18(gscUrlInspections.url, url));
|
|
11562
|
-
const rows = app.db.select().from(gscUrlInspections).where(
|
|
11665
|
+
const rows = app.db.select().from(gscUrlInspections).where(and8(...conditions)).orderBy(desc8(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
11563
11666
|
return rows.map((r) => ({
|
|
11564
11667
|
id: r.id,
|
|
11565
11668
|
url: r.url,
|
|
@@ -11911,7 +12014,7 @@ async function googleRoutes(app, opts) {
|
|
|
11911
12014
|
|
|
11912
12015
|
// ../api-routes/src/bing.ts
|
|
11913
12016
|
import crypto15 from "crypto";
|
|
11914
|
-
import { eq as eq19, and as
|
|
12017
|
+
import { eq as eq19, and as and9, desc as desc9 } from "drizzle-orm";
|
|
11915
12018
|
|
|
11916
12019
|
// ../integration-bing/src/constants.ts
|
|
11917
12020
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -12325,7 +12428,7 @@ async function bingRoutes(app, opts) {
|
|
|
12325
12428
|
requireConnectionStore();
|
|
12326
12429
|
const project = resolveProject(app.db, request.params.name);
|
|
12327
12430
|
const { url, limit } = request.query;
|
|
12328
|
-
const whereClause = url ?
|
|
12431
|
+
const whereClause = url ? and9(eq19(bingUrlInspections.projectId, project.id), eq19(bingUrlInspections.url, url)) : eq19(bingUrlInspections.projectId, project.id);
|
|
12329
12432
|
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc9(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
12330
12433
|
return filtered.map((r) => ({
|
|
12331
12434
|
id: r.id,
|
|
@@ -12557,7 +12660,7 @@ async function bingRoutes(app, opts) {
|
|
|
12557
12660
|
import fs from "fs";
|
|
12558
12661
|
import path from "path";
|
|
12559
12662
|
import os2 from "os";
|
|
12560
|
-
import { eq as eq20, and as
|
|
12663
|
+
import { eq as eq20, and as and10 } from "drizzle-orm";
|
|
12561
12664
|
function getScreenshotDir() {
|
|
12562
12665
|
return path.join(os2.homedir(), ".canonry", "screenshots");
|
|
12563
12666
|
}
|
|
@@ -12630,7 +12733,7 @@ async function cdpRoutes(app, opts) {
|
|
|
12630
12733
|
async (request, reply) => {
|
|
12631
12734
|
const project = resolveProject(app.db, request.params.name);
|
|
12632
12735
|
const { runId } = request.params;
|
|
12633
|
-
const run = app.db.select().from(runs).where(
|
|
12736
|
+
const run = app.db.select().from(runs).where(and10(eq20(runs.id, runId), eq20(runs.projectId, project.id))).get();
|
|
12634
12737
|
if (!run) {
|
|
12635
12738
|
const err = notFound("Run", runId);
|
|
12636
12739
|
return reply.code(err.statusCode).send(err.toJSON());
|
|
@@ -12727,7 +12830,7 @@ async function cdpRoutes(app, opts) {
|
|
|
12727
12830
|
|
|
12728
12831
|
// ../api-routes/src/ga.ts
|
|
12729
12832
|
import crypto16 from "crypto";
|
|
12730
|
-
import { eq as eq21, desc as desc10, and as
|
|
12833
|
+
import { eq as eq21, desc as desc10, and as and11, sql as sql5 } from "drizzle-orm";
|
|
12731
12834
|
function gaLog(level, action, ctx) {
|
|
12732
12835
|
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
|
|
12733
12836
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
@@ -13022,7 +13125,7 @@ async function ga4Routes(app, opts) {
|
|
|
13022
13125
|
app.db.transaction((tx) => {
|
|
13023
13126
|
if (syncTraffic) {
|
|
13024
13127
|
tx.delete(gaTrafficSnapshots).where(
|
|
13025
|
-
|
|
13128
|
+
and11(
|
|
13026
13129
|
eq21(gaTrafficSnapshots.projectId, project.id),
|
|
13027
13130
|
sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
|
|
13028
13131
|
sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
|
|
@@ -13046,7 +13149,7 @@ async function ga4Routes(app, opts) {
|
|
|
13046
13149
|
}
|
|
13047
13150
|
if (syncAi) {
|
|
13048
13151
|
tx.delete(gaAiReferrals).where(
|
|
13049
|
-
|
|
13152
|
+
and11(
|
|
13050
13153
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13051
13154
|
sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
|
|
13052
13155
|
sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
|
|
@@ -13072,7 +13175,7 @@ async function ga4Routes(app, opts) {
|
|
|
13072
13175
|
}
|
|
13073
13176
|
if (syncSocial) {
|
|
13074
13177
|
tx.delete(gaSocialReferrals).where(
|
|
13075
|
-
|
|
13178
|
+
and11(
|
|
13076
13179
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13077
13180
|
sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
|
|
13078
13181
|
sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
|
|
@@ -13176,7 +13279,7 @@ async function ga4Routes(app, opts) {
|
|
|
13176
13279
|
totalDirectSessions: gaTrafficWindowSummaries.totalDirectSessions,
|
|
13177
13280
|
totalUsers: gaTrafficWindowSummaries.totalUsers
|
|
13178
13281
|
}).from(gaTrafficWindowSummaries).where(
|
|
13179
|
-
|
|
13282
|
+
and11(
|
|
13180
13283
|
eq21(gaTrafficWindowSummaries.projectId, project.id),
|
|
13181
13284
|
eq21(gaTrafficWindowSummaries.windowKey, window)
|
|
13182
13285
|
)
|
|
@@ -13185,7 +13288,7 @@ async function ga4Routes(app, opts) {
|
|
|
13185
13288
|
totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
|
|
13186
13289
|
totalOrganicSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)`,
|
|
13187
13290
|
totalUsers: sql5`COALESCE(SUM(${gaTrafficSnapshots.users}), 0)`
|
|
13188
|
-
}).from(gaTrafficSnapshots).where(
|
|
13291
|
+
}).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get() : null;
|
|
13189
13292
|
const summaryRow = cutoffDate ? windowSummaryRow ?? snapshotTotalsRow : app.db.select({
|
|
13190
13293
|
totalSessions: gaTrafficSummaries.totalSessions,
|
|
13191
13294
|
totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
|
|
@@ -13193,7 +13296,7 @@ async function ga4Routes(app, opts) {
|
|
|
13193
13296
|
}).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).get();
|
|
13194
13297
|
const directTotalRow = windowSummaryRow ? { totalDirectSessions: windowSummaryRow.totalDirectSessions } : app.db.select({
|
|
13195
13298
|
totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
|
|
13196
|
-
}).from(gaTrafficSnapshots).where(
|
|
13299
|
+
}).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).get();
|
|
13197
13300
|
const summaryMeta = app.db.select({
|
|
13198
13301
|
periodStart: gaTrafficSummaries.periodStart,
|
|
13199
13302
|
periodEnd: gaTrafficSummaries.periodEnd
|
|
@@ -13204,14 +13307,14 @@ async function ga4Routes(app, opts) {
|
|
|
13204
13307
|
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
13205
13308
|
directSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`,
|
|
13206
13309
|
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
13207
|
-
}).from(gaTrafficSnapshots).where(
|
|
13310
|
+
}).from(gaTrafficSnapshots).where(and11(...snapshotConditions)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).limit(limit).all();
|
|
13208
13311
|
const aiReferralRows = app.db.select({
|
|
13209
13312
|
source: gaAiReferrals.source,
|
|
13210
13313
|
medium: gaAiReferrals.medium,
|
|
13211
13314
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
13212
13315
|
sessions: sql5`SUM(${gaAiReferrals.sessions})`,
|
|
13213
13316
|
users: sql5`SUM(${gaAiReferrals.users})`
|
|
13214
|
-
}).from(gaAiReferrals).where(
|
|
13317
|
+
}).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).all();
|
|
13215
13318
|
const aiReferralLandingPageRows = app.db.select({
|
|
13216
13319
|
source: gaAiReferrals.source,
|
|
13217
13320
|
medium: gaAiReferrals.medium,
|
|
@@ -13219,7 +13322,7 @@ async function ga4Routes(app, opts) {
|
|
|
13219
13322
|
landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
|
|
13220
13323
|
sessions: sql5`SUM(${gaAiReferrals.sessions})`,
|
|
13221
13324
|
users: sql5`SUM(${gaAiReferrals.users})`
|
|
13222
|
-
}).from(gaAiReferrals).where(
|
|
13325
|
+
}).from(gaAiReferrals).where(and11(...aiConditions)).groupBy(
|
|
13223
13326
|
gaAiReferrals.source,
|
|
13224
13327
|
gaAiReferrals.medium,
|
|
13225
13328
|
gaAiReferrals.sourceDimension,
|
|
@@ -13256,7 +13359,7 @@ async function ga4Routes(app, opts) {
|
|
|
13256
13359
|
channelGroup: gaAiReferrals.channelGroup,
|
|
13257
13360
|
sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
|
|
13258
13361
|
users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
|
|
13259
|
-
}).from(gaAiReferrals).where(
|
|
13362
|
+
}).from(gaAiReferrals).where(and11(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
|
|
13260
13363
|
const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
|
|
13261
13364
|
let aiBySessionUsers = 0;
|
|
13262
13365
|
for (const row of aiBySessionRows) {
|
|
@@ -13270,11 +13373,11 @@ async function ga4Routes(app, opts) {
|
|
|
13270
13373
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
13271
13374
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
|
|
13272
13375
|
users: sql5`SUM(${gaSocialReferrals.users})`
|
|
13273
|
-
}).from(gaSocialReferrals).where(
|
|
13376
|
+
}).from(gaSocialReferrals).where(and11(...socialConditions)).groupBy(gaSocialReferrals.source, gaSocialReferrals.medium, gaSocialReferrals.channelGroup).orderBy(sql5`SUM(${gaSocialReferrals.sessions}) DESC`).all();
|
|
13274
13377
|
const socialTotals = app.db.select({
|
|
13275
13378
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
|
|
13276
13379
|
users: sql5`SUM(${gaSocialReferrals.users})`
|
|
13277
|
-
}).from(gaSocialReferrals).where(
|
|
13380
|
+
}).from(gaSocialReferrals).where(and11(...socialConditions)).get();
|
|
13278
13381
|
const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
|
|
13279
13382
|
const total = summaryRow?.totalSessions ?? 0;
|
|
13280
13383
|
const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
|
|
@@ -13365,7 +13468,7 @@ async function ga4Routes(app, opts) {
|
|
|
13365
13468
|
sourceDimension: gaAiReferrals.sourceDimension,
|
|
13366
13469
|
sessions: sql5`SUM(${gaAiReferrals.sessions})`,
|
|
13367
13470
|
users: sql5`SUM(${gaAiReferrals.users})`
|
|
13368
|
-
}).from(gaAiReferrals).where(
|
|
13471
|
+
}).from(gaAiReferrals).where(and11(...conditions)).groupBy(
|
|
13369
13472
|
gaAiReferrals.date,
|
|
13370
13473
|
gaAiReferrals.source,
|
|
13371
13474
|
gaAiReferrals.medium,
|
|
@@ -13387,7 +13490,7 @@ async function ga4Routes(app, opts) {
|
|
|
13387
13490
|
channelGroup: gaSocialReferrals.channelGroup,
|
|
13388
13491
|
sessions: gaSocialReferrals.sessions,
|
|
13389
13492
|
users: gaSocialReferrals.users
|
|
13390
|
-
}).from(gaSocialReferrals).where(
|
|
13493
|
+
}).from(gaSocialReferrals).where(and11(...conditions)).orderBy(gaSocialReferrals.date).all();
|
|
13391
13494
|
return rows;
|
|
13392
13495
|
});
|
|
13393
13496
|
app.get("/projects/:name/ga/social-referral-trend", async (request, _reply) => {
|
|
@@ -13400,7 +13503,7 @@ async function ga4Routes(app, opts) {
|
|
|
13400
13503
|
d.setDate(d.getDate() - n);
|
|
13401
13504
|
return fmt(d);
|
|
13402
13505
|
};
|
|
13403
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
13506
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(
|
|
13404
13507
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13405
13508
|
sql5`${gaSocialReferrals.date} >= ${from}`,
|
|
13406
13509
|
sql5`${gaSocialReferrals.date} < ${to}`
|
|
@@ -13413,7 +13516,7 @@ async function ga4Routes(app, opts) {
|
|
|
13413
13516
|
const sourceCurrent = app.db.select({
|
|
13414
13517
|
source: gaSocialReferrals.source,
|
|
13415
13518
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
13416
|
-
}).from(gaSocialReferrals).where(
|
|
13519
|
+
}).from(gaSocialReferrals).where(and11(
|
|
13417
13520
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13418
13521
|
sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
|
|
13419
13522
|
sql5`${gaSocialReferrals.date} < ${fmt(today)}`
|
|
@@ -13421,7 +13524,7 @@ async function ga4Routes(app, opts) {
|
|
|
13421
13524
|
const sourcePrev = app.db.select({
|
|
13422
13525
|
source: gaSocialReferrals.source,
|
|
13423
13526
|
sessions: sql5`SUM(${gaSocialReferrals.sessions})`
|
|
13424
|
-
}).from(gaSocialReferrals).where(
|
|
13527
|
+
}).from(gaSocialReferrals).where(and11(
|
|
13425
13528
|
eq21(gaSocialReferrals.projectId, project.id),
|
|
13426
13529
|
sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
|
|
13427
13530
|
sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
|
|
@@ -13463,16 +13566,16 @@ async function ga4Routes(app, opts) {
|
|
|
13463
13566
|
return fmt(d);
|
|
13464
13567
|
};
|
|
13465
13568
|
const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
|
|
13466
|
-
const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
13467
|
-
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
13468
|
-
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(
|
|
13469
|
-
const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
13569
|
+
const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
13570
|
+
const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
13571
|
+
const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and11(eq21(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
|
|
13572
|
+
const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
|
|
13470
13573
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13471
13574
|
sql5`${gaAiReferrals.date} >= ${from}`,
|
|
13472
13575
|
sql5`${gaAiReferrals.date} < ${to}`,
|
|
13473
13576
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
13474
13577
|
)).get();
|
|
13475
|
-
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(
|
|
13578
|
+
const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
|
|
13476
13579
|
const todayStr = fmt(today);
|
|
13477
13580
|
const buildTrend = (sum) => {
|
|
13478
13581
|
const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
|
|
@@ -13481,13 +13584,13 @@ async function ga4Routes(app, opts) {
|
|
|
13481
13584
|
const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
|
|
13482
13585
|
return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
|
|
13483
13586
|
};
|
|
13484
|
-
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
13587
|
+
const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
|
|
13485
13588
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13486
13589
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
|
|
13487
13590
|
sql5`${gaAiReferrals.date} < ${todayStr}`,
|
|
13488
13591
|
eq21(gaAiReferrals.sourceDimension, "session")
|
|
13489
13592
|
)).groupBy(gaAiReferrals.source).all();
|
|
13490
|
-
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(
|
|
13593
|
+
const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and11(
|
|
13491
13594
|
eq21(gaAiReferrals.projectId, project.id),
|
|
13492
13595
|
sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
|
|
13493
13596
|
sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
|
|
@@ -13507,8 +13610,8 @@ async function ga4Routes(app, opts) {
|
|
|
13507
13610
|
}
|
|
13508
13611
|
return mover;
|
|
13509
13612
|
};
|
|
13510
|
-
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
13511
|
-
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(
|
|
13613
|
+
const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
|
|
13614
|
+
const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and11(eq21(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
|
|
13512
13615
|
return {
|
|
13513
13616
|
total: buildTrend(sumTotal),
|
|
13514
13617
|
organic: buildTrend(sumOrganic),
|
|
@@ -13530,7 +13633,7 @@ async function ga4Routes(app, opts) {
|
|
|
13530
13633
|
sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
|
|
13531
13634
|
organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
|
|
13532
13635
|
users: sql5`SUM(${gaTrafficSnapshots.users})`
|
|
13533
|
-
}).from(gaTrafficSnapshots).where(
|
|
13636
|
+
}).from(gaTrafficSnapshots).where(and11(...conditions)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
|
|
13534
13637
|
return rows.map((r) => ({
|
|
13535
13638
|
date: r.date,
|
|
13536
13639
|
sessions: r.sessions ?? 0,
|
|
@@ -15183,7 +15286,7 @@ async function wordpressRoutes(app, opts) {
|
|
|
15183
15286
|
|
|
15184
15287
|
// ../api-routes/src/backlinks.ts
|
|
15185
15288
|
import crypto18 from "crypto";
|
|
15186
|
-
import { and as
|
|
15289
|
+
import { and as and13, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
|
|
15187
15290
|
|
|
15188
15291
|
// ../integration-commoncrawl/src/constants.ts
|
|
15189
15292
|
import os3 from "os";
|
|
@@ -15580,7 +15683,7 @@ function pruneCachedRelease(release, opts = {}) {
|
|
|
15580
15683
|
}
|
|
15581
15684
|
|
|
15582
15685
|
// ../api-routes/src/backlinks-filter.ts
|
|
15583
|
-
import { and as
|
|
15686
|
+
import { and as and12, ne, notLike } from "drizzle-orm";
|
|
15584
15687
|
var BACKLINK_FILTER_PATTERNS = [
|
|
15585
15688
|
"*.google.com",
|
|
15586
15689
|
"*.googleusercontent.com",
|
|
@@ -15603,7 +15706,7 @@ function backlinkCrawlerExclusionClause() {
|
|
|
15603
15706
|
conditions.push(ne(backlinkDomains.linkingDomain, pattern));
|
|
15604
15707
|
}
|
|
15605
15708
|
}
|
|
15606
|
-
const combined =
|
|
15709
|
+
const combined = and12(...conditions);
|
|
15607
15710
|
if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
|
|
15608
15711
|
return combined;
|
|
15609
15712
|
}
|
|
@@ -15664,7 +15767,7 @@ function mapRunRow(row) {
|
|
|
15664
15767
|
};
|
|
15665
15768
|
}
|
|
15666
15769
|
function latestSummaryForProject(db, projectId, release) {
|
|
15667
|
-
const condition = release ?
|
|
15770
|
+
const condition = release ? and13(eq22(backlinkSummaries.projectId, projectId), eq22(backlinkSummaries.release, release)) : eq22(backlinkSummaries.projectId, projectId);
|
|
15668
15771
|
return db.select().from(backlinkSummaries).where(condition).orderBy(desc11(backlinkSummaries.queriedAt)).limit(1).get();
|
|
15669
15772
|
}
|
|
15670
15773
|
function parseExcludeCrawlers(value) {
|
|
@@ -15673,11 +15776,11 @@ function parseExcludeCrawlers(value) {
|
|
|
15673
15776
|
return lower === "1" || lower === "true" || lower === "yes";
|
|
15674
15777
|
}
|
|
15675
15778
|
function computeFilteredSummary(db, base) {
|
|
15676
|
-
const baseDomainCondition =
|
|
15779
|
+
const baseDomainCondition = and13(
|
|
15677
15780
|
eq22(backlinkDomains.projectId, base.projectId),
|
|
15678
15781
|
eq22(backlinkDomains.release, base.release)
|
|
15679
15782
|
);
|
|
15680
|
-
const filteredCondition =
|
|
15783
|
+
const filteredCondition = and13(baseDomainCondition, backlinkCrawlerExclusionClause());
|
|
15681
15784
|
const unfilteredAgg = db.select({
|
|
15682
15785
|
count: sql6`count(*)`,
|
|
15683
15786
|
total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
|
|
@@ -15853,11 +15956,11 @@ async function backlinksRoutes(app, opts) {
|
|
|
15853
15956
|
const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
|
|
15854
15957
|
const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
|
|
15855
15958
|
const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
|
|
15856
|
-
const baseDomainCondition =
|
|
15959
|
+
const baseDomainCondition = and13(
|
|
15857
15960
|
eq22(backlinkDomains.projectId, project.id),
|
|
15858
15961
|
eq22(backlinkDomains.release, targetRelease)
|
|
15859
15962
|
);
|
|
15860
|
-
const domainCondition = excludeCrawlers ?
|
|
15963
|
+
const domainCondition = excludeCrawlers ? and13(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
|
|
15861
15964
|
const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
|
|
15862
15965
|
const rows = app.db.select({
|
|
15863
15966
|
linkingDomain: backlinkDomains.linkingDomain,
|
|
@@ -15893,7 +15996,7 @@ async function backlinksRoutes(app, opts) {
|
|
|
15893
15996
|
|
|
15894
15997
|
// ../api-routes/src/traffic.ts
|
|
15895
15998
|
import crypto20 from "crypto";
|
|
15896
|
-
import { eq as eq23, sql as sql7 } from "drizzle-orm";
|
|
15999
|
+
import { and as and14, desc as desc12, eq as eq23, gte, lte, sql as sql7 } from "drizzle-orm";
|
|
15897
16000
|
|
|
15898
16001
|
// ../integration-cloud-run/src/auth.ts
|
|
15899
16002
|
import crypto19 from "crypto";
|
|
@@ -16500,10 +16603,11 @@ function incrementBucket(map, key, fields) {
|
|
|
16500
16603
|
}
|
|
16501
16604
|
|
|
16502
16605
|
// ../api-routes/src/traffic.ts
|
|
16503
|
-
var DEFAULT_SYNC_WINDOW_MINUTES =
|
|
16606
|
+
var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
|
|
16504
16607
|
var DEFAULT_PAGE_SIZE2 = 1e3;
|
|
16505
16608
|
var DEFAULT_MAX_PAGES2 = 5;
|
|
16506
16609
|
var DEFAULT_SAMPLE_LIMIT2 = 100;
|
|
16610
|
+
var MAX_TRACKED_EVENT_IDS = 1e3;
|
|
16507
16611
|
function parseSourceConfig(row) {
|
|
16508
16612
|
return parseJsonColumn(row.configJson, {});
|
|
16509
16613
|
}
|
|
@@ -16664,17 +16768,24 @@ async function trafficRoutes(app, opts) {
|
|
|
16664
16768
|
kind: RunKinds["traffic-sync"],
|
|
16665
16769
|
status: RunStatuses.running,
|
|
16666
16770
|
trigger: RunTriggers.manual,
|
|
16771
|
+
sourceId: sourceRow.id,
|
|
16667
16772
|
startedAt,
|
|
16668
16773
|
createdAt: startedAt
|
|
16669
16774
|
}).run();
|
|
16775
|
+
const markFailed = (msg) => {
|
|
16776
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16777
|
+
app.db.transaction((tx) => {
|
|
16778
|
+
tx.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: failedAt }).where(eq23(runs.id, runId)).run();
|
|
16779
|
+
tx.update(trafficSources).set({ status: TrafficSourceStatuses.error, lastError: msg, updatedAt: failedAt }).where(eq23(trafficSources.id, sourceRow.id)).run();
|
|
16780
|
+
});
|
|
16781
|
+
};
|
|
16670
16782
|
let accessToken;
|
|
16671
16783
|
try {
|
|
16672
16784
|
accessToken = await resolveAccessToken2(credential);
|
|
16673
16785
|
} catch (e) {
|
|
16674
16786
|
const msg = e instanceof Error ? e.message : String(e);
|
|
16675
|
-
|
|
16676
|
-
|
|
16677
|
-
throw validationError(`Failed to resolve Cloud Run access token: ${msg}`);
|
|
16787
|
+
markFailed(msg);
|
|
16788
|
+
throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
|
|
16678
16789
|
}
|
|
16679
16790
|
let allEvents = [];
|
|
16680
16791
|
try {
|
|
@@ -16690,11 +16801,23 @@ async function trafficRoutes(app, opts) {
|
|
|
16690
16801
|
allEvents = page.events;
|
|
16691
16802
|
} catch (e) {
|
|
16692
16803
|
const msg = e instanceof Error ? e.message : String(e);
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
|
|
16696
|
-
|
|
16697
|
-
const
|
|
16804
|
+
markFailed(msg);
|
|
16805
|
+
throw providerError(`Cloud Run pull failed: ${msg}`);
|
|
16806
|
+
}
|
|
16807
|
+
const seenEventIds = new Set(parseJsonColumn(sourceRow.lastEventIds, []));
|
|
16808
|
+
const dedupedEvents = seenEventIds.size === 0 ? allEvents : allEvents.filter((e) => !seenEventIds.has(e.eventId));
|
|
16809
|
+
const newSorted = dedupedEvents.slice().sort((a, b) => a.observedAt < b.observedAt ? 1 : a.observedAt > b.observedAt ? -1 : 0).map((e) => e.eventId);
|
|
16810
|
+
const previousIds = parseJsonColumn(sourceRow.lastEventIds, []);
|
|
16811
|
+
const merged = [];
|
|
16812
|
+
const mergedSet = /* @__PURE__ */ new Set();
|
|
16813
|
+
for (const id of [...newSorted, ...previousIds]) {
|
|
16814
|
+
if (mergedSet.has(id)) continue;
|
|
16815
|
+
mergedSet.add(id);
|
|
16816
|
+
merged.push(id);
|
|
16817
|
+
if (merged.length >= MAX_TRACKED_EVENT_IDS) break;
|
|
16818
|
+
}
|
|
16819
|
+
const nextEventIds = merged;
|
|
16820
|
+
const report = buildTrafficProbeReport(dedupedEvents, { sampleLimit });
|
|
16698
16821
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16699
16822
|
let crawlerBucketRows = 0;
|
|
16700
16823
|
let aiReferralBucketRows = 0;
|
|
@@ -16800,6 +16923,7 @@ async function trafficRoutes(app, opts) {
|
|
|
16800
16923
|
status: TrafficSourceStatuses.connected,
|
|
16801
16924
|
lastSyncedAt: finishedAt,
|
|
16802
16925
|
lastError: null,
|
|
16926
|
+
lastEventIds: JSON.stringify(nextEventIds),
|
|
16803
16927
|
updatedAt: finishedAt
|
|
16804
16928
|
}).where(eq23(trafficSources.id, sourceRow.id)).run();
|
|
16805
16929
|
tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq23(runs.id, runId)).run();
|
|
@@ -16827,6 +16951,177 @@ async function trafficRoutes(app, opts) {
|
|
|
16827
16951
|
};
|
|
16828
16952
|
return response;
|
|
16829
16953
|
});
|
|
16954
|
+
function buildSourceDetail(projectId, row, since) {
|
|
16955
|
+
const crawlerTotals = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
16956
|
+
and14(
|
|
16957
|
+
eq23(crawlerEventsHourly.sourceId, row.id),
|
|
16958
|
+
gte(crawlerEventsHourly.tsHour, since)
|
|
16959
|
+
)
|
|
16960
|
+
).get();
|
|
16961
|
+
const aiTotals = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
16962
|
+
and14(
|
|
16963
|
+
eq23(aiReferralEventsHourly.sourceId, row.id),
|
|
16964
|
+
gte(aiReferralEventsHourly.tsHour, since)
|
|
16965
|
+
)
|
|
16966
|
+
).get();
|
|
16967
|
+
const sampleTotals = app.db.select({ total: sql7`COUNT(*)` }).from(rawEventSamples).where(
|
|
16968
|
+
and14(
|
|
16969
|
+
eq23(rawEventSamples.sourceId, row.id),
|
|
16970
|
+
gte(rawEventSamples.ts, since)
|
|
16971
|
+
)
|
|
16972
|
+
).get();
|
|
16973
|
+
const latestRun = app.db.select().from(runs).where(
|
|
16974
|
+
and14(
|
|
16975
|
+
eq23(runs.projectId, projectId),
|
|
16976
|
+
eq23(runs.kind, RunKinds["traffic-sync"]),
|
|
16977
|
+
eq23(runs.sourceId, row.id)
|
|
16978
|
+
)
|
|
16979
|
+
).orderBy(desc12(runs.startedAt)).limit(1).get();
|
|
16980
|
+
return {
|
|
16981
|
+
...rowToDto(row),
|
|
16982
|
+
totals24h: {
|
|
16983
|
+
crawlerHits: Number(crawlerTotals?.total ?? 0),
|
|
16984
|
+
aiReferralHits: Number(aiTotals?.total ?? 0),
|
|
16985
|
+
sampleCount: Number(sampleTotals?.total ?? 0)
|
|
16986
|
+
},
|
|
16987
|
+
latestRun: latestRun ? {
|
|
16988
|
+
runId: latestRun.id,
|
|
16989
|
+
status: latestRun.status,
|
|
16990
|
+
startedAt: latestRun.startedAt,
|
|
16991
|
+
finishedAt: latestRun.finishedAt ?? null,
|
|
16992
|
+
error: latestRun.error ?? null
|
|
16993
|
+
} : null
|
|
16994
|
+
};
|
|
16995
|
+
}
|
|
16996
|
+
app.get("/projects/:name/traffic/sources", async (request) => {
|
|
16997
|
+
const project = resolveProject(app.db, request.params.name);
|
|
16998
|
+
const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
|
|
16999
|
+
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map(rowToDto);
|
|
17000
|
+
const response = { sources };
|
|
17001
|
+
return response;
|
|
17002
|
+
});
|
|
17003
|
+
app.get("/projects/:name/traffic/status", async (request) => {
|
|
17004
|
+
const project = resolveProject(app.db, request.params.name);
|
|
17005
|
+
const rows = app.db.select().from(trafficSources).where(eq23(trafficSources.projectId, project.id)).orderBy(desc12(trafficSources.createdAt)).all();
|
|
17006
|
+
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
17007
|
+
const sources = rows.filter((row) => row.status !== TrafficSourceStatuses.archived).map((row) => buildSourceDetail(project.id, row, since));
|
|
17008
|
+
const response = { sources };
|
|
17009
|
+
return response;
|
|
17010
|
+
});
|
|
17011
|
+
app.get(
|
|
17012
|
+
"/projects/:name/traffic/sources/:id",
|
|
17013
|
+
async (request) => {
|
|
17014
|
+
const project = resolveProject(app.db, request.params.name);
|
|
17015
|
+
const row = app.db.select().from(trafficSources).where(eq23(trafficSources.id, request.params.id)).get();
|
|
17016
|
+
if (!row || row.projectId !== project.id) {
|
|
17017
|
+
throw notFound("Traffic source", request.params.id);
|
|
17018
|
+
}
|
|
17019
|
+
const since = new Date(Date.now() - 24 * 60 * 6e4).toISOString();
|
|
17020
|
+
return buildSourceDetail(project.id, row, since);
|
|
17021
|
+
}
|
|
17022
|
+
);
|
|
17023
|
+
app.get("/projects/:name/traffic/events", async (request) => {
|
|
17024
|
+
const project = resolveProject(app.db, request.params.name);
|
|
17025
|
+
const now = /* @__PURE__ */ new Date();
|
|
17026
|
+
const defaultSince = new Date(now.getTime() - 24 * 60 * 6e4);
|
|
17027
|
+
const sinceParam = request.query?.since;
|
|
17028
|
+
const untilParam = request.query?.until;
|
|
17029
|
+
const since = sinceParam ? new Date(sinceParam) : defaultSince;
|
|
17030
|
+
const until = untilParam ? new Date(untilParam) : now;
|
|
17031
|
+
if (Number.isNaN(since.getTime())) {
|
|
17032
|
+
throw validationError('"since" must be an ISO-8601 timestamp');
|
|
17033
|
+
}
|
|
17034
|
+
if (Number.isNaN(until.getTime())) {
|
|
17035
|
+
throw validationError('"until" must be an ISO-8601 timestamp');
|
|
17036
|
+
}
|
|
17037
|
+
if (since.getTime() > until.getTime()) {
|
|
17038
|
+
throw validationError('"since" must be \u2264 "until"');
|
|
17039
|
+
}
|
|
17040
|
+
const kindParam = request.query?.kind;
|
|
17041
|
+
let kind = "all";
|
|
17042
|
+
if (kindParam !== void 0) {
|
|
17043
|
+
if (kindParam === "all" || kindParam === TrafficEventKinds.crawler || kindParam === TrafficEventKinds["ai-referral"]) {
|
|
17044
|
+
kind = kindParam;
|
|
17045
|
+
} else {
|
|
17046
|
+
throw validationError(`"kind" must be one of: all, ${TrafficEventKinds.crawler}, ${TrafficEventKinds["ai-referral"]}`);
|
|
17047
|
+
}
|
|
17048
|
+
}
|
|
17049
|
+
const limitParam = request.query?.limit;
|
|
17050
|
+
const requestedLimit = limitParam ? parseInt(limitParam, 10) : 500;
|
|
17051
|
+
if (!Number.isFinite(requestedLimit) || requestedLimit <= 0) {
|
|
17052
|
+
throw validationError('"limit" must be a positive integer');
|
|
17053
|
+
}
|
|
17054
|
+
const limit = Math.min(requestedLimit, 5e3);
|
|
17055
|
+
const sourceIdParam = request.query?.sourceId;
|
|
17056
|
+
const sinceIso = since.toISOString();
|
|
17057
|
+
const untilIso = until.toISOString();
|
|
17058
|
+
const events = [];
|
|
17059
|
+
let crawlerTotal = 0;
|
|
17060
|
+
let aiReferralTotal = 0;
|
|
17061
|
+
if (kind === "all" || kind === TrafficEventKinds.crawler) {
|
|
17062
|
+
const crawlerFilters = [
|
|
17063
|
+
eq23(crawlerEventsHourly.projectId, project.id),
|
|
17064
|
+
gte(crawlerEventsHourly.tsHour, sinceIso),
|
|
17065
|
+
lte(crawlerEventsHourly.tsHour, untilIso)
|
|
17066
|
+
];
|
|
17067
|
+
if (sourceIdParam) crawlerFilters.push(eq23(crawlerEventsHourly.sourceId, sourceIdParam));
|
|
17068
|
+
const crawlerWhere = and14(...crawlerFilters);
|
|
17069
|
+
const total = app.db.select({ total: sql7`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(crawlerWhere).get();
|
|
17070
|
+
crawlerTotal = Number(total?.total ?? 0);
|
|
17071
|
+
const rows = app.db.select().from(crawlerEventsHourly).where(crawlerWhere).orderBy(desc12(crawlerEventsHourly.tsHour)).limit(limit).all();
|
|
17072
|
+
for (const r of rows) {
|
|
17073
|
+
events.push({
|
|
17074
|
+
kind: TrafficEventKinds.crawler,
|
|
17075
|
+
sourceId: r.sourceId,
|
|
17076
|
+
tsHour: r.tsHour,
|
|
17077
|
+
botId: r.botId,
|
|
17078
|
+
operator: r.operator,
|
|
17079
|
+
verificationStatus: r.verificationStatus,
|
|
17080
|
+
pathNormalized: r.pathNormalized,
|
|
17081
|
+
status: r.status,
|
|
17082
|
+
hits: r.hits
|
|
17083
|
+
});
|
|
17084
|
+
}
|
|
17085
|
+
}
|
|
17086
|
+
if (kind === "all" || kind === TrafficEventKinds["ai-referral"]) {
|
|
17087
|
+
const aiFilters = [
|
|
17088
|
+
eq23(aiReferralEventsHourly.projectId, project.id),
|
|
17089
|
+
gte(aiReferralEventsHourly.tsHour, sinceIso),
|
|
17090
|
+
lte(aiReferralEventsHourly.tsHour, untilIso)
|
|
17091
|
+
];
|
|
17092
|
+
if (sourceIdParam) aiFilters.push(eq23(aiReferralEventsHourly.sourceId, sourceIdParam));
|
|
17093
|
+
const aiWhere = and14(...aiFilters);
|
|
17094
|
+
const total = app.db.select({ total: sql7`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(aiWhere).get();
|
|
17095
|
+
aiReferralTotal = Number(total?.total ?? 0);
|
|
17096
|
+
const rows = app.db.select().from(aiReferralEventsHourly).where(aiWhere).orderBy(desc12(aiReferralEventsHourly.tsHour)).limit(limit).all();
|
|
17097
|
+
for (const r of rows) {
|
|
17098
|
+
events.push({
|
|
17099
|
+
kind: TrafficEventKinds["ai-referral"],
|
|
17100
|
+
sourceId: r.sourceId,
|
|
17101
|
+
tsHour: r.tsHour,
|
|
17102
|
+
product: r.product,
|
|
17103
|
+
operator: r.operator,
|
|
17104
|
+
sourceDomain: r.sourceDomain,
|
|
17105
|
+
evidenceType: r.evidenceType,
|
|
17106
|
+
landingPathNormalized: r.landingPathNormalized,
|
|
17107
|
+
status: r.status,
|
|
17108
|
+
hits: r.sessionsOrHits
|
|
17109
|
+
});
|
|
17110
|
+
}
|
|
17111
|
+
}
|
|
17112
|
+
events.sort((a, b) => a.tsHour < b.tsHour ? 1 : a.tsHour > b.tsHour ? -1 : 0);
|
|
17113
|
+
const trimmed = events.slice(0, limit);
|
|
17114
|
+
const response = {
|
|
17115
|
+
windowStart: sinceIso,
|
|
17116
|
+
windowEnd: untilIso,
|
|
17117
|
+
totals: {
|
|
17118
|
+
crawlerHits: crawlerTotal,
|
|
17119
|
+
aiReferralHits: aiReferralTotal
|
|
17120
|
+
},
|
|
17121
|
+
events: trimmed
|
|
17122
|
+
};
|
|
17123
|
+
return response;
|
|
17124
|
+
});
|
|
16830
17125
|
}
|
|
16831
17126
|
|
|
16832
17127
|
// ../api-routes/src/doctor/checks/bing-auth.ts
|
|
@@ -20198,7 +20493,7 @@ import crypto22 from "crypto";
|
|
|
20198
20493
|
import fs7 from "fs";
|
|
20199
20494
|
import path9 from "path";
|
|
20200
20495
|
import os5 from "os";
|
|
20201
|
-
import { and as
|
|
20496
|
+
import { and as and15, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
|
|
20202
20497
|
|
|
20203
20498
|
// src/run-telemetry.ts
|
|
20204
20499
|
import crypto21 from "crypto";
|
|
@@ -20577,7 +20872,7 @@ var JobRunner = class {
|
|
|
20577
20872
|
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
20578
20873
|
}
|
|
20579
20874
|
if (existingRun.status === "queued") {
|
|
20580
|
-
this.db.update(runs).set({ status: "running", startedAt: now }).where(
|
|
20875
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and15(eq24(runs.id, runId), eq24(runs.status, "queued"))).run();
|
|
20581
20876
|
}
|
|
20582
20877
|
this.throwIfRunCancelled(runId);
|
|
20583
20878
|
const project = this.db.select().from(projects).where(eq24(projects.id, projectId)).get();
|
|
@@ -20936,7 +21231,7 @@ function buildPhases(input) {
|
|
|
20936
21231
|
|
|
20937
21232
|
// src/gsc-sync.ts
|
|
20938
21233
|
import crypto23 from "crypto";
|
|
20939
|
-
import { eq as eq25, and as
|
|
21234
|
+
import { eq as eq25, and as and16, sql as sql9 } from "drizzle-orm";
|
|
20940
21235
|
var log2 = createLogger("GscSync");
|
|
20941
21236
|
function formatDate3(d) {
|
|
20942
21237
|
return d.toISOString().split("T")[0];
|
|
@@ -20988,7 +21283,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
20988
21283
|
});
|
|
20989
21284
|
log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
|
|
20990
21285
|
db.delete(gscSearchData).where(
|
|
20991
|
-
|
|
21286
|
+
and16(
|
|
20992
21287
|
eq25(gscSearchData.projectId, projectId),
|
|
20993
21288
|
sql9`${gscSearchData.date} >= ${startDate}`,
|
|
20994
21289
|
sql9`${gscSearchData.date} <= ${endDate}`
|
|
@@ -21077,7 +21372,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
21077
21372
|
}
|
|
21078
21373
|
}
|
|
21079
21374
|
const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
|
|
21080
|
-
db.delete(gscCoverageSnapshots).where(
|
|
21375
|
+
db.delete(gscCoverageSnapshots).where(and16(eq25(gscCoverageSnapshots.projectId, projectId), eq25(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
21081
21376
|
db.insert(gscCoverageSnapshots).values({
|
|
21082
21377
|
id: crypto23.randomUUID(),
|
|
21083
21378
|
projectId,
|
|
@@ -21100,7 +21395,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
21100
21395
|
|
|
21101
21396
|
// src/gsc-inspect-sitemap.ts
|
|
21102
21397
|
import crypto24 from "crypto";
|
|
21103
|
-
import { eq as eq26, and as
|
|
21398
|
+
import { eq as eq26, and as and17 } from "drizzle-orm";
|
|
21104
21399
|
|
|
21105
21400
|
// src/sitemap-parser.ts
|
|
21106
21401
|
var log3 = createLogger("SitemapParser");
|
|
@@ -21316,7 +21611,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
21316
21611
|
}
|
|
21317
21612
|
}
|
|
21318
21613
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
21319
|
-
db.delete(gscCoverageSnapshots).where(
|
|
21614
|
+
db.delete(gscCoverageSnapshots).where(and17(eq26(gscCoverageSnapshots.projectId, projectId), eq26(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
21320
21615
|
db.insert(gscCoverageSnapshots).values({
|
|
21321
21616
|
id: crypto24.randomUUID(),
|
|
21322
21617
|
projectId,
|
|
@@ -21340,7 +21635,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
21340
21635
|
|
|
21341
21636
|
// src/bing-inspect-sitemap.ts
|
|
21342
21637
|
import crypto25 from "crypto";
|
|
21343
|
-
import { eq as eq27, desc as
|
|
21638
|
+
import { eq as eq27, desc as desc13 } from "drizzle-orm";
|
|
21344
21639
|
var log5 = createLogger("BingInspectSitemap");
|
|
21345
21640
|
function parseBingDate2(value) {
|
|
21346
21641
|
if (!value) return null;
|
|
@@ -21461,7 +21756,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
21461
21756
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
21462
21757
|
}
|
|
21463
21758
|
}
|
|
21464
|
-
const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(
|
|
21759
|
+
const allInspections = db.select().from(bingUrlInspections).where(eq27(bingUrlInspections.projectId, projectId)).orderBy(desc13(bingUrlInspections.inspectedAt)).all();
|
|
21465
21760
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
21466
21761
|
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
21467
21762
|
for (const row of allInspections) {
|
|
@@ -21527,7 +21822,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
|
|
|
21527
21822
|
// src/commoncrawl-sync.ts
|
|
21528
21823
|
import crypto26 from "crypto";
|
|
21529
21824
|
import path10 from "path";
|
|
21530
|
-
import { and as
|
|
21825
|
+
import { and as and18, eq as eq28, sql as sql10 } from "drizzle-orm";
|
|
21531
21826
|
var log6 = createLogger("CommonCrawlSync");
|
|
21532
21827
|
var INSERT_CHUNK_SIZE = 1e4;
|
|
21533
21828
|
function defaultDeps() {
|
|
@@ -21718,7 +22013,7 @@ function computeSummary(rows) {
|
|
|
21718
22013
|
// src/backlink-extract.ts
|
|
21719
22014
|
import crypto27 from "crypto";
|
|
21720
22015
|
import fs8 from "fs";
|
|
21721
|
-
import { and as
|
|
22016
|
+
import { and as and19, desc as desc14, eq as eq29 } from "drizzle-orm";
|
|
21722
22017
|
var log7 = createLogger("BacklinkExtract");
|
|
21723
22018
|
function defaultDeps2() {
|
|
21724
22019
|
return {
|
|
@@ -21736,7 +22031,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
21736
22031
|
if (!project) {
|
|
21737
22032
|
throw new Error(`Project not found: ${projectId}`);
|
|
21738
22033
|
}
|
|
21739
|
-
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(
|
|
22034
|
+
const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc14(ccReleaseSyncs.createdAt)).limit(1).get();
|
|
21740
22035
|
if (!sync) {
|
|
21741
22036
|
throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
|
|
21742
22037
|
}
|
|
@@ -21764,7 +22059,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
|
|
|
21764
22059
|
const targetDomain = project.canonicalDomain;
|
|
21765
22060
|
db.transaction((tx) => {
|
|
21766
22061
|
tx.delete(backlinkDomains).where(
|
|
21767
|
-
|
|
22062
|
+
and19(eq29(backlinkDomains.projectId, projectId), eq29(backlinkDomains.release, release))
|
|
21768
22063
|
).run();
|
|
21769
22064
|
if (rows.length > 0) {
|
|
21770
22065
|
const values = rows.map((r) => ({
|
|
@@ -21886,8 +22181,11 @@ var ProviderRegistry = class {
|
|
|
21886
22181
|
|
|
21887
22182
|
// src/scheduler.ts
|
|
21888
22183
|
import cron from "node-cron";
|
|
21889
|
-
import { eq as eq30 } from "drizzle-orm";
|
|
22184
|
+
import { and as and20, eq as eq30 } from "drizzle-orm";
|
|
21890
22185
|
var log8 = createLogger("Scheduler");
|
|
22186
|
+
function taskKey(projectId, kind) {
|
|
22187
|
+
return `${projectId}::${kind}`;
|
|
22188
|
+
}
|
|
21891
22189
|
var Scheduler = class {
|
|
21892
22190
|
db;
|
|
21893
22191
|
callbacks;
|
|
@@ -21903,78 +22201,110 @@ var Scheduler = class {
|
|
|
21903
22201
|
const missedRunAt = schedule.nextRunAt;
|
|
21904
22202
|
this.registerCronTask(schedule);
|
|
21905
22203
|
if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
|
|
21906
|
-
log8.info("run.catch-up", { projectId: schedule.projectId, missedRunAt });
|
|
21907
|
-
this.triggerRun(schedule.id, schedule.projectId);
|
|
22204
|
+
log8.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
|
|
22205
|
+
this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
|
|
21908
22206
|
}
|
|
21909
22207
|
}
|
|
21910
22208
|
log8.info("started", { scheduleCount: allSchedules.length });
|
|
21911
22209
|
}
|
|
21912
22210
|
/** Stop all cron tasks for graceful shutdown. */
|
|
21913
22211
|
stop() {
|
|
21914
|
-
for (const [
|
|
21915
|
-
this.stopTask(
|
|
22212
|
+
for (const [key, task] of this.tasks) {
|
|
22213
|
+
this.stopTask(key, task, "Stopped");
|
|
21916
22214
|
}
|
|
21917
22215
|
this.tasks.clear();
|
|
21918
22216
|
}
|
|
21919
|
-
/**
|
|
21920
|
-
|
|
21921
|
-
|
|
22217
|
+
/**
|
|
22218
|
+
* Add or update a cron registration at runtime (called when schedule API
|
|
22219
|
+
* is used). Keyed by `(projectId, kind)` so a project's traffic-sync and
|
|
22220
|
+
* answer-visibility schedules can coexist independently.
|
|
22221
|
+
*/
|
|
22222
|
+
upsert(projectId, kind) {
|
|
22223
|
+
const key = taskKey(projectId, kind);
|
|
22224
|
+
const existing = this.tasks.get(key);
|
|
21922
22225
|
if (existing) {
|
|
21923
|
-
this.stopTask(
|
|
21924
|
-
this.tasks.delete(
|
|
22226
|
+
this.stopTask(key, existing, "Stopped");
|
|
22227
|
+
this.tasks.delete(key);
|
|
21925
22228
|
}
|
|
21926
|
-
const schedule = this.db.select().from(schedules).where(eq30(schedules.projectId, projectId)).get();
|
|
22229
|
+
const schedule = this.db.select().from(schedules).where(and20(eq30(schedules.projectId, projectId), eq30(schedules.kind, kind))).get();
|
|
21927
22230
|
if (schedule && schedule.enabled === 1) {
|
|
21928
22231
|
this.registerCronTask(schedule);
|
|
21929
22232
|
}
|
|
21930
22233
|
}
|
|
21931
|
-
/** Remove a cron registration (
|
|
21932
|
-
remove(projectId) {
|
|
21933
|
-
const
|
|
22234
|
+
/** Remove a single cron registration (kind-scoped). */
|
|
22235
|
+
remove(projectId, kind) {
|
|
22236
|
+
const key = taskKey(projectId, kind);
|
|
22237
|
+
const existing = this.tasks.get(key);
|
|
21934
22238
|
if (existing) {
|
|
21935
|
-
this.stopTask(
|
|
21936
|
-
this.tasks.delete(
|
|
22239
|
+
this.stopTask(key, existing, "Removed");
|
|
22240
|
+
this.tasks.delete(key);
|
|
21937
22241
|
}
|
|
21938
22242
|
}
|
|
21939
|
-
|
|
22243
|
+
/** Remove ALL cron registrations for a project (used on project delete). */
|
|
22244
|
+
removeAllForProject(projectId) {
|
|
22245
|
+
for (const kind of Object.values(SchedulableRunKinds)) {
|
|
22246
|
+
this.remove(projectId, kind);
|
|
22247
|
+
}
|
|
22248
|
+
}
|
|
22249
|
+
stopTask(key, task, verb) {
|
|
21940
22250
|
task.stop();
|
|
21941
22251
|
task.destroy();
|
|
21942
|
-
log8.info(`task.${verb.toLowerCase()}`, {
|
|
22252
|
+
log8.info(`task.${verb.toLowerCase()}`, { key });
|
|
21943
22253
|
}
|
|
21944
22254
|
registerCronTask(schedule) {
|
|
21945
22255
|
const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
|
|
22256
|
+
const kind = schedule.kind;
|
|
21946
22257
|
if (!cron.validate(cronExpr)) {
|
|
21947
|
-
log8.error("cron.invalid", { projectId, cronExpr });
|
|
22258
|
+
log8.error("cron.invalid", { projectId, kind, cronExpr });
|
|
21948
22259
|
return;
|
|
21949
22260
|
}
|
|
21950
22261
|
const task = cron.schedule(cronExpr, () => {
|
|
21951
|
-
this.triggerRun(scheduleId, projectId);
|
|
22262
|
+
this.triggerRun(scheduleId, projectId, kind);
|
|
21952
22263
|
}, {
|
|
21953
22264
|
timezone
|
|
21954
22265
|
});
|
|
21955
|
-
this.tasks.set(projectId, task);
|
|
22266
|
+
this.tasks.set(taskKey(projectId, kind), task);
|
|
21956
22267
|
this.db.update(schedules).set({
|
|
21957
22268
|
nextRunAt: task.getNextRun()?.toISOString() ?? null,
|
|
21958
22269
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21959
22270
|
}).where(eq30(schedules.id, scheduleId)).run();
|
|
21960
22271
|
const label = schedule.preset ?? cronExpr;
|
|
21961
|
-
log8.info("cron.registered", { projectId, schedule: label, timezone });
|
|
22272
|
+
log8.info("cron.registered", { projectId, kind, schedule: label, timezone });
|
|
21962
22273
|
}
|
|
21963
|
-
triggerRun(scheduleId, projectId) {
|
|
22274
|
+
triggerRun(scheduleId, projectId, kind) {
|
|
21964
22275
|
try {
|
|
21965
22276
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21966
22277
|
const currentSchedule = this.db.select().from(schedules).where(eq30(schedules.id, scheduleId)).get();
|
|
21967
22278
|
if (!currentSchedule || currentSchedule.enabled !== 1) {
|
|
21968
|
-
log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
|
|
21969
|
-
this.remove(projectId);
|
|
22279
|
+
log8.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
22280
|
+
this.remove(projectId, kind);
|
|
21970
22281
|
return;
|
|
21971
22282
|
}
|
|
21972
|
-
const task = this.tasks.get(projectId);
|
|
22283
|
+
const task = this.tasks.get(taskKey(projectId, kind));
|
|
21973
22284
|
const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
|
|
21974
22285
|
const project = this.db.select().from(projects).where(eq30(projects.id, projectId)).get();
|
|
21975
22286
|
if (!project) {
|
|
21976
|
-
log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
|
|
21977
|
-
this.remove(projectId);
|
|
22287
|
+
log8.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
22288
|
+
this.remove(projectId, kind);
|
|
22289
|
+
return;
|
|
22290
|
+
}
|
|
22291
|
+
if (kind === SchedulableRunKinds["traffic-sync"]) {
|
|
22292
|
+
const sourceId = currentSchedule.sourceId;
|
|
22293
|
+
if (!sourceId) {
|
|
22294
|
+
log8.warn("traffic-sync.missing-source", { scheduleId, projectId });
|
|
22295
|
+
return;
|
|
22296
|
+
}
|
|
22297
|
+
if (!this.callbacks.onTrafficSyncRequested) {
|
|
22298
|
+
log8.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
|
|
22299
|
+
return;
|
|
22300
|
+
}
|
|
22301
|
+
this.db.update(schedules).set({
|
|
22302
|
+
lastRunAt: now,
|
|
22303
|
+
nextRunAt,
|
|
22304
|
+
updatedAt: now
|
|
22305
|
+
}).where(eq30(schedules.id, currentSchedule.id)).run();
|
|
22306
|
+
log8.info("traffic-sync.triggered", { projectName: project.name, sourceId });
|
|
22307
|
+
this.callbacks.onTrafficSyncRequested(project.name, sourceId);
|
|
21978
22308
|
return;
|
|
21979
22309
|
}
|
|
21980
22310
|
const projectLocations = parseJsonColumn(project.locations, []);
|
|
@@ -22014,13 +22344,13 @@ var Scheduler = class {
|
|
|
22014
22344
|
log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
|
|
22015
22345
|
this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
|
|
22016
22346
|
} catch (err) {
|
|
22017
|
-
log8.error("trigger.error", { scheduleId, projectId, error: err instanceof Error ? err.message : String(err) });
|
|
22347
|
+
log8.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
|
|
22018
22348
|
}
|
|
22019
22349
|
}
|
|
22020
22350
|
};
|
|
22021
22351
|
|
|
22022
22352
|
// src/notifier.ts
|
|
22023
|
-
import { eq as eq31, desc as
|
|
22353
|
+
import { eq as eq31, desc as desc15, and as and21, or as or4 } from "drizzle-orm";
|
|
22024
22354
|
import crypto28 from "crypto";
|
|
22025
22355
|
var log9 = createLogger("Notifier");
|
|
22026
22356
|
var Notifier = class {
|
|
@@ -22126,11 +22456,11 @@ var Notifier = class {
|
|
|
22126
22456
|
}
|
|
22127
22457
|
computeTransitions(runId, projectId) {
|
|
22128
22458
|
const recentRuns = this.db.select().from(runs).where(
|
|
22129
|
-
|
|
22459
|
+
and21(
|
|
22130
22460
|
eq31(runs.projectId, projectId),
|
|
22131
22461
|
or4(eq31(runs.status, "completed"), eq31(runs.status, "partial"))
|
|
22132
22462
|
)
|
|
22133
|
-
).orderBy(
|
|
22463
|
+
).orderBy(desc15(runs.createdAt)).limit(2).all();
|
|
22134
22464
|
if (recentRuns.length < 2) return [];
|
|
22135
22465
|
const currentRunId = recentRuns[0].id;
|
|
22136
22466
|
const previousRunId = recentRuns[1].id;
|
|
@@ -22612,7 +22942,7 @@ function resolveSessionProviderAndModel(config, opts) {
|
|
|
22612
22942
|
|
|
22613
22943
|
// src/agent/memory-store.ts
|
|
22614
22944
|
import crypto29 from "crypto";
|
|
22615
|
-
import { and as
|
|
22945
|
+
import { and as and22, desc as desc16, eq as eq32, like as like2, sql as sql11 } from "drizzle-orm";
|
|
22616
22946
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
22617
22947
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
22618
22948
|
function rowToDto2(row) {
|
|
@@ -22626,7 +22956,7 @@ function rowToDto2(row) {
|
|
|
22626
22956
|
};
|
|
22627
22957
|
}
|
|
22628
22958
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
22629
|
-
const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(
|
|
22959
|
+
const query = db.select().from(agentMemory).where(eq32(agentMemory.projectId, projectId)).orderBy(desc16(agentMemory.updatedAt));
|
|
22630
22960
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
22631
22961
|
return rows.map(rowToDto2);
|
|
22632
22962
|
}
|
|
@@ -22657,12 +22987,12 @@ function upsertMemoryEntry(db, args) {
|
|
|
22657
22987
|
updatedAt: now
|
|
22658
22988
|
}
|
|
22659
22989
|
}).run();
|
|
22660
|
-
const row = db.select().from(agentMemory).where(
|
|
22990
|
+
const row = db.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, args.key))).get();
|
|
22661
22991
|
if (!row) throw new Error("memory upsert produced no row");
|
|
22662
22992
|
return rowToDto2(row);
|
|
22663
22993
|
}
|
|
22664
22994
|
function deleteMemoryEntry(db, projectId, key) {
|
|
22665
|
-
const result = db.delete(agentMemory).where(
|
|
22995
|
+
const result = db.delete(agentMemory).where(and22(eq32(agentMemory.projectId, projectId), eq32(agentMemory.key, key))).run();
|
|
22666
22996
|
const changes = result.changes ?? 0;
|
|
22667
22997
|
return changes > 0;
|
|
22668
22998
|
}
|
|
@@ -22691,16 +23021,16 @@ function writeCompactionNote(db, args) {
|
|
|
22691
23021
|
}).run();
|
|
22692
23022
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
22693
23023
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
22694
|
-
|
|
23024
|
+
and22(
|
|
22695
23025
|
eq32(agentMemory.projectId, args.projectId),
|
|
22696
23026
|
like2(agentMemory.key, `${sessionPrefix}%`)
|
|
22697
23027
|
)
|
|
22698
|
-
).orderBy(
|
|
23028
|
+
).orderBy(desc16(agentMemory.updatedAt)).all();
|
|
22699
23029
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
22700
23030
|
if (stale.length > 0) {
|
|
22701
23031
|
tx.delete(agentMemory).where(sql11`${agentMemory.id} IN (${sql11.join(stale.map((s) => sql11`${s}`), sql11`, `)})`).run();
|
|
22702
23032
|
}
|
|
22703
|
-
const row = tx.select().from(agentMemory).where(
|
|
23033
|
+
const row = tx.select().from(agentMemory).where(and22(eq32(agentMemory.projectId, args.projectId), eq32(agentMemory.key, key))).get();
|
|
22704
23034
|
if (row) inserted = rowToDto2(row);
|
|
22705
23035
|
});
|
|
22706
23036
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -24367,6 +24697,11 @@ async function createServer(opts) {
|
|
|
24367
24697
|
jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
|
|
24368
24698
|
app.log.error({ runId, err }, "Scheduled job runner failed");
|
|
24369
24699
|
});
|
|
24700
|
+
},
|
|
24701
|
+
onTrafficSyncRequested: (projectName, sourceId) => {
|
|
24702
|
+
aeroClient.trafficSync(projectName, sourceId).catch((err) => {
|
|
24703
|
+
app.log.error({ projectName, sourceId, err: err instanceof Error ? err.message : String(err) }, "Scheduled traffic sync failed");
|
|
24704
|
+
});
|
|
24370
24705
|
}
|
|
24371
24706
|
});
|
|
24372
24707
|
const providerSummary = API_ADAPTERS.map((adapter) => ({
|
|
@@ -24898,12 +25233,12 @@ async function createServer(opts) {
|
|
|
24898
25233
|
return null;
|
|
24899
25234
|
}
|
|
24900
25235
|
},
|
|
24901
|
-
onScheduleUpdated: (action, projectId) => {
|
|
24902
|
-
if (action === "upsert") scheduler.upsert(projectId);
|
|
24903
|
-
if (action === "delete") scheduler.remove(projectId);
|
|
25236
|
+
onScheduleUpdated: (action, projectId, kind) => {
|
|
25237
|
+
if (action === "upsert") scheduler.upsert(projectId, kind);
|
|
25238
|
+
if (action === "delete") scheduler.remove(projectId, kind);
|
|
24904
25239
|
},
|
|
24905
25240
|
onProjectDeleted: (projectId) => {
|
|
24906
|
-
scheduler.
|
|
25241
|
+
scheduler.removeAllForProject(projectId);
|
|
24907
25242
|
},
|
|
24908
25243
|
getTelemetryStatus: () => {
|
|
24909
25244
|
const enabled = isTelemetryEnabled();
|