@canonry/canonry 4.139.0 → 4.141.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.
Files changed (31) hide show
  1. package/assets/agent-workspace/skills/aero/references/reporting.md +22 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +21 -0
  3. package/assets/agent-workspace/skills/canonry/references/indexing.md +2 -1
  4. package/assets/assets/{AuditHistoryPanel-DL9chF3F.js → AuditHistoryPanel-DlyLhzqq.js} +1 -1
  5. package/assets/assets/{BacklinksPage-sA_tgQJv.js → BacklinksPage-DNxtwQBF.js} +1 -1
  6. package/assets/assets/{ChartPrimitives-D7nLxIbe.js → ChartPrimitives-BStLR_NC.js} +1 -1
  7. package/assets/assets/{HistoryPage-CLWbcu5m.js → HistoryPage-CIcCgoWK.js} +1 -1
  8. package/assets/assets/{ProjectPage-C_oXMCNU.js → ProjectPage-BSI3Qum9.js} +8 -8
  9. package/assets/assets/{RunRow-DQ4icEiv.js → RunRow-C0mFtLlc.js} +1 -1
  10. package/assets/assets/{RunsPage-D5sizm9T.js → RunsPage-C-yo0yoj.js} +1 -1
  11. package/assets/assets/{SettingsPage-XdQjvWsg.js → SettingsPage-jg4MXOuo.js} +1 -1
  12. package/assets/assets/{TrafficPage-C12osXDa.js → TrafficPage-CnYHIg8t.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-CXAiNK8Y.js → TrafficSourceDetailPage-CEdyQT2P.js} +1 -1
  14. package/assets/assets/{arrow-left-LaI5a16g.js → arrow-left-DUv0imIl.js} +1 -1
  15. package/assets/assets/{extract-error-message-CYWaTM3m.js → extract-error-message-CCOGDwlm.js} +1 -1
  16. package/assets/assets/index-BDo9nl4O.css +1 -0
  17. package/assets/assets/index-D6EP4dQs.js +213 -0
  18. package/assets/assets/{trash-2-DoH00q7m.js → trash-2-1Yg9W-vs.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-IGU67QFM.js → chunk-2LUSZFB6.js} +122 -19
  21. package/dist/{chunk-DLKUDNDA.js → chunk-HXXO2AF5.js} +51 -4
  22. package/dist/{chunk-OA2EGCUS.js → chunk-OJOA425V.js} +42 -1
  23. package/dist/{chunk-IYTHIAAE.js → chunk-ZCFQGF3V.js} +46 -4
  24. package/dist/cli.js +145 -17
  25. package/dist/index.d.ts +12 -0
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-SMYVN3KN.js → intelligence-service-4HQIUC7U.js} +2 -2
  28. package/dist/mcp.js +2 -2
  29. package/package.json +8 -8
  30. package/assets/assets/index-CguwySA5.js +0 -213
  31. package/assets/assets/index-CntuM6Th.css +0 -1
@@ -1 +1 @@
1
- import{c as a}from"./index-CguwySA5.js";const t=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],h=a("circle-question-mark",t);const e=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],d=a("download",e);const o=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],p=a("external-link",o);const c=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],s=a("trash-2",c);export{h as C,d as D,p as E,s as T};
1
+ import{c as a}from"./index-D6EP4dQs.js";const t=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],h=a("circle-question-mark",t);const e=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],d=a("download",e);const o=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],p=a("external-link",o);const c=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],s=a("trash-2",c);export{h as C,d as D,p as E,s as T};
package/assets/index.html CHANGED
@@ -12,12 +12,12 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-CguwySA5.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-D6EP4dQs.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-DSxTZhaH.js">
19
19
  <link rel="modulepreload" crossorigin href="./assets/vendor-markdown-6dwjPOKK.js">
20
- <link rel="stylesheet" crossorigin href="./assets/index-CntuM6Th.css">
20
+ <link rel="stylesheet" crossorigin href="./assets/index-BDo9nl4O.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -240,11 +240,13 @@ import {
240
240
  gscDeindexedRowSchema,
241
241
  gscDiscoverSitemapsResponseDtoSchema,
242
242
  gscPerformanceDailyDtoSchema,
