@ainyc/canonry 1.13.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/assets/index-CQoA5dn_.css +1 -0
- package/assets/assets/index-s9Kz4WUv.js +245 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-65PXJTPN.js → chunk-WCZMFJUY.js} +568 -68
- package/dist/cli.js +363 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/assets/assets/index-B2IIQpXZ.js +0 -243
- package/assets/assets/index-MP6oQcCa.css +0 -1
|
@@ -63,6 +63,15 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
normalizeGoogleConfig(parsed);
|
|
66
|
+
const portOverride = process.env.CANONRY_PORT?.trim();
|
|
67
|
+
if (portOverride) {
|
|
68
|
+
try {
|
|
69
|
+
const url = new URL(parsed.apiUrl);
|
|
70
|
+
url.port = portOverride;
|
|
71
|
+
parsed.apiUrl = url.origin;
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
66
75
|
return parsed;
|
|
67
76
|
}
|
|
68
77
|
function saveConfig(config) {
|
|
@@ -151,7 +160,7 @@ function trackEvent(event, properties) {
|
|
|
151
160
|
|
|
152
161
|
// src/server.ts
|
|
153
162
|
import { createRequire as createRequire2 } from "module";
|
|
154
|
-
import
|
|
163
|
+
import crypto18 from "crypto";
|
|
155
164
|
import fs2 from "fs";
|
|
156
165
|
import path2 from "path";
|
|
157
166
|
import { fileURLToPath } from "url";
|
|
@@ -174,6 +183,7 @@ __export(schema_exports, {
|
|
|
174
183
|
auditLog: () => auditLog,
|
|
175
184
|
competitors: () => competitors,
|
|
176
185
|
googleConnections: () => googleConnections,
|
|
186
|
+
gscCoverageSnapshots: () => gscCoverageSnapshots,
|
|
177
187
|
gscSearchData: () => gscSearchData,
|
|
178
188
|
gscUrlInspections: () => gscUrlInspections,
|
|
179
189
|
keywords: () => keywords,
|
|
@@ -196,6 +206,8 @@ var projects = sqliteTable("projects", {
|
|
|
196
206
|
tags: text("tags").notNull().default("[]"),
|
|
197
207
|
labels: text("labels").notNull().default("{}"),
|
|
198
208
|
providers: text("providers").notNull().default("[]"),
|
|
209
|
+
locations: text("locations").notNull().default("[]"),
|
|
210
|
+
defaultLocation: text("default_location"),
|
|
199
211
|
configSource: text("config_source").notNull().default("cli"),
|
|
200
212
|
configRevision: integer("config_revision").notNull().default(1),
|
|
201
213
|
createdAt: text("created_at").notNull(),
|
|
@@ -225,6 +237,7 @@ var runs = sqliteTable("runs", {
|
|
|
225
237
|
kind: text("kind").notNull().default("answer-visibility"),
|
|
226
238
|
status: text("status").notNull().default("queued"),
|
|
227
239
|
trigger: text("trigger").notNull().default("manual"),
|
|
240
|
+
location: text("location"),
|
|
228
241
|
startedAt: text("started_at"),
|
|
229
242
|
finishedAt: text("finished_at"),
|
|
230
243
|
error: text("error"),
|
|
@@ -243,6 +256,7 @@ var querySnapshots = sqliteTable("query_snapshots", {
|
|
|
243
256
|
answerText: text("answer_text"),
|
|
244
257
|
citedDomains: text("cited_domains").notNull().default("[]"),
|
|
245
258
|
competitorOverlap: text("competitor_overlap").notNull().default("[]"),
|
|
259
|
+
location: text("location"),
|
|
246
260
|
rawResponse: text("raw_response"),
|
|
247
261
|
createdAt: text("created_at").notNull()
|
|
248
262
|
}, (table) => [
|
|
@@ -306,6 +320,7 @@ var googleConnections = sqliteTable("google_connections", {
|
|
|
306
320
|
domain: text("domain").notNull(),
|
|
307
321
|
connectionType: text("connection_type").notNull(),
|
|
308
322
|
propertyId: text("property_id"),
|
|
323
|
+
sitemapUrl: text("sitemap_url"),
|
|
309
324
|
accessToken: text("access_token"),
|
|
310
325
|
refreshToken: text("refresh_token"),
|
|
311
326
|
tokenExpiresAt: text("token_expires_at"),
|
|
@@ -356,6 +371,19 @@ var gscUrlInspections = sqliteTable("gsc_url_inspections", {
|
|
|
356
371
|
index("idx_gsc_inspect_run").on(table.syncRunId),
|
|
357
372
|
index("idx_gsc_inspect_url_time").on(table.url, table.inspectedAt)
|
|
358
373
|
]);
|
|
374
|
+
var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
|
|
375
|
+
id: text("id").primaryKey(),
|
|
376
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
377
|
+
syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "cascade" }),
|
|
378
|
+
date: text("date").notNull(),
|
|
379
|
+
indexed: integer("indexed").notNull().default(0),
|
|
380
|
+
notIndexed: integer("not_indexed").notNull().default(0),
|
|
381
|
+
reasonBreakdown: text("reason_breakdown").notNull().default("{}"),
|
|
382
|
+
createdAt: text("created_at").notNull()
|
|
383
|
+
}, (table) => [
|
|
384
|
+
index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
|
|
385
|
+
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
386
|
+
]);
|
|
359
387
|
var usageCounters = sqliteTable("usage_counters", {
|
|
360
388
|
id: text("id").primaryKey(),
|
|
361
389
|
scope: text("scope").notNull(),
|
|
@@ -573,7 +601,28 @@ var MIGRATIONS = [
|
|
|
573
601
|
)`,
|
|
574
602
|
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_project_url ON gsc_url_inspections(project_id, url)`,
|
|
575
603
|
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_run ON gsc_url_inspections(sync_run_id)`,
|
|
576
|
-
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_url_time ON gsc_url_inspections(url, inspected_at)
|
|
604
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_inspect_url_time ON gsc_url_inspections(url, inspected_at)`,
|
|
605
|
+
// v7: GSC coverage snapshots for historical tracking
|
|
606
|
+
`CREATE TABLE IF NOT EXISTS gsc_coverage_snapshots (
|
|
607
|
+
id TEXT PRIMARY KEY,
|
|
608
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
609
|
+
sync_run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
|
|
610
|
+
date TEXT NOT NULL,
|
|
611
|
+
indexed INTEGER NOT NULL DEFAULT 0,
|
|
612
|
+
not_indexed INTEGER NOT NULL DEFAULT 0,
|
|
613
|
+
reason_breakdown TEXT NOT NULL DEFAULT '{}',
|
|
614
|
+
created_at TEXT NOT NULL
|
|
615
|
+
)`,
|
|
616
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_project_date ON gsc_coverage_snapshots(project_id, date)`,
|
|
617
|
+
`CREATE INDEX IF NOT EXISTS idx_gsc_coverage_snap_run ON gsc_coverage_snapshots(sync_run_id)`,
|
|
618
|
+
// v8: Location-aware sweeps — project locations + snapshot location tag
|
|
619
|
+
`ALTER TABLE projects ADD COLUMN locations TEXT NOT NULL DEFAULT '[]'`,
|
|
620
|
+
`ALTER TABLE projects ADD COLUMN default_location TEXT`,
|
|
621
|
+
`ALTER TABLE query_snapshots ADD COLUMN location TEXT`,
|
|
622
|
+
// v9: Add location column to runs for per-location run tracking
|
|
623
|
+
`ALTER TABLE runs ADD COLUMN location TEXT`,
|
|
624
|
+
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
625
|
+
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`
|
|
577
626
|
];
|
|
578
627
|
function migrate(db) {
|
|
579
628
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -604,6 +653,13 @@ function parseProviderName(input) {
|
|
|
604
653
|
const lower = input.trim().toLowerCase();
|
|
605
654
|
return PROVIDER_NAMES.includes(lower) ? lower : void 0;
|
|
606
655
|
}
|
|
656
|
+
var locationContextSchema = z.object({
|
|
657
|
+
label: z.string().min(1),
|
|
658
|
+
city: z.string().min(1),
|
|
659
|
+
region: z.string().min(1),
|
|
660
|
+
country: z.string().length(2),
|
|
661
|
+
timezone: z.string().optional()
|
|
662
|
+
});
|
|
607
663
|
|
|
608
664
|
// ../contracts/src/notification.ts
|
|
609
665
|
import { z as z2 } from "zod";
|
|
@@ -664,6 +720,8 @@ var configSpecSchema = z3.object({
|
|
|
664
720
|
keywords: z3.array(z3.string().min(1)).optional().default([]),
|
|
665
721
|
competitors: z3.array(z3.string().min(1)).optional().default([]),
|
|
666
722
|
providers: z3.array(providerNameSchema).optional().default([]),
|
|
723
|
+
locations: z3.array(locationContextSchema).optional().default([]),
|
|
724
|
+
defaultLocation: z3.string().optional(),
|
|
667
725
|
schedule: configScheduleSchema,
|
|
668
726
|
notifications: z3.array(configNotificationSchema).optional().default([]),
|
|
669
727
|
google: configGoogleSchema
|
|
@@ -776,6 +834,7 @@ var googleConnectionDtoSchema = z4.object({
|
|
|
776
834
|
domain: z4.string(),
|
|
777
835
|
connectionType: googleConnectionTypeSchema,
|
|
778
836
|
propertyId: z4.string().nullable().optional(),
|
|
837
|
+
sitemapUrl: z4.string().nullable().optional(),
|
|
779
838
|
scopes: z4.array(z4.string()).default([]),
|
|
780
839
|
createdAt: z4.string(),
|
|
781
840
|
updatedAt: z4.string()
|
|
@@ -812,6 +871,11 @@ var gscDeindexedRowSchema = z4.object({
|
|
|
812
871
|
currentState: z4.string().nullable(),
|
|
813
872
|
transitionDate: z4.string()
|
|
814
873
|
});
|
|
874
|
+
var gscReasonGroupSchema = z4.object({
|
|
875
|
+
reason: z4.string(),
|
|
876
|
+
count: z4.number(),
|
|
877
|
+
urls: z4.array(gscUrlInspectionDtoSchema).default([])
|
|
878
|
+
});
|
|
815
879
|
var gscCoverageSummaryDtoSchema = z4.object({
|
|
816
880
|
summary: z4.object({
|
|
817
881
|
total: z4.number(),
|
|
@@ -823,7 +887,14 @@ var gscCoverageSummaryDtoSchema = z4.object({
|
|
|
823
887
|
lastInspectedAt: z4.string().nullable(),
|
|
824
888
|
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
825
889
|
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
826
|
-
deindexed: z4.array(gscDeindexedRowSchema).default([])
|
|
890
|
+
deindexed: z4.array(gscDeindexedRowSchema).default([]),
|
|
891
|
+
reasonGroups: z4.array(gscReasonGroupSchema).default([])
|
|
892
|
+
});
|
|
893
|
+
var gscCoverageSnapshotDtoSchema = z4.object({
|
|
894
|
+
date: z4.string(),
|
|
895
|
+
indexed: z4.number(),
|
|
896
|
+
notIndexed: z4.number(),
|
|
897
|
+
reasonBreakdown: z4.record(z4.string(), z4.number()).default({})
|
|
827
898
|
});
|
|
828
899
|
|
|
829
900
|
// ../contracts/src/project.ts
|
|
@@ -839,6 +910,8 @@ var projectDtoSchema = z5.object({
|
|
|
839
910
|
language: z5.string().min(2),
|
|
840
911
|
tags: z5.array(z5.string()).default([]),
|
|
841
912
|
labels: z5.record(z5.string(), z5.string()).default({}),
|
|
913
|
+
locations: z5.array(locationContextSchema).default([]),
|
|
914
|
+
defaultLocation: z5.string().nullable().optional(),
|
|
842
915
|
configSource: configSourceSchema.default("cli"),
|
|
843
916
|
configRevision: z5.number().int().positive().default(1),
|
|
844
917
|
createdAt: z5.string().optional(),
|
|
@@ -882,6 +955,7 @@ var runDtoSchema = z6.object({
|
|
|
882
955
|
kind: runKindSchema,
|
|
883
956
|
status: runStatusSchema,
|
|
884
957
|
trigger: runTriggerSchema.default("manual"),
|
|
958
|
+
location: z6.string().nullable().optional(),
|
|
885
959
|
startedAt: z6.string().nullable().optional(),
|
|
886
960
|
finishedAt: z6.string().nullable().optional(),
|
|
887
961
|
error: z6.string().nullable().optional(),
|
|
@@ -905,6 +979,7 @@ var querySnapshotDtoSchema = z6.object({
|
|
|
905
979
|
groundingSources: z6.array(groundingSourceSchema).default([]),
|
|
906
980
|
searchQueries: z6.array(z6.string()).default([]),
|
|
907
981
|
model: z6.string().nullable().optional(),
|
|
982
|
+
location: z6.string().nullable().optional(),
|
|
908
983
|
createdAt: z6.string()
|
|
909
984
|
});
|
|
910
985
|
var auditLogEntrySchema = z6.object({
|
|
@@ -1023,6 +1098,8 @@ async function projectRoutes(app, opts) {
|
|
|
1023
1098
|
tags: JSON.stringify(body.tags ?? []),
|
|
1024
1099
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1025
1100
|
providers: JSON.stringify(body.providers ?? []),
|
|
1101
|
+
locations: JSON.stringify(body.locations ?? JSON.parse(existing.locations || "[]")),
|
|
1102
|
+
defaultLocation: body.defaultLocation !== void 0 ? body.defaultLocation ?? null : existing.defaultLocation,
|
|
1026
1103
|
configSource: body.configSource ?? "api",
|
|
1027
1104
|
configRevision: existing.configRevision + 1,
|
|
1028
1105
|
updatedAt: now
|
|
@@ -1049,6 +1126,8 @@ async function projectRoutes(app, opts) {
|
|
|
1049
1126
|
tags: JSON.stringify(body.tags ?? []),
|
|
1050
1127
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1051
1128
|
providers: JSON.stringify(body.providers ?? []),
|
|
1129
|
+
locations: JSON.stringify(body.locations ?? []),
|
|
1130
|
+
defaultLocation: body.defaultLocation ?? null,
|
|
1052
1131
|
configSource: body.configSource ?? "api",
|
|
1053
1132
|
configRevision: 1,
|
|
1054
1133
|
createdAt: now,
|
|
@@ -1102,6 +1181,131 @@ async function projectRoutes(app, opts) {
|
|
|
1102
1181
|
opts.onProjectDeleted?.(project.id);
|
|
1103
1182
|
return reply.status(204).send();
|
|
1104
1183
|
});
|
|
1184
|
+
app.post("/projects/:name/locations", async (request, reply) => {
|
|
1185
|
+
let project;
|
|
1186
|
+
try {
|
|
1187
|
+
project = resolveProject(app.db, request.params.name);
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1190
|
+
const err = e;
|
|
1191
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1192
|
+
}
|
|
1193
|
+
throw e;
|
|
1194
|
+
}
|
|
1195
|
+
const parsed = locationContextSchema.safeParse(request.body);
|
|
1196
|
+
if (!parsed.success) {
|
|
1197
|
+
const err = validationError(parsed.error.issues.map((i) => i.message).join(", "));
|
|
1198
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1199
|
+
}
|
|
1200
|
+
const location = parsed.data;
|
|
1201
|
+
const existing = JSON.parse(project.locations || "[]");
|
|
1202
|
+
if (existing.some((l) => l.label === location.label)) {
|
|
1203
|
+
const err = validationError(`Location "${location.label}" already exists`);
|
|
1204
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1205
|
+
}
|
|
1206
|
+
existing.push(location);
|
|
1207
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1208
|
+
app.db.update(projects).set({
|
|
1209
|
+
locations: JSON.stringify(existing),
|
|
1210
|
+
updatedAt: now
|
|
1211
|
+
}).where(eq3(projects.id, project.id)).run();
|
|
1212
|
+
writeAuditLog(app.db, {
|
|
1213
|
+
projectId: project.id,
|
|
1214
|
+
actor: "api",
|
|
1215
|
+
action: "location.added",
|
|
1216
|
+
entityType: "location",
|
|
1217
|
+
entityId: location.label
|
|
1218
|
+
});
|
|
1219
|
+
return reply.status(201).send(location);
|
|
1220
|
+
});
|
|
1221
|
+
app.get("/projects/:name/locations", async (request, reply) => {
|
|
1222
|
+
let project;
|
|
1223
|
+
try {
|
|
1224
|
+
project = resolveProject(app.db, request.params.name);
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1227
|
+
const err = e;
|
|
1228
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1229
|
+
}
|
|
1230
|
+
throw e;
|
|
1231
|
+
}
|
|
1232
|
+
const locations = JSON.parse(project.locations || "[]");
|
|
1233
|
+
return reply.send({
|
|
1234
|
+
locations,
|
|
1235
|
+
defaultLocation: project.defaultLocation
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
app.delete("/projects/:name/locations/:label", async (request, reply) => {
|
|
1239
|
+
let project;
|
|
1240
|
+
try {
|
|
1241
|
+
project = resolveProject(app.db, request.params.name);
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1244
|
+
const err = e;
|
|
1245
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1246
|
+
}
|
|
1247
|
+
throw e;
|
|
1248
|
+
}
|
|
1249
|
+
const label = decodeURIComponent(request.params.label);
|
|
1250
|
+
const existing = JSON.parse(project.locations || "[]");
|
|
1251
|
+
const filtered = existing.filter((l) => l.label !== label);
|
|
1252
|
+
if (filtered.length === existing.length) {
|
|
1253
|
+
const err = validationError(`Location "${label}" not found`);
|
|
1254
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1255
|
+
}
|
|
1256
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1257
|
+
const updates = {
|
|
1258
|
+
locations: JSON.stringify(filtered),
|
|
1259
|
+
updatedAt: now
|
|
1260
|
+
};
|
|
1261
|
+
if (project.defaultLocation === label) {
|
|
1262
|
+
updates.defaultLocation = null;
|
|
1263
|
+
}
|
|
1264
|
+
app.db.update(projects).set(updates).where(eq3(projects.id, project.id)).run();
|
|
1265
|
+
writeAuditLog(app.db, {
|
|
1266
|
+
projectId: project.id,
|
|
1267
|
+
actor: "api",
|
|
1268
|
+
action: "location.removed",
|
|
1269
|
+
entityType: "location",
|
|
1270
|
+
entityId: label
|
|
1271
|
+
});
|
|
1272
|
+
return reply.status(204).send();
|
|
1273
|
+
});
|
|
1274
|
+
app.put("/projects/:name/locations/default", async (request, reply) => {
|
|
1275
|
+
let project;
|
|
1276
|
+
try {
|
|
1277
|
+
project = resolveProject(app.db, request.params.name);
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
if (e && typeof e === "object" && "statusCode" in e && "toJSON" in e) {
|
|
1280
|
+
const err = e;
|
|
1281
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1282
|
+
}
|
|
1283
|
+
throw e;
|
|
1284
|
+
}
|
|
1285
|
+
const label = request.body?.label;
|
|
1286
|
+
if (!label) {
|
|
1287
|
+
const err = validationError("label is required");
|
|
1288
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1289
|
+
}
|
|
1290
|
+
const existing = JSON.parse(project.locations || "[]");
|
|
1291
|
+
if (!existing.some((l) => l.label === label)) {
|
|
1292
|
+
const err = validationError(`Location "${label}" not found. Add it first.`);
|
|
1293
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1294
|
+
}
|
|
1295
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1296
|
+
app.db.update(projects).set({
|
|
1297
|
+
defaultLocation: label,
|
|
1298
|
+
updatedAt: now
|
|
1299
|
+
}).where(eq3(projects.id, project.id)).run();
|
|
1300
|
+
writeAuditLog(app.db, {
|
|
1301
|
+
projectId: project.id,
|
|
1302
|
+
actor: "api",
|
|
1303
|
+
action: "location.default-set",
|
|
1304
|
+
entityType: "location",
|
|
1305
|
+
entityId: label
|
|
1306
|
+
});
|
|
1307
|
+
return reply.send({ defaultLocation: label });
|
|
1308
|
+
});
|
|
1105
1309
|
app.get("/projects/:name/export", async (request, reply) => {
|
|
1106
1310
|
let project;
|
|
1107
1311
|
try {
|
|
@@ -1133,6 +1337,8 @@ async function projectRoutes(app, opts) {
|
|
|
1133
1337
|
keywords: kws.map((k) => k.keyword),
|
|
1134
1338
|
competitors: comps.map((c) => c.domain),
|
|
1135
1339
|
providers: JSON.parse(project.providers || "[]"),
|
|
1340
|
+
locations: JSON.parse(project.locations || "[]"),
|
|
1341
|
+
...project.defaultLocation ? { defaultLocation: project.defaultLocation } : {},
|
|
1136
1342
|
notifications: notificationRows.map((row) => {
|
|
1137
1343
|
const cfg = JSON.parse(row.config);
|
|
1138
1344
|
return {
|
|
@@ -1165,6 +1371,8 @@ function formatProject(row) {
|
|
|
1165
1371
|
tags: JSON.parse(row.tags),
|
|
1166
1372
|
labels: JSON.parse(row.labels),
|
|
1167
1373
|
providers: JSON.parse(row.providers || "[]"),
|
|
1374
|
+
locations: JSON.parse(row.locations || "[]"),
|
|
1375
|
+
defaultLocation: row.defaultLocation,
|
|
1168
1376
|
configSource: row.configSource,
|
|
1169
1377
|
configRevision: row.configRevision,
|
|
1170
1378
|
createdAt: row.createdAt,
|
|
@@ -1384,6 +1592,7 @@ function resolveProjectSafe2(app, name, reply) {
|
|
|
1384
1592
|
}
|
|
1385
1593
|
|
|
1386
1594
|
// ../api-routes/src/runs.ts
|
|
1595
|
+
import crypto8 from "crypto";
|
|
1387
1596
|
import { eq as eq7, asc } from "drizzle-orm";
|
|
1388
1597
|
|
|
1389
1598
|
// ../api-routes/src/run-queue.ts
|
|
@@ -1410,6 +1619,7 @@ function queueRunIfProjectIdle(db, params) {
|
|
|
1410
1619
|
kind,
|
|
1411
1620
|
status: "queued",
|
|
1412
1621
|
trigger,
|
|
1622
|
+
location: params.location ?? null,
|
|
1413
1623
|
createdAt
|
|
1414
1624
|
}).run();
|
|
1415
1625
|
return { conflict: false, runId };
|
|
@@ -1438,11 +1648,60 @@ async function runRoutes(app, opts) {
|
|
|
1438
1648
|
rawProviders.splice(0, rawProviders.length, ...parsed.filter(Boolean));
|
|
1439
1649
|
}
|
|
1440
1650
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1651
|
+
let resolvedLocation;
|
|
1652
|
+
const projectLocations = JSON.parse(project.locations || "[]");
|
|
1653
|
+
if (request.body?.noLocation) {
|
|
1654
|
+
resolvedLocation = null;
|
|
1655
|
+
} else if (request.body?.allLocations) {
|
|
1656
|
+
} else if (request.body?.location) {
|
|
1657
|
+
const loc = projectLocations.find((l) => l.label === request.body.location);
|
|
1658
|
+
if (!loc) {
|
|
1659
|
+
return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: `Location "${request.body.location}" not found. Configure it first.` } });
|
|
1660
|
+
}
|
|
1661
|
+
resolvedLocation = loc;
|
|
1662
|
+
}
|
|
1663
|
+
if (request.body?.allLocations) {
|
|
1664
|
+
if (projectLocations.length === 0) {
|
|
1665
|
+
return reply.status(400).send({ error: { code: "VALIDATION_ERROR", message: "No locations configured for this project" } });
|
|
1666
|
+
}
|
|
1667
|
+
const newRuns = [];
|
|
1668
|
+
for (const loc of projectLocations) {
|
|
1669
|
+
const runId2 = crypto8.randomUUID();
|
|
1670
|
+
app.db.insert(runs).values({
|
|
1671
|
+
id: runId2,
|
|
1672
|
+
projectId: project.id,
|
|
1673
|
+
kind,
|
|
1674
|
+
status: "queued",
|
|
1675
|
+
trigger,
|
|
1676
|
+
location: loc.label,
|
|
1677
|
+
createdAt: now
|
|
1678
|
+
}).run();
|
|
1679
|
+
newRuns.push({ runId: runId2, loc });
|
|
1680
|
+
}
|
|
1681
|
+
const results = [];
|
|
1682
|
+
for (const { runId: runId2, loc } of newRuns) {
|
|
1683
|
+
writeAuditLog(app.db, {
|
|
1684
|
+
projectId: project.id,
|
|
1685
|
+
actor: "api",
|
|
1686
|
+
action: "run.created",
|
|
1687
|
+
entityType: "run",
|
|
1688
|
+
entityId: runId2
|
|
1689
|
+
});
|
|
1690
|
+
const r = app.db.select().from(runs).where(eq7(runs.id, runId2)).get();
|
|
1691
|
+
if (opts.onRunCreated) {
|
|
1692
|
+
opts.onRunCreated(runId2, project.id, providers, loc);
|
|
1693
|
+
}
|
|
1694
|
+
results.push({ ...formatRun(r), location: loc.label });
|
|
1695
|
+
}
|
|
1696
|
+
return reply.status(207).send(results);
|
|
1697
|
+
}
|
|
1698
|
+
const locationLabel = resolvedLocation?.label ?? null;
|
|
1441
1699
|
const queueResult = queueRunIfProjectIdle(app.db, {
|
|
1442
1700
|
createdAt: now,
|
|
1443
1701
|
kind,
|
|
1444
1702
|
projectId: project.id,
|
|
1445
|
-
trigger
|
|
1703
|
+
trigger,
|
|
1704
|
+
location: locationLabel
|
|
1446
1705
|
});
|
|
1447
1706
|
if (queueResult.conflict) {
|
|
1448
1707
|
const err = runInProgress(project.name);
|
|
@@ -1458,7 +1717,7 @@ async function runRoutes(app, opts) {
|
|
|
1458
1717
|
});
|
|
1459
1718
|
const run = app.db.select().from(runs).where(eq7(runs.id, runId)).get();
|
|
1460
1719
|
if (opts.onRunCreated) {
|
|
1461
|
-
opts.onRunCreated(runId, project.id, providers);
|
|
1720
|
+
opts.onRunCreated(runId, project.id, providers, resolvedLocation);
|
|
1462
1721
|
}
|
|
1463
1722
|
return reply.status(201).send(formatRun(run));
|
|
1464
1723
|
});
|
|
@@ -1537,6 +1796,7 @@ async function runRoutes(app, opts) {
|
|
|
1537
1796
|
answerText: querySnapshots.answerText,
|
|
1538
1797
|
citedDomains: querySnapshots.citedDomains,
|
|
1539
1798
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
1799
|
+
location: querySnapshots.location,
|
|
1540
1800
|
rawResponse: querySnapshots.rawResponse,
|
|
1541
1801
|
createdAt: querySnapshots.createdAt
|
|
1542
1802
|
}).from(querySnapshots).leftJoin(keywords, eq7(querySnapshots.keywordId, keywords.id)).where(eq7(querySnapshots.runId, run.id)).all();
|
|
@@ -1555,6 +1815,7 @@ async function runRoutes(app, opts) {
|
|
|
1555
1815
|
citedDomains: tryParseJson(s.citedDomains, []),
|
|
1556
1816
|
competitorOverlap: tryParseJson(s.competitorOverlap, []),
|
|
1557
1817
|
model: s.model ?? rawParsed.model,
|
|
1818
|
+
location: s.location,
|
|
1558
1819
|
groundingSources: rawParsed.groundingSources,
|
|
1559
1820
|
searchQueries: rawParsed.searchQueries,
|
|
1560
1821
|
createdAt: s.createdAt
|
|
@@ -1570,6 +1831,7 @@ function formatRun(row) {
|
|
|
1570
1831
|
kind: row.kind,
|
|
1571
1832
|
status: row.status,
|
|
1572
1833
|
trigger: row.trigger,
|
|
1834
|
+
location: row.location,
|
|
1573
1835
|
startedAt: row.startedAt,
|
|
1574
1836
|
finishedAt: row.finishedAt,
|
|
1575
1837
|
error: row.error,
|
|
@@ -1605,7 +1867,7 @@ function resolveProjectSafe3(app, name, reply) {
|
|
|
1605
1867
|
}
|
|
1606
1868
|
|
|
1607
1869
|
// ../api-routes/src/apply.ts
|
|
1608
|
-
import
|
|
1870
|
+
import crypto10 from "crypto";
|
|
1609
1871
|
import { eq as eq8 } from "drizzle-orm";
|
|
1610
1872
|
|
|
1611
1873
|
// ../api-routes/src/schedule-utils.ts
|
|
@@ -1697,7 +1959,7 @@ function isValidTimezone(tz) {
|
|
|
1697
1959
|
}
|
|
1698
1960
|
|
|
1699
1961
|
// ../api-routes/src/webhooks.ts
|
|
1700
|
-
import
|
|
1962
|
+
import crypto9 from "crypto";
|
|
1701
1963
|
import dns from "dns/promises";
|
|
1702
1964
|
import http from "http";
|
|
1703
1965
|
import https from "https";
|
|
@@ -1749,7 +2011,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
1749
2011
|
"User-Agent": "Canonry/0.1.0"
|
|
1750
2012
|
};
|
|
1751
2013
|
if (webhookSecret) {
|
|
1752
|
-
headers["X-Canonry-Signature"] = "sha256=" +
|
|
2014
|
+
headers["X-Canonry-Signature"] = "sha256=" + crypto9.createHmac("sha256", webhookSecret).update(body).digest("hex");
|
|
1753
2015
|
}
|
|
1754
2016
|
return await new Promise((resolve) => {
|
|
1755
2017
|
const requestOptions = {
|
|
@@ -1880,6 +2142,8 @@ async function applyRoutes(app, opts) {
|
|
|
1880
2142
|
language: config.spec.language,
|
|
1881
2143
|
labels: JSON.stringify(config.metadata.labels),
|
|
1882
2144
|
providers: JSON.stringify(config.spec.providers ?? []),
|
|
2145
|
+
locations: JSON.stringify(config.spec.locations ?? []),
|
|
2146
|
+
defaultLocation: config.spec.defaultLocation ?? null,
|
|
1883
2147
|
configSource: "config-file",
|
|
1884
2148
|
configRevision: existing.configRevision + 1,
|
|
1885
2149
|
updatedAt: now
|
|
@@ -1892,7 +2156,7 @@ async function applyRoutes(app, opts) {
|
|
|
1892
2156
|
entityId: projectId
|
|
1893
2157
|
});
|
|
1894
2158
|
} else {
|
|
1895
|
-
projectId =
|
|
2159
|
+
projectId = crypto10.randomUUID();
|
|
1896
2160
|
app.db.insert(projects).values({
|
|
1897
2161
|
id: projectId,
|
|
1898
2162
|
name,
|
|
@@ -1904,6 +2168,8 @@ async function applyRoutes(app, opts) {
|
|
|
1904
2168
|
tags: "[]",
|
|
1905
2169
|
labels: JSON.stringify(config.metadata.labels),
|
|
1906
2170
|
providers: JSON.stringify(config.spec.providers ?? []),
|
|
2171
|
+
locations: JSON.stringify(config.spec.locations ?? []),
|
|
2172
|
+
defaultLocation: config.spec.defaultLocation ?? null,
|
|
1907
2173
|
configSource: "config-file",
|
|
1908
2174
|
configRevision: 1,
|
|
1909
2175
|
createdAt: now,
|
|
@@ -1921,7 +2187,7 @@ async function applyRoutes(app, opts) {
|
|
|
1921
2187
|
tx.delete(keywords).where(eq8(keywords.projectId, projectId)).run();
|
|
1922
2188
|
for (const kw of config.spec.keywords) {
|
|
1923
2189
|
tx.insert(keywords).values({
|
|
1924
|
-
id:
|
|
2190
|
+
id: crypto10.randomUUID(),
|
|
1925
2191
|
projectId,
|
|
1926
2192
|
keyword: kw,
|
|
1927
2193
|
createdAt: now
|
|
@@ -1937,7 +2203,7 @@ async function applyRoutes(app, opts) {
|
|
|
1937
2203
|
tx.delete(competitors).where(eq8(competitors.projectId, projectId)).run();
|
|
1938
2204
|
for (const domain of config.spec.competitors) {
|
|
1939
2205
|
tx.insert(competitors).values({
|
|
1940
|
-
id:
|
|
2206
|
+
id: crypto10.randomUUID(),
|
|
1941
2207
|
projectId,
|
|
1942
2208
|
domain,
|
|
1943
2209
|
createdAt: now
|
|
@@ -1993,7 +2259,7 @@ async function applyRoutes(app, opts) {
|
|
|
1993
2259
|
}).where(eq8(schedules.id, existingSched.id)).run();
|
|
1994
2260
|
} else {
|
|
1995
2261
|
app.db.insert(schedules).values({
|
|
1996
|
-
id:
|
|
2262
|
+
id: crypto10.randomUUID(),
|
|
1997
2263
|
projectId,
|
|
1998
2264
|
cronExpr,
|
|
1999
2265
|
preset,
|
|
@@ -2025,11 +2291,11 @@ async function applyRoutes(app, opts) {
|
|
|
2025
2291
|
app.db.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
|
|
2026
2292
|
for (const notif of config.spec.notifications) {
|
|
2027
2293
|
app.db.insert(notifications).values({
|
|
2028
|
-
id:
|
|
2294
|
+
id: crypto10.randomUUID(),
|
|
2029
2295
|
projectId,
|
|
2030
2296
|
channel: notif.channel,
|
|
2031
2297
|
config: JSON.stringify({ url: notif.url, events: notif.events }),
|
|
2032
|
-
webhookSecret:
|
|
2298
|
+
webhookSecret: crypto10.randomBytes(32).toString("hex"),
|
|
2033
2299
|
enabled: 1,
|
|
2034
2300
|
createdAt: now,
|
|
2035
2301
|
updatedAt: now
|
|
@@ -2058,6 +2324,8 @@ async function applyRoutes(app, opts) {
|
|
|
2058
2324
|
tags: JSON.parse(project.tags),
|
|
2059
2325
|
labels: JSON.parse(project.labels),
|
|
2060
2326
|
providers: JSON.parse(project.providers || "[]"),
|
|
2327
|
+
locations: JSON.parse(project.locations || "[]"),
|
|
2328
|
+
defaultLocation: project.defaultLocation,
|
|
2061
2329
|
configSource: project.configSource,
|
|
2062
2330
|
configRevision: project.configRevision,
|
|
2063
2331
|
createdAt: project.createdAt,
|
|
@@ -2099,10 +2367,13 @@ async function historyRoutes(app) {
|
|
|
2099
2367
|
answerText: querySnapshots.answerText,
|
|
2100
2368
|
citedDomains: querySnapshots.citedDomains,
|
|
2101
2369
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
2370
|
+
location: querySnapshots.location,
|
|
2102
2371
|
createdAt: querySnapshots.createdAt
|
|
2103
2372
|
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc(querySnapshots.createdAt)).all();
|
|
2104
|
-
const
|
|
2105
|
-
const
|
|
2373
|
+
const locationFilter = request.query.location;
|
|
2374
|
+
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
2375
|
+
const total = filtered.length;
|
|
2376
|
+
const paged = filtered.slice(offset, offset + limit);
|
|
2106
2377
|
return reply.send({
|
|
2107
2378
|
snapshots: paged.map((s) => ({
|
|
2108
2379
|
id: s.id,
|
|
@@ -2115,6 +2386,7 @@ async function historyRoutes(app) {
|
|
|
2115
2386
|
answerText: s.answerText,
|
|
2116
2387
|
citedDomains: tryParseJson2(s.citedDomains, []),
|
|
2117
2388
|
competitorOverlap: tryParseJson2(s.competitorOverlap, []),
|
|
2389
|
+
location: s.location,
|
|
2118
2390
|
createdAt: s.createdAt
|
|
2119
2391
|
})),
|
|
2120
2392
|
total
|
|
@@ -2129,7 +2401,9 @@ async function historyRoutes(app) {
|
|
|
2129
2401
|
return reply.send([]);
|
|
2130
2402
|
}
|
|
2131
2403
|
const runIds = new Set(projectRuns.map((r) => r.id));
|
|
2132
|
-
const
|
|
2404
|
+
const rawSnapshots = app.db.select().from(querySnapshots).where(inArray(querySnapshots.runId, [...runIds])).all();
|
|
2405
|
+
const timelineLocationFilter = request.query.location;
|
|
2406
|
+
const allSnapshots = timelineLocationFilter !== void 0 ? rawSnapshots.filter((s) => s.location === (timelineLocationFilter || null)) : rawSnapshots;
|
|
2133
2407
|
const deduped = /* @__PURE__ */ new Map();
|
|
2134
2408
|
for (const snap of allSnapshots) {
|
|
2135
2409
|
const key = `${snap.runId}:${snap.keywordId}`;
|
|
@@ -3038,7 +3312,7 @@ async function telemetryRoutes(app, opts) {
|
|
|
3038
3312
|
}
|
|
3039
3313
|
|
|
3040
3314
|
// ../api-routes/src/schedules.ts
|
|
3041
|
-
import
|
|
3315
|
+
import crypto11 from "crypto";
|
|
3042
3316
|
import { eq as eq10 } from "drizzle-orm";
|
|
3043
3317
|
async function scheduleRoutes(app, opts) {
|
|
3044
3318
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
@@ -3085,7 +3359,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
3085
3359
|
}).where(eq10(schedules.id, existing.id)).run();
|
|
3086
3360
|
} else {
|
|
3087
3361
|
app.db.insert(schedules).values({
|
|
3088
|
-
id:
|
|
3362
|
+
id: crypto11.randomUUID(),
|
|
3089
3363
|
projectId: project.id,
|
|
3090
3364
|
cronExpr,
|
|
3091
3365
|
preset: preset ?? null,
|
|
@@ -3164,7 +3438,7 @@ function resolveProjectSafe5(app, name, reply) {
|
|
|
3164
3438
|
}
|
|
3165
3439
|
|
|
3166
3440
|
// ../api-routes/src/notifications.ts
|
|
3167
|
-
import
|
|
3441
|
+
import crypto12 from "crypto";
|
|
3168
3442
|
import { eq as eq11 } from "drizzle-orm";
|
|
3169
3443
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
|
|
3170
3444
|
async function notificationRoutes(app) {
|
|
@@ -3198,8 +3472,8 @@ async function notificationRoutes(app) {
|
|
|
3198
3472
|
});
|
|
3199
3473
|
}
|
|
3200
3474
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3201
|
-
const id =
|
|
3202
|
-
const webhookSecret =
|
|
3475
|
+
const id = crypto12.randomUUID();
|
|
3476
|
+
const webhookSecret = crypto12.randomBytes(32).toString("hex");
|
|
3203
3477
|
app.db.insert(notifications).values({
|
|
3204
3478
|
id,
|
|
3205
3479
|
projectId: project.id,
|
|
@@ -3318,7 +3592,7 @@ function resolveProjectSafe6(app, name, reply) {
|
|
|
3318
3592
|
}
|
|
3319
3593
|
|
|
3320
3594
|
// ../api-routes/src/google.ts
|
|
3321
|
-
import
|
|
3595
|
+
import crypto13 from "crypto";
|
|
3322
3596
|
import { eq as eq12, and as and3, desc as desc2, sql as sql2 } from "drizzle-orm";
|
|
3323
3597
|
|
|
3324
3598
|
// ../integration-google/src/constants.ts
|
|
@@ -3426,6 +3700,14 @@ async function listSites(accessToken) {
|
|
|
3426
3700
|
);
|
|
3427
3701
|
return data.siteEntry ?? [];
|
|
3428
3702
|
}
|
|
3703
|
+
async function listSitemaps(accessToken, siteUrl) {
|
|
3704
|
+
const encodedSiteUrl = encodeURIComponent(siteUrl);
|
|
3705
|
+
const data = await gscFetch(
|
|
3706
|
+
accessToken,
|
|
3707
|
+
`${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps`
|
|
3708
|
+
);
|
|
3709
|
+
return data.sitemap ?? [];
|
|
3710
|
+
}
|
|
3429
3711
|
async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
|
|
3430
3712
|
const allRows = [];
|
|
3431
3713
|
let startRow = 0;
|
|
@@ -3477,7 +3759,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
|
|
|
3477
3759
|
|
|
3478
3760
|
// ../api-routes/src/google.ts
|
|
3479
3761
|
function signState(payload, secret) {
|
|
3480
|
-
return
|
|
3762
|
+
return crypto13.createHmac("sha256", secret).update(payload).digest("hex");
|
|
3481
3763
|
}
|
|
3482
3764
|
function buildSignedState(data, secret) {
|
|
3483
3765
|
const payload = JSON.stringify(data);
|
|
@@ -3488,7 +3770,7 @@ function verifySignedState(encoded, secret) {
|
|
|
3488
3770
|
try {
|
|
3489
3771
|
const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
|
|
3490
3772
|
const expected = signState(payload, secret);
|
|
3491
|
-
if (!
|
|
3773
|
+
if (!crypto13.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
|
|
3492
3774
|
return JSON.parse(payload);
|
|
3493
3775
|
} catch {
|
|
3494
3776
|
return null;
|
|
@@ -3543,6 +3825,7 @@ async function googleRoutes(app, opts) {
|
|
|
3543
3825
|
domain: connection.domain,
|
|
3544
3826
|
connectionType: connection.connectionType,
|
|
3545
3827
|
propertyId: connection.propertyId ?? null,
|
|
3828
|
+
sitemapUrl: connection.sitemapUrl ?? null,
|
|
3546
3829
|
scopes: connection.scopes ?? [],
|
|
3547
3830
|
createdAt: connection.createdAt,
|
|
3548
3831
|
updatedAt: connection.updatedAt
|
|
@@ -3714,7 +3997,7 @@ async function googleRoutes(app, opts) {
|
|
|
3714
3997
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3715
3998
|
}
|
|
3716
3999
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3717
|
-
const runId =
|
|
4000
|
+
const runId = crypto13.randomUUID();
|
|
3718
4001
|
app.db.insert(runs).values({
|
|
3719
4002
|
id: runId,
|
|
3720
4003
|
projectId: project.id,
|
|
@@ -3776,7 +4059,7 @@ async function googleRoutes(app, opts) {
|
|
|
3776
4059
|
const mob = ir.mobileUsabilityResult;
|
|
3777
4060
|
const rich = ir.richResultsResult;
|
|
3778
4061
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3779
|
-
const id =
|
|
4062
|
+
const id = crypto13.randomUUID();
|
|
3780
4063
|
app.db.insert(gscUrlInspections).values({
|
|
3781
4064
|
id,
|
|
3782
4065
|
projectId: project.id,
|
|
@@ -3921,6 +4204,21 @@ async function googleRoutes(app, opts) {
|
|
|
3921
4204
|
richResults: JSON.parse(r.richResults),
|
|
3922
4205
|
inspectedAt: r.inspectedAt
|
|
3923
4206
|
});
|
|
4207
|
+
const reasonMap = /* @__PURE__ */ new Map();
|
|
4208
|
+
for (const row of notIndexedUrls) {
|
|
4209
|
+
const reason = row.coverageState ?? "Unknown";
|
|
4210
|
+
const existing = reasonMap.get(reason);
|
|
4211
|
+
if (existing) {
|
|
4212
|
+
existing.push(row);
|
|
4213
|
+
} else {
|
|
4214
|
+
reasonMap.set(reason, [row]);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
const reasonGroups = Array.from(reasonMap.entries()).map(([reason, urls]) => ({
|
|
4218
|
+
reason,
|
|
4219
|
+
count: urls.length,
|
|
4220
|
+
urls: urls.map(formatRow)
|
|
4221
|
+
})).sort((a, b) => b.count - a.count);
|
|
3924
4222
|
return {
|
|
3925
4223
|
summary: {
|
|
3926
4224
|
total,
|
|
@@ -3932,9 +4230,85 @@ async function googleRoutes(app, opts) {
|
|
|
3932
4230
|
lastInspectedAt,
|
|
3933
4231
|
indexed: indexedUrls.map(formatRow),
|
|
3934
4232
|
notIndexed: notIndexedUrls.map(formatRow),
|
|
3935
|
-
deindexed: deindexedUrls
|
|
4233
|
+
deindexed: deindexedUrls,
|
|
4234
|
+
reasonGroups
|
|
3936
4235
|
};
|
|
3937
4236
|
});
|
|
4237
|
+
app.get("/projects/:name/google/gsc/coverage/history", async (request) => {
|
|
4238
|
+
const project = resolveProject(app.db, request.params.name);
|
|
4239
|
+
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
4240
|
+
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
4241
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq12(gscCoverageSnapshots.projectId, project.id)).orderBy(desc2(gscCoverageSnapshots.date)).limit(limit).all();
|
|
4242
|
+
return rows.map((r) => ({
|
|
4243
|
+
date: r.date,
|
|
4244
|
+
indexed: r.indexed,
|
|
4245
|
+
notIndexed: r.notIndexed,
|
|
4246
|
+
reasonBreakdown: JSON.parse(r.reasonBreakdown)
|
|
4247
|
+
})).reverse();
|
|
4248
|
+
});
|
|
4249
|
+
app.get("/projects/:name/google/gsc/sitemaps", async (request, reply) => {
|
|
4250
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
4251
|
+
if (!googleClientId || !googleClientSecret) {
|
|
4252
|
+
const err = validationError("Google OAuth is not configured");
|
|
4253
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4254
|
+
}
|
|
4255
|
+
const store = requireConnectionStore(reply);
|
|
4256
|
+
if (!store) return;
|
|
4257
|
+
const project = resolveProject(app.db, request.params.name);
|
|
4258
|
+
const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
4259
|
+
if (!propertyId) {
|
|
4260
|
+
const err = validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
|
|
4261
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4262
|
+
}
|
|
4263
|
+
const sitemaps = await listSitemaps(accessToken, propertyId);
|
|
4264
|
+
return { sitemaps };
|
|
4265
|
+
});
|
|
4266
|
+
app.post("/projects/:name/google/gsc/discover-sitemaps", async (request, reply) => {
|
|
4267
|
+
const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
|
|
4268
|
+
if (!googleClientId || !googleClientSecret) {
|
|
4269
|
+
const err = validationError("Google OAuth is not configured");
|
|
4270
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4271
|
+
}
|
|
4272
|
+
const store = requireConnectionStore(reply);
|
|
4273
|
+
if (!store) return;
|
|
4274
|
+
const project = resolveProject(app.db, request.params.name);
|
|
4275
|
+
const conn = store.getConnection(project.canonicalDomain, "gsc");
|
|
4276
|
+
if (!conn) {
|
|
4277
|
+
const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
|
|
4278
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4279
|
+
}
|
|
4280
|
+
if (!conn.propertyId) {
|
|
4281
|
+
const err = validationError("No GSC property configured for this connection");
|
|
4282
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4283
|
+
}
|
|
4284
|
+
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
4285
|
+
const sitemaps = await listSitemaps(accessToken, conn.propertyId);
|
|
4286
|
+
if (sitemaps.length === 0) {
|
|
4287
|
+
const err = validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
|
|
4288
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4289
|
+
}
|
|
4290
|
+
const primary = sitemaps.find((s) => !s.isSitemapsIndex) ?? sitemaps[0];
|
|
4291
|
+
const sitemapUrl = primary.path;
|
|
4292
|
+
store.updateConnection(project.canonicalDomain, "gsc", {
|
|
4293
|
+
sitemapUrl,
|
|
4294
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4295
|
+
});
|
|
4296
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4297
|
+
const runId = crypto13.randomUUID();
|
|
4298
|
+
app.db.insert(runs).values({
|
|
4299
|
+
id: runId,
|
|
4300
|
+
projectId: project.id,
|
|
4301
|
+
kind: "inspect-sitemap",
|
|
4302
|
+
status: "queued",
|
|
4303
|
+
trigger: "manual",
|
|
4304
|
+
createdAt: now
|
|
4305
|
+
}).run();
|
|
4306
|
+
if (opts.onInspectSitemapRequested) {
|
|
4307
|
+
opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
|
|
4308
|
+
}
|
|
4309
|
+
const run = app.db.select().from(runs).where(eq12(runs.id, runId)).get();
|
|
4310
|
+
return { sitemaps, primarySitemapUrl: sitemapUrl, run };
|
|
4311
|
+
});
|
|
3938
4312
|
app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
|
|
3939
4313
|
const store = requireConnectionStore(reply);
|
|
3940
4314
|
if (!store) return;
|
|
@@ -3949,7 +4323,7 @@ async function googleRoutes(app, opts) {
|
|
|
3949
4323
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
3950
4324
|
}
|
|
3951
4325
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3952
|
-
const runId =
|
|
4326
|
+
const runId = crypto13.randomUUID();
|
|
3953
4327
|
app.db.insert(runs).values({
|
|
3954
4328
|
id: runId,
|
|
3955
4329
|
projectId: project.id,
|
|
@@ -3965,6 +4339,26 @@ async function googleRoutes(app, opts) {
|
|
|
3965
4339
|
const run = app.db.select().from(runs).where(eq12(runs.id, runId)).get();
|
|
3966
4340
|
return run;
|
|
3967
4341
|
});
|
|
4342
|
+
app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
|
|
4343
|
+
const store = requireConnectionStore(reply);
|
|
4344
|
+
if (!store) return;
|
|
4345
|
+
const project = resolveProject(app.db, request.params.name);
|
|
4346
|
+
const { sitemapUrl } = request.body ?? {};
|
|
4347
|
+
if (!sitemapUrl || !sitemapUrl.trim()) {
|
|
4348
|
+
const err = validationError("sitemapUrl is required");
|
|
4349
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4350
|
+
}
|
|
4351
|
+
const conn = store.updateConnection(
|
|
4352
|
+
project.canonicalDomain,
|
|
4353
|
+
request.params.type,
|
|
4354
|
+
{ sitemapUrl: sitemapUrl.trim(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4355
|
+
);
|
|
4356
|
+
if (!conn) {
|
|
4357
|
+
const err = notFound("Google connection", request.params.type);
|
|
4358
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4359
|
+
}
|
|
4360
|
+
return { sitemapUrl: sitemapUrl.trim() };
|
|
4361
|
+
});
|
|
3968
4362
|
app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
|
|
3969
4363
|
const store = requireConnectionStore(reply);
|
|
3970
4364
|
if (!store) return;
|
|
@@ -4093,7 +4487,7 @@ async function executeTrackedQuery(input) {
|
|
|
4093
4487
|
model,
|
|
4094
4488
|
tools: [{ googleSearch: {} }]
|
|
4095
4489
|
});
|
|
4096
|
-
const prompt = buildPrompt(input.keyword);
|
|
4490
|
+
const prompt = buildPrompt(input.keyword, input.location);
|
|
4097
4491
|
const result = await generativeModel.generateContent(prompt);
|
|
4098
4492
|
const response = result.response;
|
|
4099
4493
|
const groundingMetadata = extractGroundingMetadata(response);
|
|
@@ -4117,7 +4511,10 @@ function normalizeResult(raw) {
|
|
|
4117
4511
|
searchQueries: raw.searchQueries
|
|
4118
4512
|
};
|
|
4119
4513
|
}
|
|
4120
|
-
function buildPrompt(keyword) {
|
|
4514
|
+
function buildPrompt(keyword, location) {
|
|
4515
|
+
if (location) {
|
|
4516
|
+
return `${keyword} (searching from ${location.city}, ${location.region}, ${location.country})`;
|
|
4517
|
+
}
|
|
4121
4518
|
return keyword;
|
|
4122
4519
|
}
|
|
4123
4520
|
function extractAnswerText(rawResponse) {
|
|
@@ -4259,7 +4656,8 @@ var geminiAdapter = {
|
|
|
4259
4656
|
keyword: input.keyword,
|
|
4260
4657
|
canonicalDomains: input.canonicalDomains,
|
|
4261
4658
|
competitorDomains: input.competitorDomains,
|
|
4262
|
-
config: toGeminiConfig(config)
|
|
4659
|
+
config: toGeminiConfig(config),
|
|
4660
|
+
location: input.location
|
|
4263
4661
|
});
|
|
4264
4662
|
return {
|
|
4265
4663
|
provider: "gemini",
|
|
@@ -4333,9 +4731,19 @@ async function healthcheck2(config) {
|
|
|
4333
4731
|
async function executeTrackedQuery2(input) {
|
|
4334
4732
|
const model = input.config.model ?? DEFAULT_MODEL2;
|
|
4335
4733
|
const client = new OpenAI({ apiKey: input.config.apiKey });
|
|
4734
|
+
const webSearchTool = { type: "web_search_preview" };
|
|
4735
|
+
if (input.location) {
|
|
4736
|
+
webSearchTool.user_location = {
|
|
4737
|
+
type: "approximate",
|
|
4738
|
+
city: input.location.city,
|
|
4739
|
+
region: input.location.region,
|
|
4740
|
+
country: input.location.country,
|
|
4741
|
+
...input.location.timezone ? { timezone: input.location.timezone } : {}
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4336
4744
|
const response = await client.responses.create({
|
|
4337
4745
|
model,
|
|
4338
|
-
tools: [
|
|
4746
|
+
tools: [webSearchTool],
|
|
4339
4747
|
tool_choice: "required",
|
|
4340
4748
|
input: buildPrompt2(input.keyword)
|
|
4341
4749
|
});
|
|
@@ -4500,7 +4908,8 @@ var openaiAdapter = {
|
|
|
4500
4908
|
keyword: input.keyword,
|
|
4501
4909
|
canonicalDomains: input.canonicalDomains,
|
|
4502
4910
|
competitorDomains: input.competitorDomains,
|
|
4503
|
-
config: toOpenAIConfig(config)
|
|
4911
|
+
config: toOpenAIConfig(config),
|
|
4912
|
+
location: input.location
|
|
4504
4913
|
});
|
|
4505
4914
|
return {
|
|
4506
4915
|
provider: "openai",
|
|
@@ -4575,16 +4984,24 @@ async function healthcheck3(config) {
|
|
|
4575
4984
|
async function executeTrackedQuery3(input) {
|
|
4576
4985
|
const model = input.config.model ?? DEFAULT_MODEL3;
|
|
4577
4986
|
const client = new Anthropic({ apiKey: input.config.apiKey });
|
|
4987
|
+
const webSearchTool = {
|
|
4988
|
+
type: "web_search_20250305",
|
|
4989
|
+
name: "web_search",
|
|
4990
|
+
max_uses: 5
|
|
4991
|
+
};
|
|
4992
|
+
if (input.location) {
|
|
4993
|
+
webSearchTool.user_location = {
|
|
4994
|
+
type: "approximate",
|
|
4995
|
+
city: input.location.city,
|
|
4996
|
+
region: input.location.region,
|
|
4997
|
+
country: input.location.country,
|
|
4998
|
+
...input.location.timezone ? { timezone: input.location.timezone } : {}
|
|
4999
|
+
};
|
|
5000
|
+
}
|
|
4578
5001
|
const response = await client.messages.create({
|
|
4579
5002
|
model,
|
|
4580
5003
|
max_tokens: 4096,
|
|
4581
|
-
tools: [
|
|
4582
|
-
{
|
|
4583
|
-
type: "web_search_20250305",
|
|
4584
|
-
name: "web_search",
|
|
4585
|
-
max_uses: 5
|
|
4586
|
-
}
|
|
4587
|
-
],
|
|
5004
|
+
tools: [webSearchTool],
|
|
4588
5005
|
messages: [{ role: "user", content: input.keyword }]
|
|
4589
5006
|
});
|
|
4590
5007
|
const groundingSources = extractGroundingSources2(response);
|
|
@@ -4737,7 +5154,8 @@ var claudeAdapter = {
|
|
|
4737
5154
|
keyword: input.keyword,
|
|
4738
5155
|
canonicalDomains: input.canonicalDomains,
|
|
4739
5156
|
competitorDomains: input.competitorDomains,
|
|
4740
|
-
config: toClaudeConfig(config)
|
|
5157
|
+
config: toClaudeConfig(config),
|
|
5158
|
+
location: input.location
|
|
4741
5159
|
});
|
|
4742
5160
|
return {
|
|
4743
5161
|
provider: "claude",
|
|
@@ -4827,7 +5245,7 @@ async function executeTrackedQuery4(input) {
|
|
|
4827
5245
|
},
|
|
4828
5246
|
{
|
|
4829
5247
|
role: "user",
|
|
4830
|
-
content: buildPrompt3(input.keyword)
|
|
5248
|
+
content: buildPrompt3(input.keyword, input.location)
|
|
4831
5249
|
}
|
|
4832
5250
|
]
|
|
4833
5251
|
});
|
|
@@ -4850,8 +5268,9 @@ function normalizeResult4(raw) {
|
|
|
4850
5268
|
searchQueries: raw.searchQueries
|
|
4851
5269
|
};
|
|
4852
5270
|
}
|
|
4853
|
-
function buildPrompt3(keyword) {
|
|
4854
|
-
|
|
5271
|
+
function buildPrompt3(keyword, location) {
|
|
5272
|
+
const locationContext = location ? ` The user is searching from ${location.city}, ${location.region}, ${location.country}.` : "";
|
|
5273
|
+
return `Based on your training knowledge, what websites, services, or organizations are commonly associated with "${keyword}"?${locationContext} List the most relevant ones and include their domain names (e.g. example.com) where you know them.`;
|
|
4855
5274
|
}
|
|
4856
5275
|
function extractAnswerText2(rawResponse) {
|
|
4857
5276
|
try {
|
|
@@ -4922,7 +5341,8 @@ var localAdapter = {
|
|
|
4922
5341
|
keyword: input.keyword,
|
|
4923
5342
|
canonicalDomains: input.canonicalDomains,
|
|
4924
5343
|
competitorDomains: input.competitorDomains,
|
|
4925
|
-
config: toLocalConfig(config)
|
|
5344
|
+
config: toLocalConfig(config),
|
|
5345
|
+
location: input.location
|
|
4926
5346
|
});
|
|
4927
5347
|
return {
|
|
4928
5348
|
provider: "local",
|
|
@@ -4989,6 +5409,7 @@ function upsertGoogleConnection(config, connection) {
|
|
|
4989
5409
|
const normalized = {
|
|
4990
5410
|
...connection,
|
|
4991
5411
|
propertyId: connection.propertyId ?? null,
|
|
5412
|
+
sitemapUrl: connection.sitemapUrl ?? null,
|
|
4992
5413
|
refreshToken: connection.refreshToken ?? null,
|
|
4993
5414
|
tokenExpiresAt: connection.tokenExpiresAt ?? null,
|
|
4994
5415
|
scopes: connection.scopes ?? []
|
|
@@ -5007,6 +5428,7 @@ function patchGoogleConnection(config, domain, connectionType, patch) {
|
|
|
5007
5428
|
...existing,
|
|
5008
5429
|
...patch,
|
|
5009
5430
|
propertyId: Object.prototype.hasOwnProperty.call(patch, "propertyId") ? patch.propertyId ?? null : existing.propertyId ?? null,
|
|
5431
|
+
sitemapUrl: Object.prototype.hasOwnProperty.call(patch, "sitemapUrl") ? patch.sitemapUrl ?? null : existing.sitemapUrl ?? null,
|
|
5010
5432
|
refreshToken: Object.prototype.hasOwnProperty.call(patch, "refreshToken") ? patch.refreshToken ?? null : existing.refreshToken ?? null,
|
|
5011
5433
|
tokenExpiresAt: Object.prototype.hasOwnProperty.call(patch, "tokenExpiresAt") ? patch.tokenExpiresAt ?? null : existing.tokenExpiresAt ?? null,
|
|
5012
5434
|
scopes: patch.scopes ?? existing.scopes ?? []
|
|
@@ -5026,7 +5448,7 @@ function removeGoogleConnection(config, domain, connectionType) {
|
|
|
5026
5448
|
}
|
|
5027
5449
|
|
|
5028
5450
|
// src/job-runner.ts
|
|
5029
|
-
import
|
|
5451
|
+
import crypto14 from "crypto";
|
|
5030
5452
|
import { eq as eq13, inArray as inArray2 } from "drizzle-orm";
|
|
5031
5453
|
var JobRunner = class {
|
|
5032
5454
|
db;
|
|
@@ -5045,7 +5467,7 @@ var JobRunner = class {
|
|
|
5045
5467
|
console.log(`[JobRunner] Recovered stale run ${run.id} (was ${run.status})`);
|
|
5046
5468
|
}
|
|
5047
5469
|
}
|
|
5048
|
-
async executeRun(runId, projectId, providerOverride) {
|
|
5470
|
+
async executeRun(runId, projectId, providerOverride, locationOverride) {
|
|
5049
5471
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5050
5472
|
const startTime = Date.now();
|
|
5051
5473
|
try {
|
|
@@ -5054,6 +5476,17 @@ var JobRunner = class {
|
|
|
5054
5476
|
if (!project) {
|
|
5055
5477
|
throw new Error(`Project ${projectId} not found`);
|
|
5056
5478
|
}
|
|
5479
|
+
let runLocation;
|
|
5480
|
+
if (locationOverride === null) {
|
|
5481
|
+
runLocation = void 0;
|
|
5482
|
+
} else if (locationOverride) {
|
|
5483
|
+
runLocation = locationOverride;
|
|
5484
|
+
} else {
|
|
5485
|
+
const projectLocations = JSON.parse(project.locations || "[]");
|
|
5486
|
+
if (project.defaultLocation && projectLocations.length > 0) {
|
|
5487
|
+
runLocation = projectLocations.find((l) => l.label === project.defaultLocation);
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5057
5490
|
const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
|
|
5058
5491
|
const activeProviders = this.registry.getForProject(projectProviders);
|
|
5059
5492
|
if (activeProviders.length === 0) {
|
|
@@ -5098,7 +5531,8 @@ var JobRunner = class {
|
|
|
5098
5531
|
{
|
|
5099
5532
|
keyword: kw.keyword,
|
|
5100
5533
|
canonicalDomains: allDomains,
|
|
5101
|
-
competitorDomains
|
|
5534
|
+
competitorDomains,
|
|
5535
|
+
location: runLocation
|
|
5102
5536
|
},
|
|
5103
5537
|
config
|
|
5104
5538
|
);
|
|
@@ -5107,7 +5541,7 @@ var JobRunner = class {
|
|
|
5107
5541
|
const citationState = determineCitationState(normalized, allDomains);
|
|
5108
5542
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
5109
5543
|
this.db.insert(querySnapshots).values({
|
|
5110
|
-
id:
|
|
5544
|
+
id: crypto14.randomUUID(),
|
|
5111
5545
|
runId,
|
|
5112
5546
|
keywordId: kw.id,
|
|
5113
5547
|
provider: providerName,
|
|
@@ -5116,6 +5550,7 @@ var JobRunner = class {
|
|
|
5116
5550
|
answerText: normalized.answerText,
|
|
5117
5551
|
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
5118
5552
|
competitorOverlap: JSON.stringify(overlap),
|
|
5553
|
+
location: runLocation?.label ?? null,
|
|
5119
5554
|
rawResponse: JSON.stringify({
|
|
5120
5555
|
model: raw.model,
|
|
5121
5556
|
groundingSources: normalized.groundingSources,
|
|
@@ -5151,7 +5586,8 @@ var JobRunner = class {
|
|
|
5151
5586
|
providerCount: activeProviders.length,
|
|
5152
5587
|
providers: activeProviders.map((p) => p.adapter.name),
|
|
5153
5588
|
keywordCount: projectKeywords.length,
|
|
5154
|
-
durationMs: Date.now() - startTime
|
|
5589
|
+
durationMs: Date.now() - startTime,
|
|
5590
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
5155
5591
|
});
|
|
5156
5592
|
for (const p of activeProviders) {
|
|
5157
5593
|
this.incrementUsage(`${projectId}:${p.adapter.name}`, "queries", queriesPerProvider);
|
|
@@ -5204,7 +5640,7 @@ var JobRunner = class {
|
|
|
5204
5640
|
incrementUsage(scope, metric, count) {
|
|
5205
5641
|
const now = /* @__PURE__ */ new Date();
|
|
5206
5642
|
const period = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
5207
|
-
const id =
|
|
5643
|
+
const id = crypto14.randomUUID();
|
|
5208
5644
|
const existing = this.db.select().from(usageCounters).where(eq13(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
5209
5645
|
if (existing) {
|
|
5210
5646
|
this.db.update(usageCounters).set({ count: existing.count + count, updatedAt: now.toISOString() }).where(eq13(usageCounters.id, existing.id)).run();
|
|
@@ -5287,7 +5723,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
5287
5723
|
}
|
|
5288
5724
|
|
|
5289
5725
|
// src/gsc-sync.ts
|
|
5290
|
-
import
|
|
5726
|
+
import crypto15 from "crypto";
|
|
5291
5727
|
import { eq as eq14, and as and4, sql as sql3 } from "drizzle-orm";
|
|
5292
5728
|
function formatDate(d) {
|
|
5293
5729
|
return d.toISOString().split("T")[0];
|
|
@@ -5352,7 +5788,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5352
5788
|
for (const row of batch) {
|
|
5353
5789
|
const [query, page, country, device, date] = row.keys;
|
|
5354
5790
|
db.insert(gscSearchData).values({
|
|
5355
|
-
id:
|
|
5791
|
+
id: crypto15.randomUUID(),
|
|
5356
5792
|
projectId,
|
|
5357
5793
|
syncRunId: runId,
|
|
5358
5794
|
date: date ?? "",
|
|
@@ -5386,7 +5822,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5386
5822
|
const rich = ir.richResultsResult;
|
|
5387
5823
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5388
5824
|
db.insert(gscUrlInspections).values({
|
|
5389
|
-
id:
|
|
5825
|
+
id: crypto15.randomUUID(),
|
|
5390
5826
|
projectId,
|
|
5391
5827
|
syncRunId: runId,
|
|
5392
5828
|
url: pageUrl,
|
|
@@ -5407,8 +5843,40 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5407
5843
|
console.error(`[GSC Sync] Failed to inspect ${pageUrl}:`, err instanceof Error ? err.message : err);
|
|
5408
5844
|
}
|
|
5409
5845
|
}
|
|
5846
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq14(gscUrlInspections.projectId, projectId)).all();
|
|
5847
|
+
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5848
|
+
for (const row of allInspections) {
|
|
5849
|
+
const existing = latestByUrl.get(row.url);
|
|
5850
|
+
if (!existing || row.inspectedAt > existing.inspectedAt) {
|
|
5851
|
+
latestByUrl.set(row.url, row);
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
let snapIndexed = 0;
|
|
5855
|
+
let snapNotIndexed = 0;
|
|
5856
|
+
const reasonCounts = {};
|
|
5857
|
+
for (const [, row] of latestByUrl) {
|
|
5858
|
+
if (row.indexingState === "INDEXING_ALLOWED") {
|
|
5859
|
+
snapIndexed++;
|
|
5860
|
+
} else {
|
|
5861
|
+
snapNotIndexed++;
|
|
5862
|
+
const reason = row.coverageState ?? "Unknown";
|
|
5863
|
+
reasonCounts[reason] = (reasonCounts[reason] ?? 0) + 1;
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
const snapshotDate = formatDate(/* @__PURE__ */ new Date());
|
|
5867
|
+
db.delete(gscCoverageSnapshots).where(and4(eq14(gscCoverageSnapshots.projectId, projectId), eq14(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
5868
|
+
db.insert(gscCoverageSnapshots).values({
|
|
5869
|
+
id: crypto15.randomUUID(),
|
|
5870
|
+
projectId,
|
|
5871
|
+
syncRunId: runId,
|
|
5872
|
+
date: snapshotDate,
|
|
5873
|
+
indexed: snapIndexed,
|
|
5874
|
+
notIndexed: snapNotIndexed,
|
|
5875
|
+
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
5876
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5877
|
+
}).run();
|
|
5410
5878
|
db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq14(runs.id, runId)).run();
|
|
5411
|
-
console.log(`[GSC Sync] Completed. ${rows.length} search data rows, ${topPages.length} URL inspections.`);
|
|
5879
|
+
console.log(`[GSC Sync] Completed. ${rows.length} search data rows, ${topPages.length} URL inspections, coverage snapshot: ${snapIndexed} indexed / ${snapNotIndexed} not-indexed.`);
|
|
5412
5880
|
} catch (err) {
|
|
5413
5881
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5414
5882
|
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq14(runs.id, runId)).run();
|
|
@@ -5418,8 +5886,8 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
5418
5886
|
}
|
|
5419
5887
|
|
|
5420
5888
|
// src/gsc-inspect-sitemap.ts
|
|
5421
|
-
import
|
|
5422
|
-
import { eq as eq15 } from "drizzle-orm";
|
|
5889
|
+
import crypto16 from "crypto";
|
|
5890
|
+
import { eq as eq15, and as and5 } from "drizzle-orm";
|
|
5423
5891
|
|
|
5424
5892
|
// src/sitemap-parser.ts
|
|
5425
5893
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -5534,7 +6002,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
5534
6002
|
const rich = ir.richResultsResult;
|
|
5535
6003
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5536
6004
|
db.insert(gscUrlInspections).values({
|
|
5537
|
-
id:
|
|
6005
|
+
id: crypto16.randomUUID(),
|
|
5538
6006
|
projectId,
|
|
5539
6007
|
syncRunId: runId,
|
|
5540
6008
|
url: pageUrl,
|
|
@@ -5561,9 +6029,41 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
5561
6029
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
5562
6030
|
}
|
|
5563
6031
|
}
|
|
6032
|
+
const allInspections = db.select().from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, projectId)).all();
|
|
6033
|
+
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6034
|
+
for (const row of allInspections) {
|
|
6035
|
+
const existing = latestByUrl.get(row.url);
|
|
6036
|
+
if (!existing || row.inspectedAt > existing.inspectedAt) {
|
|
6037
|
+
latestByUrl.set(row.url, row);
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
let snapIndexed = 0;
|
|
6041
|
+
let snapNotIndexed = 0;
|
|
6042
|
+
const reasonCounts = {};
|
|
6043
|
+
for (const [, row] of latestByUrl) {
|
|
6044
|
+
if (row.indexingState === "INDEXING_ALLOWED") {
|
|
6045
|
+
snapIndexed++;
|
|
6046
|
+
} else {
|
|
6047
|
+
snapNotIndexed++;
|
|
6048
|
+
const reason = row.coverageState ?? "Unknown";
|
|
6049
|
+
reasonCounts[reason] = (reasonCounts[reason] ?? 0) + 1;
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
6053
|
+
db.delete(gscCoverageSnapshots).where(and5(eq15(gscCoverageSnapshots.projectId, projectId), eq15(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
6054
|
+
db.insert(gscCoverageSnapshots).values({
|
|
6055
|
+
id: crypto16.randomUUID(),
|
|
6056
|
+
projectId,
|
|
6057
|
+
syncRunId: runId,
|
|
6058
|
+
date: snapshotDate,
|
|
6059
|
+
indexed: snapIndexed,
|
|
6060
|
+
notIndexed: snapNotIndexed,
|
|
6061
|
+
reasonBreakdown: JSON.stringify(reasonCounts),
|
|
6062
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6063
|
+
}).run();
|
|
5564
6064
|
const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
|
|
5565
6065
|
db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(runs.id, runId)).run();
|
|
5566
|
-
console.log(`[Inspect Sitemap] Done. ${inspected} inspected, ${errors} errors out of ${urls.length} URLs.`);
|
|
6066
|
+
console.log(`[Inspect Sitemap] Done. ${inspected} inspected, ${errors} errors out of ${urls.length} URLs. Coverage: ${snapIndexed} indexed / ${snapNotIndexed} not-indexed.`);
|
|
5567
6067
|
} catch (err) {
|
|
5568
6068
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
5569
6069
|
db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq15(runs.id, runId)).run();
|
|
@@ -5734,8 +6234,8 @@ var Scheduler = class {
|
|
|
5734
6234
|
};
|
|
5735
6235
|
|
|
5736
6236
|
// src/notifier.ts
|
|
5737
|
-
import { eq as eq17, desc as desc3, and as
|
|
5738
|
-
import
|
|
6237
|
+
import { eq as eq17, desc as desc3, and as and6, or as or2 } from "drizzle-orm";
|
|
6238
|
+
import crypto17 from "crypto";
|
|
5739
6239
|
var Notifier = class {
|
|
5740
6240
|
db;
|
|
5741
6241
|
serverUrl;
|
|
@@ -5797,7 +6297,7 @@ var Notifier = class {
|
|
|
5797
6297
|
}
|
|
5798
6298
|
computeTransitions(runId, projectId) {
|
|
5799
6299
|
const recentRuns = this.db.select().from(runs).where(
|
|
5800
|
-
|
|
6300
|
+
and6(
|
|
5801
6301
|
eq17(runs.projectId, projectId),
|
|
5802
6302
|
or2(eq17(runs.status, "completed"), eq17(runs.status, "partial"))
|
|
5803
6303
|
)
|
|
@@ -5873,7 +6373,7 @@ var Notifier = class {
|
|
|
5873
6373
|
}
|
|
5874
6374
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
5875
6375
|
this.db.insert(auditLog).values({
|
|
5876
|
-
id:
|
|
6376
|
+
id: crypto17.randomUUID(),
|
|
5877
6377
|
projectId,
|
|
5878
6378
|
actor: "scheduler",
|
|
5879
6379
|
action: `notification.${status}`,
|
|
@@ -6096,7 +6596,7 @@ async function createServer(opts) {
|
|
|
6096
6596
|
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
6097
6597
|
};
|
|
6098
6598
|
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
6099
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
6599
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto18.randomBytes(32).toString("hex");
|
|
6100
6600
|
const googleConnectionStore = {
|
|
6101
6601
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
6102
6602
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
@@ -6155,8 +6655,8 @@ async function createServer(opts) {
|
|
|
6155
6655
|
},
|
|
6156
6656
|
providerSummary,
|
|
6157
6657
|
googleSettingsSummary,
|
|
6158
|
-
onRunCreated: (runId, projectId, providers2) => {
|
|
6159
|
-
jobRunner.executeRun(runId, projectId, providers2).catch((err) => {
|
|
6658
|
+
onRunCreated: (runId, projectId, providers2, location) => {
|
|
6659
|
+
jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
|
|
6160
6660
|
app.log.error({ runId, err }, "Job runner failed");
|
|
6161
6661
|
});
|
|
6162
6662
|
},
|
|
@@ -6210,7 +6710,7 @@ async function createServer(opts) {
|
|
|
6210
6710
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
6211
6711
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6212
6712
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
6213
|
-
id:
|
|
6713
|
+
id: crypto18.randomUUID(),
|
|
6214
6714
|
projectId,
|
|
6215
6715
|
actor: "api",
|
|
6216
6716
|
action: existing ? "provider.updated" : "provider.created",
|