@ainyc/canonry 4.72.4 → 4.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
- package/assets/agent-workspace/skills/canonry/SKILL.md +1 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +17 -0
- package/assets/assets/{BacklinksPage-CjfpwZEH.js → BacklinksPage-CwAplOLo.js} +1 -1
- package/assets/assets/{ChartPrimitives-Ckf2FrUy.js → ChartPrimitives-EGp5HFxn.js} +1 -1
- package/assets/assets/ProjectPage-C-zhkBKK.js +6 -0
- package/assets/assets/{RunRow-BuFyG0V_.js → RunRow-YFN2PwH-.js} +1 -1
- package/assets/assets/{RunsPage-D-pr000K.js → RunsPage-DlKS8zaS.js} +1 -1
- package/assets/assets/{SettingsPage-CiaapCYn.js → SettingsPage-Q0OZKjMD.js} +1 -1
- package/assets/assets/{TrafficPage-B40xytJD.js → TrafficPage-BbySUnhy.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-7hHem-gM.js → TrafficSourceDetailPage-BGzuvTYp.js} +1 -1
- package/assets/assets/{extract-error-message-3GkDsu1h.js → extract-error-message-Czt2jFxA.js} +1 -1
- package/assets/assets/index-CFVX11lK.css +1 -0
- package/assets/assets/{index-BVdH2O9w.js → index-DYsYdWV8.js} +118 -118
- package/assets/assets/{server-traffic-CsgPsudZ.js → server-traffic-BzIFKqGS.js} +1 -1
- package/assets/assets/{trash-2-B8Ipf9rI.js → trash-2-DKCkbZUb.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-JXFNERK4.js → chunk-JNAKRK77.js} +1103 -998
- package/dist/{chunk-HOKVBMOD.js → chunk-JUWU2DV6.js} +402 -81
- package/dist/{chunk-SRBO33HB.js → chunk-QY5WZWU4.js} +403 -202
- package/dist/{chunk-ZUBBADMR.js → chunk-WFMEK34V.js} +162 -1
- package/dist/cli.js +237 -31
- package/dist/index.d.ts +10 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CSW4R4I7.js → intelligence-service-L2A5MFB4.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/ProjectPage-DZeplYeC.js +0 -6
- package/assets/assets/index-B3nENtU0.css +0 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
RunTriggers,
|
|
22
22
|
SKILL_MANIFEST_FILENAME,
|
|
23
23
|
SchedulableRunKinds,
|
|
24
|
+
SiteAuditTrendDirections,
|
|
24
25
|
TrafficEventConfidences,
|
|
25
26
|
TrafficEventKinds,
|
|
26
27
|
TrafficEvidenceKinds,
|
|
@@ -100,6 +101,7 @@ import {
|
|
|
100
101
|
effectiveBrandNames,
|
|
101
102
|
effectiveDomains,
|
|
102
103
|
emptyCitationVisibility,
|
|
104
|
+
escapeLikePattern,
|
|
103
105
|
extractAnswerMentions,
|
|
104
106
|
findDuplicateLocationLabels,
|
|
105
107
|
forbidden,
|
|
@@ -186,6 +188,11 @@ import {
|
|
|
186
188
|
scheduleUpsertRequestSchema,
|
|
187
189
|
serializeRunError,
|
|
188
190
|
settingsDtoSchema,
|
|
191
|
+
siteAuditPagesResponseSchema,
|
|
192
|
+
siteAuditRunRequestSchema,
|
|
193
|
+
siteAuditRunResponseSchema,
|
|
194
|
+
siteAuditScoreSchema,
|
|
195
|
+
siteAuditTrendResponseSchema,
|
|
189
196
|
snapshotDiffResponseSchema,
|
|
190
197
|
snapshotListResponseSchema,
|
|
191
198
|
snapshotReportSchema,
|
|
@@ -223,10 +230,10 @@ import {
|
|
|
223
230
|
wordpressSchemaDeployResultDtoSchema,
|
|
224
231
|
wordpressSchemaStatusResultDtoSchema,
|
|
225
232
|
wordpressStatusDtoSchema
|
|
226
|
-
} from "./chunk-
|
|
233
|
+
} from "./chunk-JNAKRK77.js";
|
|
227
234
|
|
|
228
235
|
// src/intelligence-service.ts
|
|
229
|
-
import { eq as
|
|
236
|
+
import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
|
|
230
237
|
|
|
231
238
|
// ../db/src/client.ts
|
|
232
239
|
import { mkdirSync } from "fs";
|
|
@@ -285,6 +292,8 @@ __export(schema_exports, {
|
|
|
285
292
|
recommendationExplanations: () => recommendationExplanations,
|
|
286
293
|
runs: () => runs,
|
|
287
294
|
schedules: () => schedules,
|
|
295
|
+
siteAuditPages: () => siteAuditPages,
|
|
296
|
+
siteAuditSnapshots: () => siteAuditSnapshots,
|
|
288
297
|
trafficSources: () => trafficSources,
|
|
289
298
|
usageCounters: () => usageCounters
|
|
290
299
|
});
|
|
@@ -533,6 +542,39 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
|
|
|
533
542
|
index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
|
|
534
543
|
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
535
544
|
]);
|
|
545
|
+
var siteAuditSnapshots = sqliteTable("site_audit_snapshots", {
|
|
546
|
+
id: text("id").primaryKey(),
|
|
547
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
548
|
+
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
549
|
+
sitemapUrl: text("sitemap_url").notNull(),
|
|
550
|
+
auditedAt: text("audited_at").notNull(),
|
|
551
|
+
aggregateScore: integer("aggregate_score").notNull().default(0),
|
|
552
|
+
pagesDiscovered: integer("pages_discovered").notNull().default(0),
|
|
553
|
+
pagesAudited: integer("pages_audited").notNull().default(0),
|
|
554
|
+
pagesSkipped: integer("pages_skipped").notNull().default(0),
|
|
555
|
+
pagesErrored: integer("pages_errored").notNull().default(0),
|
|
556
|
+
factorAverages: text("factor_averages", { mode: "json" }).$type().notNull().default([]),
|
|
557
|
+
crossCuttingIssues: text("cross_cutting_issues", { mode: "json" }).$type().notNull().default([]),
|
|
558
|
+
prioritizedFixes: text("prioritized_fixes", { mode: "json" }).$type().notNull().default([]),
|
|
559
|
+
createdAt: text("created_at").notNull()
|
|
560
|
+
}, (table) => [
|
|
561
|
+
index("idx_site_audit_snap_project_created").on(table.projectId, table.createdAt),
|
|
562
|
+
index("idx_site_audit_snap_run").on(table.runId)
|
|
563
|
+
]);
|
|
564
|
+
var siteAuditPages = sqliteTable("site_audit_pages", {
|
|
565
|
+
id: text("id").primaryKey(),
|
|
566
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
567
|
+
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
568
|
+
url: text("url").notNull(),
|
|
569
|
+
overallScore: integer("overall_score").notNull().default(0),
|
|
570
|
+
status: text("status").notNull(),
|
|
571
|
+
error: text("error"),
|
|
572
|
+
factors: text("factors", { mode: "json" }).$type().notNull().default([]),
|
|
573
|
+
createdAt: text("created_at").notNull()
|
|
574
|
+
}, (table) => [
|
|
575
|
+
index("idx_site_audit_pages_run").on(table.runId),
|
|
576
|
+
index("idx_site_audit_pages_project_score").on(table.projectId, table.overallScore)
|
|
577
|
+
]);
|
|
536
578
|
var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
|
|
537
579
|
id: text("id").primaryKey(),
|
|
538
580
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -2780,6 +2822,47 @@ var MIGRATION_VERSIONS = [
|
|
|
2780
2822
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_briefs_unique ON recommendation_briefs(project_id, target_ref, prompt_version)`,
|
|
2781
2823
|
`CREATE INDEX IF NOT EXISTS idx_recommendation_briefs_project ON recommendation_briefs(project_id)`
|
|
2782
2824
|
]
|
|
2825
|
+
},
|
|
2826
|
+
{
|
|
2827
|
+
// Technical AEO — site-wide audit persistence. `site_audit_snapshots` is the
|
|
2828
|
+
// per-run summary (drives the score + trend); `site_audit_pages` is the
|
|
2829
|
+
// per-page breakdown (drives the drill-down table). Both cascade off runs so
|
|
2830
|
+
// a run delete cleans up its audit data.
|
|
2831
|
+
version: 75,
|
|
2832
|
+
name: "site-audit-tables",
|
|
2833
|
+
statements: [
|
|
2834
|
+
`CREATE TABLE IF NOT EXISTS site_audit_snapshots (
|
|
2835
|
+
id TEXT PRIMARY KEY,
|
|
2836
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2837
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
2838
|
+
sitemap_url TEXT NOT NULL,
|
|
2839
|
+
audited_at TEXT NOT NULL,
|
|
2840
|
+
aggregate_score INTEGER NOT NULL DEFAULT 0,
|
|
2841
|
+
pages_discovered INTEGER NOT NULL DEFAULT 0,
|
|
2842
|
+
pages_audited INTEGER NOT NULL DEFAULT 0,
|
|
2843
|
+
pages_skipped INTEGER NOT NULL DEFAULT 0,
|
|
2844
|
+
pages_errored INTEGER NOT NULL DEFAULT 0,
|
|
2845
|
+
factor_averages TEXT NOT NULL DEFAULT '[]',
|
|
2846
|
+
cross_cutting_issues TEXT NOT NULL DEFAULT '[]',
|
|
2847
|
+
prioritized_fixes TEXT NOT NULL DEFAULT '[]',
|
|
2848
|
+
created_at TEXT NOT NULL
|
|
2849
|
+
)`,
|
|
2850
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_snap_project_created ON site_audit_snapshots(project_id, created_at)`,
|
|
2851
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_snap_run ON site_audit_snapshots(run_id)`,
|
|
2852
|
+
`CREATE TABLE IF NOT EXISTS site_audit_pages (
|
|
2853
|
+
id TEXT PRIMARY KEY,
|
|
2854
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2855
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
2856
|
+
url TEXT NOT NULL,
|
|
2857
|
+
overall_score INTEGER NOT NULL DEFAULT 0,
|
|
2858
|
+
status TEXT NOT NULL,
|
|
2859
|
+
error TEXT,
|
|
2860
|
+
factors TEXT NOT NULL DEFAULT '[]',
|
|
2861
|
+
created_at TEXT NOT NULL
|
|
2862
|
+
)`,
|
|
2863
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_pages_run ON site_audit_pages(run_id)`,
|
|
2864
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_pages_project_score ON site_audit_pages(project_id, overall_score)`
|
|
2865
|
+
]
|
|
2783
2866
|
}
|
|
2784
2867
|
];
|
|
2785
2868
|
function isDuplicateColumnError(err) {
|
|
@@ -4098,15 +4181,15 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
|
|
|
4098
4181
|
totalCitations++;
|
|
4099
4182
|
}
|
|
4100
4183
|
}
|
|
4101
|
-
const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
|
|
4184
|
+
const categories = [...categoryCounts.entries()].map(([category, { label, count: count2 }]) => ({
|
|
4102
4185
|
category,
|
|
4103
4186
|
label,
|
|
4104
|
-
count,
|
|
4105
|
-
sharePct: totalCitations > 0 ? Math.round(
|
|
4187
|
+
count: count2,
|
|
4188
|
+
sharePct: totalCitations > 0 ? Math.round(count2 / totalCitations * 100) : 0
|
|
4106
4189
|
})).sort((a, b) => b.count - a.count);
|
|
4107
|
-
const topDomains = [...domainCounts.entries()].map(([domain,
|
|
4190
|
+
const topDomains = [...domainCounts.entries()].map(([domain, count2]) => ({
|
|
4108
4191
|
domain,
|
|
4109
|
-
count,
|
|
4192
|
+
count: count2,
|
|
4110
4193
|
isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
|
|
4111
4194
|
})).sort((a, b) => b.count - a.count).slice(0, topDomainsLimit);
|
|
4112
4195
|
return { categories, topDomains };
|
|
@@ -4947,7 +5030,7 @@ var SKIP_PATHS = ["/health"];
|
|
|
4947
5030
|
function shouldSkipAuth(url) {
|
|
4948
5031
|
if (SKIP_PATHS.includes(url)) return true;
|
|
4949
5032
|
if (url.endsWith("/openapi.json")) return true;
|
|
4950
|
-
if (url.
|
|
5033
|
+
if (url.endsWith("/google/callback")) return true;
|
|
4951
5034
|
if (url.endsWith("/session") || url.endsWith("/session/setup")) return true;
|
|
4952
5035
|
return false;
|
|
4953
5036
|
}
|
|
@@ -5203,7 +5286,7 @@ async function projectRoutes(app, opts) {
|
|
|
5203
5286
|
app.get("/projects/:name/delete-preview", async (request, reply) => {
|
|
5204
5287
|
const project = resolveProject(app.db, request.params.name);
|
|
5205
5288
|
const pid = project.id;
|
|
5206
|
-
const
|
|
5289
|
+
const count2 = (n) => n ?? 0;
|
|
5207
5290
|
const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
|
|
5208
5291
|
const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
|
|
5209
5292
|
const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
|
|
@@ -5213,14 +5296,14 @@ async function projectRoutes(app, opts) {
|
|
|
5213
5296
|
return reply.send({
|
|
5214
5297
|
project: { id: project.id, name: project.name },
|
|
5215
5298
|
cascadeRows: {
|
|
5216
|
-
queries:
|
|
5217
|
-
competitors:
|
|
5218
|
-
runs:
|
|
5219
|
-
snapshots:
|
|
5220
|
-
insights:
|
|
5299
|
+
queries: count2(queryCount?.n),
|
|
5300
|
+
competitors: count2(competitorCount?.n),
|
|
5301
|
+
runs: count2(runCount?.n),
|
|
5302
|
+
snapshots: count2(snapshotCount?.n),
|
|
5303
|
+
insights: count2(insightCount?.n)
|
|
5221
5304
|
},
|
|
5222
5305
|
detachedRows: {
|
|
5223
|
-
auditLog:
|
|
5306
|
+
auditLog: count2(auditLogCount?.n)
|
|
5224
5307
|
}
|
|
5225
5308
|
});
|
|
5226
5309
|
});
|
|
@@ -5552,14 +5635,14 @@ async function queryRoutes(app, opts) {
|
|
|
5552
5635
|
validProviders: validNames
|
|
5553
5636
|
});
|
|
5554
5637
|
}
|
|
5555
|
-
const
|
|
5638
|
+
const count2 = body.count ?? 5;
|
|
5556
5639
|
if (!opts.onGenerateQueries) {
|
|
5557
5640
|
throw notImplemented("Query generation is not supported in this deployment");
|
|
5558
5641
|
}
|
|
5559
5642
|
const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
|
|
5560
5643
|
const existingQueries = existingRows.map((r) => r.query);
|
|
5561
5644
|
try {
|
|
5562
|
-
const generated = await opts.onGenerateQueries(provider,
|
|
5645
|
+
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
5563
5646
|
domain: project.canonicalDomain,
|
|
5564
5647
|
displayName: project.displayName,
|
|
5565
5648
|
country: project.country,
|
|
@@ -5689,14 +5772,14 @@ async function queryRoutes(app, opts) {
|
|
|
5689
5772
|
validProviders: validNames
|
|
5690
5773
|
});
|
|
5691
5774
|
}
|
|
5692
|
-
const
|
|
5775
|
+
const count2 = body.count ?? 5;
|
|
5693
5776
|
if (!opts.onGenerateQueries) {
|
|
5694
5777
|
throw notImplemented("Keyword generation is not supported in this deployment");
|
|
5695
5778
|
}
|
|
5696
5779
|
const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
|
|
5697
5780
|
const existingQueries = existingRows.map((r) => r.query);
|
|
5698
5781
|
try {
|
|
5699
|
-
const generated = await opts.onGenerateQueries(provider,
|
|
5782
|
+
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
5700
5783
|
domain: project.canonicalDomain,
|
|
5701
5784
|
displayName: project.displayName,
|
|
5702
5785
|
country: project.country,
|
|
@@ -7391,10 +7474,10 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
|
|
|
7391
7474
|
const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
|
|
7392
7475
|
const buckets = [];
|
|
7393
7476
|
let start = new Date(earliest);
|
|
7394
|
-
start.
|
|
7477
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
7395
7478
|
while (start <= latest) {
|
|
7396
7479
|
const end = new Date(start);
|
|
7397
|
-
end.
|
|
7480
|
+
end.setUTCDate(end.getUTCDate() + bucketDays);
|
|
7398
7481
|
const startISO = start.toISOString();
|
|
7399
7482
|
const endISO = end.toISOString();
|
|
7400
7483
|
const inBucket = snapshots.filter((s) => s.createdAt >= startISO && s.createdAt < endISO);
|
|
@@ -7438,10 +7521,10 @@ function computeQueryChanges(projectQueries, cutoff) {
|
|
|
7438
7521
|
}
|
|
7439
7522
|
const days = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7440
7523
|
if (days.length <= 1) return [];
|
|
7441
|
-
return days.slice(1).map(([date,
|
|
7524
|
+
return days.slice(1).map(([date, count2]) => ({
|
|
7442
7525
|
date: (/* @__PURE__ */ new Date(date + "T00:00:00.000Z")).toISOString(),
|
|
7443
|
-
delta:
|
|
7444
|
-
label: `+${
|
|
7526
|
+
delta: count2,
|
|
7527
|
+
label: `+${count2} kp`
|
|
7445
7528
|
}));
|
|
7446
7529
|
}
|
|
7447
7530
|
function computeTrend(buckets, rateKey) {
|
|
@@ -7506,15 +7589,15 @@ function buildRankedList(domains, limit) {
|
|
|
7506
7589
|
function buildCategoryCounts(counts) {
|
|
7507
7590
|
let grandTotal = 0;
|
|
7508
7591
|
for (const domains of counts.values()) {
|
|
7509
|
-
for (const
|
|
7592
|
+
for (const count2 of domains.values()) grandTotal += count2;
|
|
7510
7593
|
}
|
|
7511
7594
|
const result = [];
|
|
7512
7595
|
for (const [category, domains] of counts) {
|
|
7513
7596
|
let categoryTotal = 0;
|
|
7514
7597
|
const domainEntries = [];
|
|
7515
|
-
for (const [domain,
|
|
7516
|
-
categoryTotal +=
|
|
7517
|
-
domainEntries.push({ domain, count });
|
|
7598
|
+
for (const [domain, count2] of domains) {
|
|
7599
|
+
categoryTotal += count2;
|
|
7600
|
+
domainEntries.push({ domain, count: count2 });
|
|
7518
7601
|
}
|
|
7519
7602
|
domainEntries.sort((a, b) => b.count - a.count);
|
|
7520
7603
|
result.push({
|
|
@@ -8488,8 +8571,8 @@ function gscDateRange(report) {
|
|
|
8488
8571
|
const end = summary?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
|
|
8489
8572
|
return formatDateRange(start, end);
|
|
8490
8573
|
}
|
|
8491
|
-
function pluralize(
|
|
8492
|
-
return
|
|
8574
|
+
function pluralize(count2, singular, plural = `${singular}s`) {
|
|
8575
|
+
return count2 === 1 ? singular : plural;
|
|
8493
8576
|
}
|
|
8494
8577
|
var PROVIDER_DISPLAY_NAMES = {
|
|
8495
8578
|
gemini: "Gemini",
|
|
@@ -10292,9 +10375,9 @@ function renderInsights(report) {
|
|
|
10292
10375
|
);
|
|
10293
10376
|
}
|
|
10294
10377
|
const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
|
|
10295
|
-
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count }) => {
|
|
10378
|
+
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count: count2 }) => {
|
|
10296
10379
|
const tone = severityTone(i.severity);
|
|
10297
|
-
const countChip =
|
|
10380
|
+
const countChip = count2 > 1 ? ` <span class="badge tone-neutral">\xD7 ${count2}</span>` : "";
|
|
10298
10381
|
return `<tr>
|
|
10299
10382
|
<td class="col-severity"><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
|
|
10300
10383
|
<td class="col-title">${escapeHtml(i.title)}${countChip}</td>
|
|
@@ -11494,9 +11577,9 @@ function contentActionVerb(action) {
|
|
|
11494
11577
|
return "Add schema to";
|
|
11495
11578
|
}
|
|
11496
11579
|
}
|
|
11497
|
-
function confidenceFromEvidence(
|
|
11498
|
-
if (
|
|
11499
|
-
if (
|
|
11580
|
+
function confidenceFromEvidence(count2) {
|
|
11581
|
+
if (count2 >= 3) return "high";
|
|
11582
|
+
if (count2 >= 1) return "medium";
|
|
11500
11583
|
return "low";
|
|
11501
11584
|
}
|
|
11502
11585
|
function actionAudienceMatches2(action, audience) {
|
|
@@ -12493,9 +12576,6 @@ function clampSearchLimit(raw) {
|
|
|
12493
12576
|
if (parsed > SEARCH_HIT_HARD_LIMIT) return SEARCH_HIT_HARD_LIMIT;
|
|
12494
12577
|
return parsed;
|
|
12495
12578
|
}
|
|
12496
|
-
function escapeLikePattern(value) {
|
|
12497
|
-
return value.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
12498
|
-
}
|
|
12499
12579
|
function summarizeRun(run) {
|
|
12500
12580
|
return {
|
|
12501
12581
|
id: run.id,
|
|
@@ -13003,6 +13083,10 @@ var SCHEMA_TABLE = {
|
|
|
13003
13083
|
SchedulableRunKind: schedulableRunKindSchema,
|
|
13004
13084
|
ScheduleDto: scheduleDtoSchema,
|
|
13005
13085
|
SettingsDto: settingsDtoSchema,
|
|
13086
|
+
SiteAuditPagesResponseDto: siteAuditPagesResponseSchema,
|
|
13087
|
+
SiteAuditRunResponseDto: siteAuditRunResponseSchema,
|
|
13088
|
+
SiteAuditScoreDto: siteAuditScoreSchema,
|
|
13089
|
+
SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
|
|
13006
13090
|
SnapshotDiffResponse: snapshotDiffResponseSchema,
|
|
13007
13091
|
SnapshotListResponse: snapshotListResponseSchema,
|
|
13008
13092
|
SnapshotReportDto: snapshotReportSchema,
|
|
@@ -16688,6 +16772,78 @@ var routeCatalog = [
|
|
|
16688
16772
|
400: errorResponse("Session is not completed, or invalid request body."),
|
|
16689
16773
|
404: errorResponse("Project or session not found.")
|
|
16690
16774
|
}
|
|
16775
|
+
},
|
|
16776
|
+
{
|
|
16777
|
+
method: "get",
|
|
16778
|
+
path: "/api/v1/projects/{name}/technical-aeo",
|
|
16779
|
+
summary: "Get the Technical AEO scorecard for a project",
|
|
16780
|
+
description: "Returns the latest completed/partial site-audit: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
|
|
16781
|
+
tags: ["technical-aeo"],
|
|
16782
|
+
parameters: [nameParameter],
|
|
16783
|
+
responses: {
|
|
16784
|
+
200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
|
|
16785
|
+
404: errorResponse("Project not found.")
|
|
16786
|
+
}
|
|
16787
|
+
},
|
|
16788
|
+
{
|
|
16789
|
+
method: "get",
|
|
16790
|
+
path: "/api/v1/projects/{name}/technical-aeo/pages",
|
|
16791
|
+
summary: "List audited pages from the latest site-audit run",
|
|
16792
|
+
description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
|
|
16793
|
+
tags: ["technical-aeo"],
|
|
16794
|
+
parameters: [
|
|
16795
|
+
nameParameter,
|
|
16796
|
+
{ name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
|
|
16797
|
+
{ name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
|
|
16798
|
+
limitQueryParameter,
|
|
16799
|
+
offsetQueryParameter
|
|
16800
|
+
],
|
|
16801
|
+
responses: {
|
|
16802
|
+
200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
|
|
16803
|
+
404: errorResponse("Project not found.")
|
|
16804
|
+
}
|
|
16805
|
+
},
|
|
16806
|
+
{
|
|
16807
|
+
method: "get",
|
|
16808
|
+
path: "/api/v1/projects/{name}/technical-aeo/trend",
|
|
16809
|
+
summary: "Get the Technical AEO aggregate-score trend",
|
|
16810
|
+
description: "Returns historical aggregate scores across completed/partial site-audit runs, oldest-first, for the trend chart.",
|
|
16811
|
+
tags: ["technical-aeo"],
|
|
16812
|
+
parameters: [
|
|
16813
|
+
nameParameter,
|
|
16814
|
+
{ name: "limit", in: "query", description: "Max data points returned (most recent runs). Default 30.", schema: integerSchema }
|
|
16815
|
+
],
|
|
16816
|
+
responses: {
|
|
16817
|
+
200: jsonResponse("Technical AEO trend returned.", "SiteAuditTrendResponseDto"),
|
|
16818
|
+
404: errorResponse("Project not found.")
|
|
16819
|
+
}
|
|
16820
|
+
},
|
|
16821
|
+
{
|
|
16822
|
+
method: "post",
|
|
16823
|
+
path: "/api/v1/projects/{name}/technical-aeo/runs",
|
|
16824
|
+
summary: "Trigger a Technical AEO site-audit run",
|
|
16825
|
+
description: "Queues a `site-audit` run that crawls the project sitemap and audits every reachable page. Idempotent: if a site-audit run is already queued/running for the project, returns that run instead of starting a second.",
|
|
16826
|
+
tags: ["technical-aeo"],
|
|
16827
|
+
parameters: [nameParameter],
|
|
16828
|
+
requestBody: {
|
|
16829
|
+
required: false,
|
|
16830
|
+
content: {
|
|
16831
|
+
"application/json": {
|
|
16832
|
+
schema: {
|
|
16833
|
+
type: "object",
|
|
16834
|
+
properties: {
|
|
16835
|
+
sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
|
|
16836
|
+
limit: { ...integerSchema, description: "Cap pages audited (highest sitemap <priority> first). Max 2000." }
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
},
|
|
16842
|
+
responses: {
|
|
16843
|
+
200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
|
|
16844
|
+
400: errorResponse("Invalid site-audit request."),
|
|
16845
|
+
404: errorResponse("Project not found.")
|
|
16846
|
+
}
|
|
16691
16847
|
}
|
|
16692
16848
|
];
|
|
16693
16849
|
var canonryLocalRouteCatalog = [
|
|
@@ -17201,6 +17357,9 @@ async function scheduleRoutes(app, opts) {
|
|
|
17201
17357
|
if (kind === SchedulableRunKinds["backlinks-sync"] && providers && providers.length > 0) {
|
|
17202
17358
|
throw validationError('"providers" is not valid for kind "backlinks-sync"');
|
|
17203
17359
|
}
|
|
17360
|
+
if (kind === SchedulableRunKinds["site-audit"] && providers && providers.length > 0) {
|
|
17361
|
+
throw validationError('"providers" is not valid for kind "site-audit"');
|
|
17362
|
+
}
|
|
17204
17363
|
const validNames = opts.validProviderNames ?? [];
|
|
17205
17364
|
if (validNames.length && providers?.length) {
|
|
17206
17365
|
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
@@ -18898,12 +19057,12 @@ async function getLodging(accessToken, locationName, opts = {}) {
|
|
|
18898
19057
|
}
|
|
18899
19058
|
}
|
|
18900
19059
|
function countPopulatedGroups(lodging) {
|
|
18901
|
-
let
|
|
19060
|
+
let count2 = 0;
|
|
18902
19061
|
for (const [key, value] of Object.entries(lodging)) {
|
|
18903
19062
|
if (key === "name" || key === "metadata") continue;
|
|
18904
|
-
if (isPopulated(value))
|
|
19063
|
+
if (isPopulated(value)) count2++;
|
|
18905
19064
|
}
|
|
18906
|
-
return
|
|
19065
|
+
return count2;
|
|
18907
19066
|
}
|
|
18908
19067
|
function isPopulated(value) {
|
|
18909
19068
|
if (value === null || value === void 0) return false;
|
|
@@ -19239,8 +19398,8 @@ async function googleRoutes(app, opts) {
|
|
|
19239
19398
|
if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
|
|
19240
19399
|
else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
|
|
19241
19400
|
if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
|
|
19242
|
-
if (query) conditions.push(sql8`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
19243
|
-
if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
19401
|
+
if (query) conditions.push(sql8`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
|
|
19402
|
+
if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
|
|
19244
19403
|
const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
|
|
19245
19404
|
const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
|
|
19246
19405
|
const rows = app.db.select().from(gscSearchData).where(and13(...conditions)).orderBy(desc10(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
|
|
@@ -22854,10 +23013,17 @@ async function withWordpressErrorHandling(handler) {
|
|
|
22854
23013
|
}
|
|
22855
23014
|
}
|
|
22856
23015
|
async function wordpressRoutes(app, opts) {
|
|
23016
|
+
const allowLoopback = opts.allowLoopbackWebhooks === true;
|
|
22857
23017
|
function requireStore() {
|
|
22858
23018
|
if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
|
|
22859
23019
|
throw validationError("WordPress connection storage is not configured for this deployment");
|
|
22860
23020
|
}
|
|
23021
|
+
async function assertWordpressUrlAllowed(rawUrl, field) {
|
|
23022
|
+
const check = await resolveWebhookTarget(rawUrl, { allowLoopback });
|
|
23023
|
+
if (!check.ok) {
|
|
23024
|
+
throw validationError(`${field} ${check.message.replace(/^"url" /, "")}`);
|
|
23025
|
+
}
|
|
23026
|
+
}
|
|
22861
23027
|
function requireConnection(store, projectName) {
|
|
22862
23028
|
const connection = store.getConnection(projectName);
|
|
22863
23029
|
if (!connection) {
|
|
@@ -22877,6 +23043,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
22877
23043
|
if (defaultEnv === "staging" && !stagingUrl) {
|
|
22878
23044
|
throw validationError('defaultEnv "staging" requires stagingUrl');
|
|
22879
23045
|
}
|
|
23046
|
+
await assertWordpressUrlAllowed(url, "url");
|
|
23047
|
+
if (stagingUrl) await assertWordpressUrlAllowed(stagingUrl, "stagingUrl");
|
|
22880
23048
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22881
23049
|
const existing = store.getConnection(project.name);
|
|
22882
23050
|
const nextConnection = {
|
|
@@ -23205,6 +23373,8 @@ async function wordpressRoutes(app, opts) {
|
|
|
23205
23373
|
if (defaultEnv === "staging" && !stagingUrl) {
|
|
23206
23374
|
throw validationError('defaultEnv "staging" requires stagingUrl');
|
|
23207
23375
|
}
|
|
23376
|
+
await assertWordpressUrlAllowed(url, "url");
|
|
23377
|
+
if (stagingUrl) await assertWordpressUrlAllowed(stagingUrl, "stagingUrl");
|
|
23208
23378
|
const steps = [];
|
|
23209
23379
|
let connection = null;
|
|
23210
23380
|
let pageUrls = [];
|
|
@@ -31675,6 +31845,151 @@ function dedupeStrings(input) {
|
|
|
31675
31845
|
return out;
|
|
31676
31846
|
}
|
|
31677
31847
|
|
|
31848
|
+
// ../api-routes/src/technical-aeo.ts
|
|
31849
|
+
import crypto27 from "crypto";
|
|
31850
|
+
import { and as and23, asc as asc3, count, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
|
|
31851
|
+
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
31852
|
+
function emptyScore(projectName) {
|
|
31853
|
+
return {
|
|
31854
|
+
project: projectName,
|
|
31855
|
+
hasData: false,
|
|
31856
|
+
runId: null,
|
|
31857
|
+
runStatus: null,
|
|
31858
|
+
sitemapUrl: null,
|
|
31859
|
+
auditedAt: null,
|
|
31860
|
+
aggregateScore: 0,
|
|
31861
|
+
pagesDiscovered: 0,
|
|
31862
|
+
pagesAudited: 0,
|
|
31863
|
+
pagesSkipped: 0,
|
|
31864
|
+
pagesErrored: 0,
|
|
31865
|
+
deltaScore: null,
|
|
31866
|
+
trend: null,
|
|
31867
|
+
previousScore: null,
|
|
31868
|
+
previousAuditedAt: null,
|
|
31869
|
+
factors: [],
|
|
31870
|
+
crossCuttingIssues: [],
|
|
31871
|
+
prioritizedFixes: []
|
|
31872
|
+
};
|
|
31873
|
+
}
|
|
31874
|
+
function parsePositiveInt(value, fallback, max) {
|
|
31875
|
+
const n = typeof value === "string" ? Number.parseInt(value, 10) : typeof value === "number" ? value : NaN;
|
|
31876
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
31877
|
+
return Math.min(max, Math.floor(n));
|
|
31878
|
+
}
|
|
31879
|
+
async function technicalAeoRoutes(app, opts) {
|
|
31880
|
+
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
31881
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31882
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31883
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31884
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31885
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31886
|
+
notProbeRun()
|
|
31887
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
31888
|
+
const latest = rows[0];
|
|
31889
|
+
if (!latest) return emptyScore(project.name);
|
|
31890
|
+
const snap = latest.snap;
|
|
31891
|
+
const previous = rows[1]?.snap ?? null;
|
|
31892
|
+
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
31893
|
+
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
31894
|
+
return {
|
|
31895
|
+
project: project.name,
|
|
31896
|
+
hasData: true,
|
|
31897
|
+
runId: snap.runId,
|
|
31898
|
+
runStatus: latest.runStatus,
|
|
31899
|
+
sitemapUrl: snap.sitemapUrl,
|
|
31900
|
+
auditedAt: snap.auditedAt,
|
|
31901
|
+
aggregateScore: snap.aggregateScore,
|
|
31902
|
+
pagesDiscovered: snap.pagesDiscovered,
|
|
31903
|
+
pagesAudited: snap.pagesAudited,
|
|
31904
|
+
pagesSkipped: snap.pagesSkipped,
|
|
31905
|
+
pagesErrored: snap.pagesErrored,
|
|
31906
|
+
deltaScore,
|
|
31907
|
+
trend,
|
|
31908
|
+
previousScore: previous?.aggregateScore ?? null,
|
|
31909
|
+
previousAuditedAt: previous?.auditedAt ?? null,
|
|
31910
|
+
factors: snap.factorAverages,
|
|
31911
|
+
crossCuttingIssues: snap.crossCuttingIssues,
|
|
31912
|
+
prioritizedFixes: snap.prioritizedFixes
|
|
31913
|
+
};
|
|
31914
|
+
});
|
|
31915
|
+
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
31916
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31917
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31918
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31919
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31920
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31921
|
+
notProbeRun()
|
|
31922
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
31923
|
+
if (!latest) {
|
|
31924
|
+
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
31925
|
+
}
|
|
31926
|
+
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
31927
|
+
const conds = [eq32(siteAuditPages.runId, latest.runId)];
|
|
31928
|
+
if (statusFilter) conds.push(eq32(siteAuditPages.status, statusFilter));
|
|
31929
|
+
const where = and23(...conds);
|
|
31930
|
+
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
31931
|
+
const total = totalRow?.value ?? 0;
|
|
31932
|
+
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
31933
|
+
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
31934
|
+
const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc3(siteAuditPages.url) : asc3(siteAuditPages.overallScore);
|
|
31935
|
+
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
31936
|
+
const pages = rows.map((row) => ({
|
|
31937
|
+
url: row.url,
|
|
31938
|
+
overallScore: row.overallScore,
|
|
31939
|
+
status: row.status === "error" ? "error" : "success",
|
|
31940
|
+
error: row.error,
|
|
31941
|
+
factors: row.factors
|
|
31942
|
+
}));
|
|
31943
|
+
return { project: project.name, runId: latest.runId, auditedAt: latest.auditedAt, total, pages };
|
|
31944
|
+
});
|
|
31945
|
+
app.get("/projects/:name/technical-aeo/trend", async (request) => {
|
|
31946
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31947
|
+
const limit = parsePositiveInt(request.query.limit, 30, 365);
|
|
31948
|
+
const rows = app.db.select({
|
|
31949
|
+
runId: siteAuditSnapshots.runId,
|
|
31950
|
+
auditedAt: siteAuditSnapshots.auditedAt,
|
|
31951
|
+
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
31952
|
+
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
31953
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31954
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31955
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31956
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31957
|
+
notProbeRun()
|
|
31958
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
31959
|
+
return { project: project.name, points: rows.reverse() };
|
|
31960
|
+
});
|
|
31961
|
+
app.post("/projects/:name/technical-aeo/runs", async (request) => {
|
|
31962
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31963
|
+
const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
|
|
31964
|
+
if (!parsed.success) {
|
|
31965
|
+
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
31966
|
+
}
|
|
31967
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and23(
|
|
31968
|
+
eq32(runs.projectId, project.id),
|
|
31969
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31970
|
+
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
31971
|
+
)).get();
|
|
31972
|
+
if (existing) {
|
|
31973
|
+
return { runId: existing.id, status: existing.status };
|
|
31974
|
+
}
|
|
31975
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31976
|
+
const runId = crypto27.randomUUID();
|
|
31977
|
+
app.db.insert(runs).values({
|
|
31978
|
+
id: runId,
|
|
31979
|
+
projectId: project.id,
|
|
31980
|
+
kind: RunKinds["site-audit"],
|
|
31981
|
+
status: RunStatuses.queued,
|
|
31982
|
+
trigger: RunTriggers.manual,
|
|
31983
|
+
createdAt: now
|
|
31984
|
+
}).run();
|
|
31985
|
+
opts.onSiteAuditRequested?.(runId, project.id, {
|
|
31986
|
+
sitemapUrl: parsed.data.sitemapUrl,
|
|
31987
|
+
limit: parsed.data.limit
|
|
31988
|
+
});
|
|
31989
|
+
return { runId, status: RunStatuses.queued };
|
|
31990
|
+
});
|
|
31991
|
+
}
|
|
31992
|
+
|
|
31678
31993
|
// ../api-routes/src/index.ts
|
|
31679
31994
|
async function apiRoutes(app, opts) {
|
|
31680
31995
|
app.decorate("db", opts.db);
|
|
@@ -31801,7 +32116,8 @@ async function apiRoutes(app, opts) {
|
|
|
31801
32116
|
});
|
|
31802
32117
|
await api.register(wordpressRoutes, {
|
|
31803
32118
|
wordpressConnectionStore: opts.wordpressConnectionStore,
|
|
31804
|
-
routePrefix: opts.routePrefix ?? "/api/v1"
|
|
32119
|
+
routePrefix: opts.routePrefix ?? "/api/v1",
|
|
32120
|
+
allowLoopbackWebhooks: opts.allowLoopbackWebhooks
|
|
31805
32121
|
});
|
|
31806
32122
|
await api.register(cdpRoutes, {
|
|
31807
32123
|
getCdpStatus: opts.getCdpStatus,
|
|
@@ -31839,6 +32155,9 @@ async function apiRoutes(app, opts) {
|
|
|
31839
32155
|
await api.register(discoveryRoutes, {
|
|
31840
32156
|
onDiscoveryRunRequested: opts.onDiscoveryRunRequested
|
|
31841
32157
|
});
|
|
32158
|
+
await api.register(technicalAeoRoutes, {
|
|
32159
|
+
onSiteAuditRequested: opts.onSiteAuditRequested
|
|
32160
|
+
});
|
|
31842
32161
|
await api.register(doctorRoutes, {
|
|
31843
32162
|
googleConnectionStore: opts.googleConnectionStore,
|
|
31844
32163
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -31994,7 +32313,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
31994
32313
|
}
|
|
31995
32314
|
|
|
31996
32315
|
// src/intelligence-service.ts
|
|
31997
|
-
import
|
|
32316
|
+
import crypto28 from "crypto";
|
|
31998
32317
|
|
|
31999
32318
|
// src/logger.ts
|
|
32000
32319
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32225,16 +32544,16 @@ var IntelligenceService = class {
|
|
|
32225
32544
|
*/
|
|
32226
32545
|
analyzeAndPersist(runId, projectId) {
|
|
32227
32546
|
const recentRuns = this.db.select().from(runs).where(
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
or5(
|
|
32547
|
+
and24(
|
|
32548
|
+
eq33(runs.projectId, projectId),
|
|
32549
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32231
32550
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32232
32551
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32233
32552
|
// directly for a probe, probes still must not pollute the
|
|
32234
32553
|
// intelligence window.
|
|
32235
32554
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32236
32555
|
)
|
|
32237
|
-
).orderBy(
|
|
32556
|
+
).orderBy(desc17(runs.finishedAt), desc17(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
32238
32557
|
if (recentRuns.length === 0) {
|
|
32239
32558
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
32240
32559
|
return null;
|
|
@@ -32309,7 +32628,7 @@ var IntelligenceService = class {
|
|
|
32309
32628
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32310
32629
|
*/
|
|
32311
32630
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32312
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
32631
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq33(runs.id, runId)).get();
|
|
32313
32632
|
if (!runRow) {
|
|
32314
32633
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32315
32634
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32317,9 +32636,9 @@ var IntelligenceService = class {
|
|
|
32317
32636
|
}
|
|
32318
32637
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32319
32638
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32320
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32321
|
-
|
|
32322
|
-
|
|
32639
|
+
const selected = this.db.select().from(gbpLocations).where(and24(
|
|
32640
|
+
eq33(gbpLocations.projectId, projectId),
|
|
32641
|
+
eq33(gbpLocations.selected, true),
|
|
32323
32642
|
gte6(gbpLocations.syncedAt, windowStart),
|
|
32324
32643
|
lte3(gbpLocations.syncedAt, windowEnd)
|
|
32325
32644
|
)).all();
|
|
@@ -32354,10 +32673,10 @@ var IntelligenceService = class {
|
|
|
32354
32673
|
}
|
|
32355
32674
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32356
32675
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
32357
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
32358
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
32359
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
32360
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
32676
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and24(eq33(gbpDailyMetrics.projectId, projectId), eq33(gbpDailyMetrics.locationName, locationName))).all();
|
|
32677
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and24(eq33(gbpPlaceActions.projectId, projectId), eq33(gbpPlaceActions.locationName, locationName))).all();
|
|
32678
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and24(eq33(gbpLodgingSnapshots.projectId, projectId), eq33(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
32679
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and24(eq33(gbpPlaceDetails.projectId, projectId), eq33(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
32361
32680
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32362
32681
|
const summary = buildGbpSummary({
|
|
32363
32682
|
locationName,
|
|
@@ -32389,7 +32708,7 @@ var IntelligenceService = class {
|
|
|
32389
32708
|
/** Build the month-over-month keyword series for a location from the
|
|
32390
32709
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32391
32710
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32392
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
32711
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and24(eq33(gbpKeywordMonthly.projectId, projectId), eq33(gbpKeywordMonthly.locationName, locationName))).all();
|
|
32393
32712
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32394
32713
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32395
32714
|
const recentMonth = months[0] ?? null;
|
|
@@ -32420,7 +32739,7 @@ var IntelligenceService = class {
|
|
|
32420
32739
|
*/
|
|
32421
32740
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32422
32741
|
const covered = new Set(coveredLocationNames);
|
|
32423
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
32742
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and24(eq33(insights.projectId, projectId), eq33(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
32424
32743
|
const staleIds = [];
|
|
32425
32744
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32426
32745
|
for (const row of existing) {
|
|
@@ -32431,7 +32750,7 @@ var IntelligenceService = class {
|
|
|
32431
32750
|
}
|
|
32432
32751
|
this.db.transaction((tx) => {
|
|
32433
32752
|
for (const id of staleIds) {
|
|
32434
|
-
tx.delete(insights).where(
|
|
32753
|
+
tx.delete(insights).where(eq33(insights.id, id)).run();
|
|
32435
32754
|
}
|
|
32436
32755
|
for (const insight of gbpInsights) {
|
|
32437
32756
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32509,7 +32828,7 @@ var IntelligenceService = class {
|
|
|
32509
32828
|
* create per run + aggregate). DB is left untouched.
|
|
32510
32829
|
*/
|
|
32511
32830
|
backfill(projectName, opts, onProgress) {
|
|
32512
|
-
const project = this.db.select().from(projects).where(
|
|
32831
|
+
const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
|
|
32513
32832
|
if (!project) {
|
|
32514
32833
|
throw new Error(`Project "${projectName}" not found`);
|
|
32515
32834
|
}
|
|
@@ -32522,13 +32841,13 @@ var IntelligenceService = class {
|
|
|
32522
32841
|
sinceTimestamp = parsed;
|
|
32523
32842
|
}
|
|
32524
32843
|
const allRuns = this.db.select().from(runs).where(
|
|
32525
|
-
|
|
32526
|
-
|
|
32527
|
-
or5(
|
|
32844
|
+
and24(
|
|
32845
|
+
eq33(runs.projectId, project.id),
|
|
32846
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32528
32847
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32529
32848
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32530
32849
|
)
|
|
32531
|
-
).orderBy(
|
|
32850
|
+
).orderBy(asc4(runs.finishedAt)).all();
|
|
32532
32851
|
let startIdx = 0;
|
|
32533
32852
|
let endIdx = allRuns.length;
|
|
32534
32853
|
if (opts?.fromRunId) {
|
|
@@ -32557,7 +32876,7 @@ var IntelligenceService = class {
|
|
|
32557
32876
|
let wouldDeleteTotal = 0;
|
|
32558
32877
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
32559
32878
|
if (isDryRun && targetRuns.length > 0) {
|
|
32560
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
32879
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray12(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
32561
32880
|
for (const r of rows) {
|
|
32562
32881
|
if (r.runId == null) continue;
|
|
32563
32882
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -32603,7 +32922,7 @@ var IntelligenceService = class {
|
|
|
32603
32922
|
return { processed, skipped, totalInsights };
|
|
32604
32923
|
}
|
|
32605
32924
|
loadTrackedCompetitors(projectId) {
|
|
32606
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32925
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
32607
32926
|
}
|
|
32608
32927
|
/**
|
|
32609
32928
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -32624,15 +32943,15 @@ var IntelligenceService = class {
|
|
|
32624
32943
|
}
|
|
32625
32944
|
persistResult(result, runId, projectId) {
|
|
32626
32945
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
32627
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
32946
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq33(insights.runId, runId)).all();
|
|
32628
32947
|
for (const row of existingInsights) {
|
|
32629
32948
|
if (row.dismissed) {
|
|
32630
32949
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
32631
32950
|
}
|
|
32632
32951
|
}
|
|
32633
32952
|
this.db.transaction((tx) => {
|
|
32634
|
-
tx.delete(insights).where(
|
|
32635
|
-
tx.delete(healthSnapshots).where(
|
|
32953
|
+
tx.delete(insights).where(eq33(insights.runId, runId)).run();
|
|
32954
|
+
tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
|
|
32636
32955
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32637
32956
|
for (const insight of result.insights) {
|
|
32638
32957
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -32652,7 +32971,7 @@ var IntelligenceService = class {
|
|
|
32652
32971
|
}).run();
|
|
32653
32972
|
}
|
|
32654
32973
|
tx.insert(healthSnapshots).values({
|
|
32655
|
-
id:
|
|
32974
|
+
id: crypto28.randomUUID(),
|
|
32656
32975
|
projectId,
|
|
32657
32976
|
runId,
|
|
32658
32977
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -32683,28 +33002,28 @@ var IntelligenceService = class {
|
|
|
32683
33002
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
32684
33003
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
32685
33004
|
if (regressions.length === 0) return rawInsights;
|
|
32686
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
33005
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq33(gscSearchData.projectId, projectId)).all();
|
|
32687
33006
|
const gscConnected = gscRows.length > 0;
|
|
32688
33007
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
32689
33008
|
for (const row of gscRows) {
|
|
32690
33009
|
const key = row.query.toLowerCase();
|
|
32691
33010
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
32692
33011
|
}
|
|
32693
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
33012
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
32694
33013
|
const locationCount = Math.max(
|
|
32695
33014
|
1,
|
|
32696
33015
|
(projectRow?.locations ?? []).length
|
|
32697
33016
|
);
|
|
32698
33017
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
32699
33018
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
32700
|
-
|
|
32701
|
-
|
|
32702
|
-
|
|
32703
|
-
or5(
|
|
33019
|
+
and24(
|
|
33020
|
+
eq33(runs.projectId, projectId),
|
|
33021
|
+
eq33(runs.kind, RunKinds["answer-visibility"]),
|
|
33022
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32704
33023
|
// Defensive — see top of file.
|
|
32705
33024
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32706
33025
|
)
|
|
32707
|
-
).orderBy(
|
|
33026
|
+
).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
32708
33027
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
32709
33028
|
const recentRunIds = [];
|
|
32710
33029
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -32720,7 +33039,7 @@ var IntelligenceService = class {
|
|
|
32720
33039
|
const haveHistory = recentRunIds.length > 0;
|
|
32721
33040
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
32722
33041
|
if (haveHistory) {
|
|
32723
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
33042
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and24(eq33(insights.type, "regression"), inArray12(insights.runId, recentRunIds))).all();
|
|
32724
33043
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
32725
33044
|
for (const row of priorRows) {
|
|
32726
33045
|
if (!row.runId) continue;
|
|
@@ -32749,7 +33068,7 @@ var IntelligenceService = class {
|
|
|
32749
33068
|
});
|
|
32750
33069
|
}
|
|
32751
33070
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
32752
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
33071
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
32753
33072
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
32754
33073
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
32755
33074
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -32765,7 +33084,7 @@ var IntelligenceService = class {
|
|
|
32765
33084
|
citedDomains: querySnapshots.citedDomains,
|
|
32766
33085
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
32767
33086
|
snapshotLocation: querySnapshots.location
|
|
32768
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
33087
|
+
}).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
|
|
32769
33088
|
const snapshots = [];
|
|
32770
33089
|
let orphanCount = 0;
|
|
32771
33090
|
for (const r of rows) {
|
|
@@ -32816,6 +33135,8 @@ export {
|
|
|
32816
33135
|
gscSearchData,
|
|
32817
33136
|
gscUrlInspections,
|
|
32818
33137
|
gscCoverageSnapshots,
|
|
33138
|
+
siteAuditSnapshots,
|
|
33139
|
+
siteAuditPages,
|
|
32819
33140
|
bingCoverageSnapshots,
|
|
32820
33141
|
bingUrlInspections,
|
|
32821
33142
|
gaTrafficSnapshots,
|