243
- gscSearchDataDtoSchema,
243
+ gscPerformanceOrderBySchema,
244
+ gscPerformanceResponseDtoSchema,
244
245
  gscSiteListResponseDtoSchema,
245
246
  gscSitemapListResponseDtoSchema,
246
247
  gscSubmitSitemapsRequestDtoSchema,
247
248
  gscSubmitSitemapsResponseDtoSchema,
249
+ gscTopPagesDtoSchema,
248
250
  gscUrlInspectionDtoSchema,
249
251
  hasLocationLabel,
250
252
  healthSnapshotDtoSchema,
@@ -378,7 +380,7 @@ import {
378
380
  wordpressSchemaDeployResultDtoSchema,
379
381
  wordpressSchemaStatusResultDtoSchema,
380
382
  wordpressStatusDtoSchema
381
- } from "./chunk-OA2EGCUS.js";
383
+ } from "./chunk-OJOA425V.js";
382
384
 
383
385
  // src/intelligence-service.ts
384
386
  import { eq as eq45, desc as desc22, asc as asc9, and as and36, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
@@ -17766,7 +17768,8 @@ var SCHEMA_TABLE = {
17766
17768
  GscCoverageSummaryDto: gscCoverageSummaryDtoSchema,
17767
17769
  GscDeindexedRowDto: gscDeindexedRowSchema,
17768
17770
  GscPerformanceDailyDto: gscPerformanceDailyDtoSchema,
17769
- GscSearchDataDto: gscSearchDataDtoSchema,
17771
+ GscPerformanceResponseDto: gscPerformanceResponseDtoSchema,
17772
+ GscTopPagesDto: gscTopPagesDtoSchema,
17770
17773
  GscDiscoverSitemapsResponseDto: gscDiscoverSitemapsResponseDtoSchema,
17771
17774
  GscSiteListResponseDto: gscSiteListResponseDtoSchema,
17772
17775
  GscSubmitSitemapsResponseDto: gscSubmitSitemapsResponseDtoSchema,
@@ -19717,15 +19720,22 @@ var routeCatalog = [
19717
19720
  { name: "endDate", in: "query", description: "Filter by end date.", schema: stringSchema },
19718
19721
  { name: "query", in: "query", description: "Filter by search query.", schema: stringSchema },
19719
19722
  { name: "page", in: "query", description: "Filter by page URL.", schema: stringSchema },
19723
+ {
19724
+ name: "orderBy",
19725
+ in: "query",
19726
+ description: "Row ordering, always descending. Defaults to clicks. Use date for time-series reads.",
19727
+ schema: { type: "string", enum: ["clicks", "impressions", "date"] }
19728
+ },
19720
19729
  limitQueryParameter,
19721
19730
  offsetQueryParameter,
19722
19731
  analyticsWindowParameter
19723
19732
  ],
19724
19733
  responses: {
19725
- // Handler returns an array of GscSearchDataDto rows (web's
19726
- // ApiGscPerformanceRow[] confirms). Was incorrectly spec'd as a
19727
- // single object, which silently truncated client types to one row.
19728
- 200: jsonArrayResponse("GSC performance rows returned.", "GscSearchDataDto"),
19734
+ // Envelope, not a bare array: `totalMatching` / `truncated` are how a
19735
+ // caller tells a page from a complete answer, and `latestAvailableDate`
19736
+ // is how it tells "no data" from "asked past the GSC reporting lag".
19737
+ 200: jsonResponse("GSC performance page plus match count and data freshness.", "GscPerformanceResponseDto"),
19738
+ 400: errorResponse("Invalid orderBy value."),
19729
19739
  404: errorResponse("Project not found.")
19730
19740
  }
19731
19741
  },
@@ -19745,6 +19755,24 @@ var routeCatalog = [
19745
19755
  404: errorResponse("Project not found.")
19746
19756
  }
19747
19757
  },
19758
+ {
19759
+ method: "get",
19760
+ path: "/api/v1/projects/{name}/google/gsc/top-pages",
19761
+ summary: "Get top GSC pages ranked by clicks",
19762
+ description: "One row per page, aggregated in SQL and ranked by summed clicks descending. The rows are a RANKING built from the dimensioned search-data table; `totals` is NOT their sum. Google withholds rare queries (summed clicks under-count) and fans one impression across every query/page/country/device combination (summed impressions over-count), so `totals` is read from the un-dimensioned property-level daily table and labelled `totalsSource: property-daily`. It is null when that table has no rows in the window.",
19763
+ tags: ["google"],
19764
+ parameters: [
19765
+ nameParameter,
19766
+ { name: "startDate", in: "query", description: "Filter by start date.", schema: stringSchema },
19767
+ { name: "endDate", in: "query", description: "Filter by end date.", schema: stringSchema },
19768
+ limitQueryParameter,
19769
+ analyticsWindowParameter
19770
+ ],
19771
+ responses: {
19772
+ 200: jsonResponse("Ranked pages plus the property-level window total.", "GscTopPagesDto"),
19773
+ 404: errorResponse("Project not found.")
19774
+ }
19775
+ },
19748
19776
  {
19749
19777
  method: "post",
19750
19778
  path: "/api/v1/projects/{name}/google/gsc/inspect",
@@ -25773,6 +25801,13 @@ async function googleRoutes(app, opts) {
25773
25801
  app.get("/projects/:name/google/gsc/performance", async (request) => {
25774
25802
  const project = resolveProject(app.db, request.params.name);
25775
25803
  const { startDate, endDate, query, page, limit, offset } = request.query;
25804
+ const parsedOrderBy = gscPerformanceOrderBySchema.safeParse(request.query.orderBy ?? "clicks");
25805
+ if (!parsedOrderBy.success) {
25806
+ throw validationError(
25807
+ `orderBy must be one of: ${gscPerformanceOrderBySchema.options.join(", ")}`
25808
+ );
25809
+ }
25810
+ const orderBy = parsedOrderBy.data;
25776
25811
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
25777
25812
  const conditions = [eq27(gscSearchData.projectId, project.id)];
25778
25813
  if (startDate) conditions.push(sql12`${gscSearchData.date} >= ${startDate}`);
@@ -25782,18 +25817,34 @@ async function googleRoutes(app, opts) {
25782
25817
  if (page) conditions.push(sql12`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
25783
25818
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
25784
25819
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
25785
- const rows = app.db.select().from(gscSearchData).where(and21(...conditions)).orderBy(desc13(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
25786
- return rows.map((r) => ({
25787
- date: r.date,
25788
- query: r.query,
25789
- page: r.page,
25790
- country: r.country,
25791
- device: r.device,
25792
- clicks: r.clicks,
25793
- impressions: r.impressions,
25794
- ctr: parseFloat(r.ctr),
25795
- position: parseFloat(r.position)
25796
- }));
25820
+ const orderColumns = {
25821
+ clicks: [desc13(gscSearchData.clicks), desc13(gscSearchData.date), gscSearchData.query],
25822
+ impressions: [desc13(gscSearchData.impressions), desc13(gscSearchData.date), gscSearchData.query],
25823
+ date: [desc13(gscSearchData.date), desc13(gscSearchData.clicks), gscSearchData.query]
25824
+ }[orderBy];
25825
+ const rows = app.db.select().from(gscSearchData).where(and21(...conditions)).orderBy(...orderColumns).limit(limitVal).offset(offsetVal).all();
25826
+ const totalMatching = app.db.select({ total: sql12`COUNT(*)` }).from(gscSearchData).where(and21(...conditions)).get()?.total ?? 0;
25827
+ const latestAvailableDate = app.db.select({ latest: sql12`MAX(${gscSearchData.date})` }).from(gscSearchData).where(eq27(gscSearchData.projectId, project.id)).get()?.latest ?? null;
25828
+ return {
25829
+ rows: rows.map((r) => ({
25830
+ date: r.date,
25831
+ query: r.query,
25832
+ page: r.page,
25833
+ country: r.country,
25834
+ device: r.device,
25835
+ clicks: r.clicks,
25836
+ impressions: r.impressions,
25837
+ ctr: parseFloat(r.ctr),
25838
+ position: parseFloat(r.position)
25839
+ })),
25840
+ totalMatching,
25841
+ // Account for the offset: `rows.length < totalMatching` alone reports
25842
+ // truncated=true for a page that sits past the end, where there is
25843
+ // nothing further to fetch. Truncation means "more rows follow this
25844
+ // page", so it has to measure from where this page ends.
25845
+ truncated: offsetVal + rows.length < totalMatching,
25846
+ latestAvailableDate
25847
+ };
25797
25848
  });
25798
25849
  app.get("/projects/:name/google/gsc/performance/daily", async (request) => {
25799
25850
  const project = resolveProject(app.db, request.params.name);
@@ -25837,6 +25888,56 @@ async function googleRoutes(app, opts) {
25837
25888
  daily
25838
25889
  };
25839
25890
  });
25891
+ app.get("/projects/:name/google/gsc/top-pages", async (request) => {
25892
+ const project = resolveProject(app.db, request.params.name);
25893
+ const { startDate, endDate, limit } = request.query;
25894
+ const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
25895
+ const conditions = [eq27(gscSearchData.projectId, project.id)];
25896
+ if (startDate) conditions.push(sql12`${gscSearchData.date} >= ${startDate}`);
25897
+ else if (cutoffDate) conditions.push(sql12`${gscSearchData.date} >= ${cutoffDate}`);
25898
+ if (endDate) conditions.push(sql12`${gscSearchData.date} <= ${endDate}`);
25899
+ const limitVal = Math.max(parseInt(limit ?? "50", 10) || 0, 1);
25900
+ const rows = app.db.select({
25901
+ page: gscSearchData.page,
25902
+ clicks: sql12`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
25903
+ impressions: sql12`COALESCE(SUM(${gscSearchData.impressions}), 0)`
25904
+ }).from(gscSearchData).where(and21(...conditions)).groupBy(gscSearchData.page).orderBy(desc13(sql12`SUM(${gscSearchData.clicks})`), desc13(sql12`SUM(${gscSearchData.impressions})`)).limit(limitVal).all();
25905
+ const windowStart = startDate ?? cutoffDate ?? "";
25906
+ const windowEnd = endDate ?? "9999-12-31";
25907
+ const dailyTotals = readGscDailyTotals(app.db, project.id, windowStart, windowEnd);
25908
+ const totalClicks = dailyTotals.reduce((sum, d) => sum + d.clicks, 0);
25909
+ const totalImpressions = dailyTotals.reduce((sum, d) => sum + d.impressions, 0);
25910
+ const rankedSpan = app.db.select({
25911
+ first: sql12`MIN(${gscSearchData.date})`,
25912
+ last: sql12`MAX(${gscSearchData.date})`
25913
+ }).from(gscSearchData).where(and21(...conditions)).get();
25914
+ const coveredFrom = dailyTotals.length > 0 ? dailyTotals[0].date : null;
25915
+ const coveredThrough = dailyTotals.length > 0 ? dailyTotals[dailyTotals.length - 1].date : null;
25916
+ const totalsComplete = Boolean(
25917
+ coveredFrom && coveredThrough && rankedSpan?.first && rankedSpan?.last && coveredFrom <= rankedSpan.first && coveredThrough >= rankedSpan.last
25918
+ );
25919
+ return {
25920
+ rows: rows.map((r) => ({
25921
+ page: r.page,
25922
+ clicks: r.clicks,
25923
+ impressions: r.impressions,
25924
+ ctr: r.impressions > 0 ? r.clicks / r.impressions : 0
25925
+ })),
25926
+ totals: dailyTotals.length > 0 ? {
25927
+ clicks: totalClicks,
25928
+ impressions: totalImpressions,
25929
+ ctr: totalImpressions > 0 ? totalClicks / totalImpressions : 0,
25930
+ days: dailyTotals.length,
25931
+ coveredFrom,
25932
+ coveredThrough,
25933
+ // False when the property-level totals span less than the rows above.
25934
+ complete: totalsComplete
25935
+ } : null,
25936
+ totalsSource: "property-daily",
25937
+ rankedFrom: rankedSpan?.first ?? null,
25938
+ rankedThrough: rankedSpan?.last ?? null
25939
+ };
25940
+ });
25840
25941
  app.post("/projects/:name/google/gsc/inspect", async (request) => {
25841
25942
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
25842
25943
  if (!googleClientId || !googleClientSecret) {
@@ -45946,6 +46047,8 @@ export {
45946
46047
  extractRecommendedCompetitors,
45947
46048
  getPlaceDetails,
45948
46049
  hashPlaceDetails,
46050
+ latestQueryBasketRevision,
46051
+ ensureCurrentQueryBasketRevision,
45949
46052
  queueRunIfProjectIdle,
45950
46053
  nextRunFromCron,
45951
46054
  resolveWebhookTarget,
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-IYTHIAAE.js";
13
+ } from "./chunk-ZCFQGF3V.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -53,6 +53,7 @@ import {
53
53
  doctorHealthState,
54
54
  downloadFile,
55
55
  dropLegacyCredentialColumns,
56
+ ensureCurrentQueryBasketRevision,
56
57
  executeDiscovery,
57
58
  extractLegacyCredentials,
58
59
  extractRecommendedCompetitors,
@@ -88,6 +89,7 @@ import {
88
89
  installDuckdb,
89
90
  isDuckdbInstalled,
90
91
  isValidReleaseId,
92
+ latestQueryBasketRevision,
91
93
  listCachedReleases,
92
94
  listLocations,
93
95
  listMonthlyKeywords,
@@ -121,7 +123,7 @@ import {
121
123
  siteAuditSnapshots,
122
124
  toAlertView,
123
125
  usageCounters
124
- } from "./chunk-IGU67QFM.js";
126
+ } from "./chunk-2LUSZFB6.js";
125
127
  import {
126
128
  AGENT_MEMORY_VALUE_MAX_BYTES,
127
129
  AGENT_PROVIDER_IDS,
@@ -208,7 +210,7 @@ import {
208
210
  validationError,
209
211
  winnabilityClassLabel,
210
212
  withRetry
211
- } from "./chunk-OA2EGCUS.js";
213
+ } from "./chunk-OJOA425V.js";
212
214
 
213
215
  // src/telemetry.ts
214
216
  import crypto from "crypto";
@@ -7176,7 +7178,7 @@ function readStoredGroundingSources(rawResponse) {
7176
7178
  return result;
7177
7179
  }
7178
7180
  async function backfillInsightsCommand(project, opts) {
7179
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SMYVN3KN.js");
7181
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-4HQIUC7U.js");
7180
7182
  const config = loadConfig();
7181
7183
  const db = createClient(config.database);
7182
7184
  migrate(db);
@@ -8151,9 +8153,36 @@ var Scheduler = class {
8151
8153
  }
8152
8154
  log13.info("health-schedule.seeded", { projectCount: projectsWithoutHealth.length, cron: DEFAULT_HEALTH_CRON });
8153
8155
  }
8156
+ /**
8157
+ * Record each project's current query set as basket revision 1 if it has
8158
+ * never been recorded. Minting normally happens when a sweep is queued, but a
8159
+ * project whose sweeps are manual (the common cadence: twice a month) would
8160
+ * otherwise keep its analytics on the pre-basket date heuristic for weeks
8161
+ * after the feature ships — with the chart still hiding exactly the history
8162
+ * the basket exists to recover. Boot is the moment the current set is known
8163
+ * and nothing has to be guessed, and ensureCurrentQueryBasketRevision is
8164
+ * idempotent, so restarts are no-ops and an unchanged set never churns
8165
+ * revisions. Runs are NOT stamped retroactively: the revision describes the
8166
+ * set as of now, and historical runs keep their null stamp.
8167
+ */
8168
+ ensureQueryBaskets() {
8169
+ const allProjects = this.db.select({ id: projects.id }).from(projects).all();
8170
+ let minted = 0;
8171
+ for (const project of allProjects) {
8172
+ try {
8173
+ const previous = latestQueryBasketRevision(this.db, project.id)?.revision ?? null;
8174
+ const current = ensureCurrentQueryBasketRevision(this.db, project.id);
8175
+ if (current !== null && current.revision !== previous) minted += 1;
8176
+ } catch (err) {
8177
+ log13.warn("query-basket.mint-failed", { projectId: project.id, err: String(err) });
8178
+ }
8179
+ }
8180
+ if (minted > 0) log13.info("query-basket.ensured", { projectCount: minted });
8181
+ }
8154
8182
  /** Load all enabled schedules from DB and register cron jobs. */
8155
8183
  start() {
8156
8184
  this.ensureHealthSchedules();
8185
+ this.ensureQueryBaskets();
8157
8186
  const allSchedules = this.db.select().from(schedules).where(eq14(schedules.enabled, true)).all();
8158
8187
  for (const schedule of allSchedules) {
8159
8188
  const missedRunAt = schedule.nextRunAt;
@@ -11979,6 +12008,12 @@ function parseBooleanEnv(value) {
11979
12008
  function resolveDashboardRequirePassword(env, config) {
11980
12009
  return parseBooleanEnv(env.CANONRY_DASHBOARD_REQUIRE_PASSWORD) ?? config.dashboard?.requirePassword ?? true;
11981
12010
  }
12011
+ function resolveDashboardShowResourceLinks(env, config) {
12012
+ return parseBooleanEnv(env.CANONRY_DASHBOARD_SHOW_RESOURCE_LINKS) ?? config.dashboard?.showResourceLinks ?? true;
12013
+ }
12014
+ function resolveDashboardShowUpdateNotification(env, config) {
12015
+ return parseBooleanEnv(env.CANONRY_DASHBOARD_SHOW_UPDATE_NOTIFICATION) ?? config.dashboard?.showUpdateNotification ?? true;
12016
+ }
11982
12017
  var API_ADAPTERS = [
11983
12018
  geminiAdapter,
11984
12019
  openaiAdapter,
@@ -12843,6 +12878,8 @@ async function createServer(opts) {
12843
12878
  );
12844
12879
  }
12845
12880
  const dashboardRequirePassword = resolveDashboardRequirePassword(process.env, opts.config);
12881
+ const dashboardShowResourceLinks = resolveDashboardShowResourceLinks(process.env, opts.config);
12882
+ const dashboardShowUpdateNotification = resolveDashboardShowUpdateNotification(process.env, opts.config);
12846
12883
  app.log.info(
12847
12884
  { dashboardRequirePassword },
12848
12885
  "Dashboard password gate resolved"
@@ -13592,6 +13629,16 @@ async function createServer(opts) {
13592
13629
  const injectConfig = (html, projectTabsOverride, themeOverride, renderTokenOverride) => {
13593
13630
  const clientConfig = {};
13594
13631
  if (basePath) clientConfig.basePath = basePath;
13632
+ const dashboardConfig = {};
13633
+ if (!dashboardShowResourceLinks) {
13634
+ dashboardConfig.showResourceLinks = false;
13635
+ }
13636
+ if (!dashboardShowUpdateNotification) {
13637
+ dashboardConfig.showUpdateNotification = false;
13638
+ }
13639
+ if (Object.keys(dashboardConfig).length > 0) {
13640
+ clientConfig.dashboard = dashboardConfig;
13641
+ }
13595
13642
  if (embed.enabled) {
13596
13643
  const embedClient = embedClientConfigForRequest(embed, projectTabsOverride, themeOverride, renderTokenOverride);
13597
13644
  if (embedClient) clientConfig.embed = embedClient;
@@ -4408,6 +4408,13 @@ var gscSearchDataDtoSchema = z11.object({
4408
4408
  ctr: z11.number(),
4409
4409
  position: z11.number()
4410
4410
  });
4411
+ var gscPerformanceOrderBySchema = z11.enum(["clicks", "impressions", "date"]);
4412
+ var gscPerformanceResponseDtoSchema = z11.object({
4413
+ rows: z11.array(gscSearchDataDtoSchema),
4414
+ totalMatching: z11.number(),
4415
+ truncated: z11.boolean(),
4416
+ latestAvailableDate: z11.string().nullable()
4417
+ });
4411
4418
  var gscPerformanceDailyPointSchema = z11.object({
4412
4419
  date: z11.string(),
4413
4420
  clicks: z11.number(),
@@ -4423,6 +4430,38 @@ var gscPerformanceDailyDtoSchema = z11.object({
4423
4430
  }),
4424
4431
  daily: z11.array(gscPerformanceDailyPointSchema)
4425
4432
  });
4433
+ var gscTopPageRowSchema = z11.object({
4434
+ page: z11.string(),
4435
+ clicks: z11.number(),
4436
+ impressions: z11.number(),
4437
+ ctr: z11.number()
4438
+ });
4439
+ var gscTopPagesDtoSchema = z11.object({
4440
+ rows: z11.array(gscTopPageRowSchema),
4441
+ totals: z11.object({
4442
+ clicks: z11.number(),
4443
+ impressions: z11.number(),
4444
+ ctr: z11.number(),
4445
+ days: z11.number(),
4446
+ /** First date the property-level totals actually cover. */
4447
+ coveredFrom: z11.string().nullable(),
4448
+ /** Last date the property-level totals actually cover. */
4449
+ coveredThrough: z11.string().nullable(),
4450
+ /**
4451
+ * False when the totals span less than the ranked rows above.
4452
+ *
4453
+ * The dimensioned and property-level tables sync independently, so a
4454
+ * 30-day totals sync can sit next to months of dimensioned rows. Printing
4455
+ * both as one period would misstate the window the totals belong to.
4456
+ */
4457
+ complete: z11.boolean()
4458
+ }).nullable(),
4459
+ totalsSource: z11.literal("property-daily"),
4460
+ /** First date the ranked rows cover, so a caller can compare spans. */
4461
+ rankedFrom: z11.string().nullable(),
4462
+ /** Last date the ranked rows cover. */
4463
+ rankedThrough: z11.string().nullable()
4464
+ });
4426
4465
  var gscUrlInspectionDtoSchema = z11.object({
4427
4466
  id: z11.string(),
4428
4467
  url: z11.string(),
@@ -10786,8 +10825,10 @@ export {
10786
10825
  evaluateModelPointerExposure,
10787
10826
  buildModelChangeNotice,
10788
10827
  googleConnectionDtoSchema,
10789
- gscSearchDataDtoSchema,
10828
+ gscPerformanceOrderBySchema,
10829
+ gscPerformanceResponseDtoSchema,
10790
10830
  gscPerformanceDailyDtoSchema,
10831
+ gscTopPagesDtoSchema,
10791
10832
  gscUrlInspectionDtoSchema,
10792
10833
  gscDeindexedRowSchema,
10793
10834
  gscCoverageSummaryDtoSchema,
@@ -23,6 +23,7 @@ import {
23
23
  discoveryRunRequestSchema,
24
24
  gaMeasurementAnalysisWindowSchema,
25
25
  gaMeasurementHostScopeSchema,
26
+ gscPerformanceOrderBySchema,
26
27
  keywordBatchRequestSchema,
27
28
  keywordGenerateRequestSchema,
28
29
  notificationCreateRequestSchema,
@@ -42,7 +43,7 @@ import {
42
43
  trafficConnectWordpressRequestSchema,
43
44
  trafficEventKindSchema,
44
45
  trafficSeriesGranularitySchema
45
- } from "./chunk-OA2EGCUS.js";
46
+ } from "./chunk-OJOA425V.js";
46
47
 
47
48
  // src/config.ts
48
49
  import fs from "fs";
@@ -2067,6 +2068,18 @@ var getApiV1ProjectsByNameGoogleGscPerformanceDaily = (options) => {
2067
2068
  ...options
2068
2069
  });
2069
2070
  };
2071
+ var getApiV1ProjectsByNameGoogleGscTopPages = (options) => {
2072
+ return (options.client ?? client).get({
2073
+ security: [
2074
+ {
2075
+ scheme: "bearer",
2076
+ type: "http"
2077
+ }
2078
+ ],
2079
+ url: "/api/v1/projects/{name}/google/gsc/top-pages",
2080
+ ...options
2081
+ });
2082
+ };
2070
2083
  var postApiV1ProjectsByNameGoogleGscInspect = (options) => {
2071
2084
  return (options.client ?? client).post({
2072
2085
  security: [
@@ -5139,6 +5152,15 @@ var ApiClient = class {
5139
5152
  })
5140
5153
  );
5141
5154
  }
5155
+ async gscTopPages(project, params) {
5156
+ return this.invoke(
5157
+ () => getApiV1ProjectsByNameGoogleGscTopPages({
5158
+ client: this.heyClient,
5159
+ path: { name: project },
5160
+ query: params
5161
+ })
5162
+ );
5163
+ }
5142
5164
  async gscInspect(project, url) {
5143
5165
  return this.invoke(
5144
5166
  () => postApiV1ProjectsByNameGoogleGscInspect({
@@ -6140,7 +6162,9 @@ var gscPerformanceInputSchema = z2.object({
6140
6162
  endDate: z2.string().optional(),
6141
6163
  query: z2.string().optional(),
6142
6164
  page: z2.string().optional(),
6143
- limit: z2.number().int().positive().max(500).optional(),
6165
+ limit: z2.number().int().positive().max(5e3).optional(),
6166
+ offset: z2.number().int().nonnegative().optional(),
6167
+ orderBy: gscPerformanceOrderBySchema.optional(),
6144
6168
  window: analyticsWindowSchema.optional()
6145
6169
  });
6146
6170
  var gscPerformanceDailyInputSchema = z2.object({
@@ -6149,6 +6173,13 @@ var gscPerformanceDailyInputSchema = z2.object({
6149
6173
  endDate: z2.string().optional(),
6150
6174
  window: analyticsWindowSchema.optional()
6151
6175
  });
6176
+ var gscTopPagesInputSchema = z2.object({
6177
+ project: projectNameSchema,
6178
+ startDate: z2.string().optional(),
6179
+ endDate: z2.string().optional(),
6180
+ limit: z2.number().int().positive().max(500).optional(),
6181
+ window: analyticsWindowSchema.optional()
6182
+ });
6152
6183
  var gscInspectionsInputSchema = z2.object({
6153
6184
  project: projectNameSchema,
6154
6185
  url: z2.string().optional(),
@@ -7074,13 +7105,13 @@ var canonryMcpTools = [
7074
7105
  defineTool({
7075
7106
  name: "canonry_gsc_performance",
7076
7107
  title: "Get GSC performance",
7077
- description: "Get stored Google Search Console performance rows for a Canonry project.",
7108
+ description: "Get stored Google Search Console performance rows for a Canonry project. Rows are ordered by clicks descending unless orderBy says otherwise, and the response reports totalMatching / truncated / latestAvailableDate. Never sum these rows for a property total, use canonry_gsc_performance_daily.",
7078
7109
  access: "read",
7079
7110
  tier: "gsc",
7080
7111
  inputSchema: gscPerformanceInputSchema,
7081
7112
  annotations: readAnnotations(),
7082
7113
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance"],
7083
- handler: (client2, input) => client2.gscPerformance(input.project, compactStringParams(input, ["startDate", "endDate", "query", "page", "limit", "window"]))
7114
+ handler: (client2, input) => client2.gscPerformance(input.project, compactStringParams(input, ["startDate", "endDate", "query", "page", "limit", "offset", "orderBy", "window"]))
7084
7115
  }),
7085
7116
  defineTool({
7086
7117
  name: "canonry_gsc_performance_daily",
@@ -7093,6 +7124,17 @@ var canonryMcpTools = [
7093
7124
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/performance/daily"],
7094
7125
  handler: (client2, input) => client2.gscPerformanceDaily(input.project, compactStringParams(input, ["startDate", "endDate", "window"]))
7095
7126
  }),
7127
+ defineTool({
7128
+ name: "canonry_gsc_top_pages",
7129
+ title: "Get top GSC pages",
7130
+ description: 'Get the project\'s pages ranked by summed GSC clicks, aggregated in SQL. The rows are a RANKING and their clicks/impressions do NOT add up to the site total: Google withholds rare queries and repeats one impression per ranking page. Read the window total from the returned `totals` block (labelled totalsSource "property-daily"), which is null when no property-level figure covers the window. Never sum the rows.',
7131
+ access: "read",
7132
+ tier: "gsc",
7133
+ inputSchema: gscTopPagesInputSchema,
7134
+ annotations: readAnnotations(),
7135
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/top-pages"],
7136
+ handler: (client2, input) => client2.gscTopPages(input.project, compactStringParams(input, ["startDate", "endDate", "limit", "window"]))
7137
+ }),
7096
7138
  defineTool({
7097
7139
  name: "canonry_gsc_inspections",
7098
7140
  title: "List GSC inspections",