@ainyc/canonry 4.60.2 → 4.61.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 (26) hide show
  1. package/assets/agent-workspace/skills/aero/references/regression-playbook.md +2 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -0
  3. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +23 -2
  4. package/assets/assets/{BacklinksPage-Dj4AVTma.js → BacklinksPage-CsGOAPNN.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-7SFwUlCh.js → ChartPrimitives-Bjow7aaC.js} +1 -1
  6. package/assets/assets/{ProjectPage-4tWuU1ZR.js → ProjectPage-BZoMD93_.js} +1 -1
  7. package/assets/assets/{RunRow-CgPJfmWX.js → RunRow-ve7H_XIu.js} +1 -1
  8. package/assets/assets/{RunsPage-Cdo4jBn4.js → RunsPage-mYmYevh0.js} +1 -1
  9. package/assets/assets/{SettingsPage-N5iccPoU.js → SettingsPage-DoRiIJK5.js} +1 -1
  10. package/assets/assets/{TrafficPage-pSlQPdDg.js → TrafficPage-1LFyX4OT.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BQx3Evf6.js → TrafficSourceDetailPage-BYDJtQdO.js} +1 -1
  12. package/assets/assets/{extract-error-message-BOembgFV.js → extract-error-message-Bt6jcL_M.js} +1 -1
  13. package/assets/assets/{index-DjKFsFsl.js → index-BQxaYi-t.js} +70 -70
  14. package/assets/assets/{server-traffic-DNgNJ4Ht.js → server-traffic-C5f87b84.js} +1 -1
  15. package/assets/assets/{trash-2-DOznxxMW.js → trash-2-BFSmyr_7.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-CKWHFAVB.js → chunk-PITZUUFV.js} +33 -1
  18. package/dist/{chunk-3G3GAT3E.js → chunk-QZN3J35I.js} +489 -156
  19. package/dist/{chunk-DXWUBWBD.js → chunk-URPUUKLC.js} +22 -0
  20. package/dist/{chunk-AUR7VMQF.js → chunk-ZTVBTGDW.js} +59 -4
  21. package/dist/cli.js +32 -4
  22. package/dist/index.d.ts +16 -0
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-UYVVKQ2K.js → intelligence-service-RWVBWSB5.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +11 -10
@@ -113,6 +113,7 @@ import {
113
113
  gbpLocationSelectionRequestSchema,
114
114
  gbpLodgingListResponseSchema,
115
115
  gbpPlaceActionListResponseSchema,
116
+ gbpPlaceDetailsListResponseSchema,
116
117
  gbpSummaryDtoSchema,
117
118
  gbpSyncRequestSchema,
118
119
  gbpSyncResponseSchema,
@@ -206,10 +207,10 @@ import {
206
207
  wordpressSchemaDeployResultDtoSchema,
207
208
  wordpressSchemaStatusResultDtoSchema,
208
209
  wordpressStatusDtoSchema
209
- } from "./chunk-DXWUBWBD.js";
210
+ } from "./chunk-URPUUKLC.js";
210
211
 
211
212
  // src/intelligence-service.ts
212
- import { eq as eq29, desc as desc15, asc as asc3, and as and23, ne as ne5, or as or5, inArray as inArray11, gte as gte6, lte as lte3 } from "drizzle-orm";
213
+ import { eq as eq30, desc as desc15, asc as asc3, and as and23, ne as ne5, or as or5, inArray as inArray11, gte as gte6, lte as lte3 } from "drizzle-orm";
213
214
 
214
215
  // ../db/src/client.ts
215
216
  import { mkdirSync } from "fs";
@@ -250,6 +251,7 @@ __export(schema_exports, {
250
251
  gbpLocations: () => gbpLocations,
251
252
  gbpLodgingSnapshots: () => gbpLodgingSnapshots,
252
253
  gbpPlaceActions: () => gbpPlaceActions,
254
+ gbpPlaceDetails: () => gbpPlaceDetails,
253
255
  googleConnections: () => googleConnections,
254
256
  gscCoverageSnapshots: () => gscCoverageSnapshots,
255
257
  gscSearchData: () => gscSearchData,
@@ -1018,6 +1020,12 @@ var gbpLocations = sqliteTable("gbp_locations", {
1018
1020
  primaryCategoryDisplayName: text("primary_category_display_name"),
1019
1021
  storefrontAddress: text("storefront_address"),
1020
1022
  websiteUri: text("website_uri"),
1023
+ // Google Maps Place ID + public Maps link, sourced from the location's
1024
+ // `metadata` (output-only; populated only when the location is on Maps).
1025
+ // `placeId` links a GBP location to the Places API for supplemental
1026
+ // rendered-listing data. Null when Google has not assigned a Place ID.
1027
+ placeId: text("place_id"),
1028
+ mapsUri: text("maps_uri"),
1021
1029
  selected: integer("selected", { mode: "boolean" }).notNull().default(true),
1022
1030
  syncedAt: text("synced_at"),
1023
1031
  createdAt: text("created_at").notNull(),
@@ -1101,6 +1109,19 @@ var gbpLodgingSnapshots = sqliteTable("gbp_lodging_snapshots", {
1101
1109
  }, (table) => [
1102
1110
  index("idx_gbp_lodging_loc").on(table.projectId, table.locationName, table.syncedAt)
1103
1111
  ]);
1112
+ var gbpPlaceDetails = sqliteTable("gbp_place_details", {
1113
+ id: text("id").primaryKey(),
1114
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1115
+ locationName: text("location_name").notNull(),
1116
+ placeId: text("place_id").notNull(),
1117
+ contentHash: text("content_hash").notNull(),
1118
+ tier: text("tier").notNull(),
1119
+ attributes: text("attributes", { mode: "json" }).$type().notNull().default({}),
1120
+ syncedAt: text("synced_at").notNull(),
1121
+ syncRunId: text("sync_run_id").references(() => runs.id, { onDelete: "set null" })
1122
+ }, (table) => [
1123
+ index("idx_gbp_place_details_loc").on(table.projectId, table.locationName, table.syncedAt)
1124
+ ]);
1104
1125
 
1105
1126
  // ../db/src/client.ts
1106
1127
  function createClient(databasePath) {
@@ -2632,6 +2653,38 @@ var MIGRATION_VERSIONS = [
2632
2653
  `CREATE INDEX IF NOT EXISTS idx_gbp_keyword_monthly_loc ON gbp_keyword_monthly(project_id, location_name, month)`,
2633
2654
  `CREATE UNIQUE INDEX IF NOT EXISTS uniq_gbp_keyword_monthly ON gbp_keyword_monthly(project_id, location_name, month, keyword)`
2634
2655
  ]
2656
+ },
2657
+ {
2658
+ // Capture the Google Maps Place ID + Maps link on each location so we can
2659
+ // link it to the Places API for supplemental rendered-listing data (#648).
2660
+ // ALTER ADD COLUMN is idempotent here — the runner swallows the duplicate-
2661
+ // column error on re-apply.
2662
+ version: 71,
2663
+ name: "gbp-locations-place-id",
2664
+ statements: [
2665
+ `ALTER TABLE gbp_locations ADD COLUMN place_id TEXT`,
2666
+ `ALTER TABLE gbp_locations ADD COLUMN maps_uri TEXT`
2667
+ ]
2668
+ },
2669
+ {
2670
+ // Places (New) Place Details snapshots for lodging locations (#648) —
2671
+ // snapshot-on-change, same shape as gbp_lodging_snapshots.
2672
+ version: 72,
2673
+ name: "gbp-place-details",
2674
+ statements: [
2675
+ `CREATE TABLE IF NOT EXISTS gbp_place_details (
2676
+ id TEXT PRIMARY KEY,
2677
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2678
+ location_name TEXT NOT NULL,
2679
+ place_id TEXT NOT NULL,
2680
+ content_hash TEXT NOT NULL,
2681
+ tier TEXT NOT NULL,
2682
+ attributes TEXT NOT NULL DEFAULT '{}',
2683
+ synced_at TEXT NOT NULL,
2684
+ sync_run_id TEXT REFERENCES runs(id) ON DELETE SET NULL
2685
+ )`,
2686
+ `CREATE INDEX IF NOT EXISTS idx_gbp_place_details_loc ON gbp_place_details(project_id, location_name, synced_at)`
2687
+ ]
2635
2688
  }
2636
2689
  ];
2637
2690
  function isDuplicateColumnError(err) {
@@ -4531,21 +4584,40 @@ var METRIC_LABELS = {
4531
4584
  function metricLabel(metric) {
4532
4585
  return METRIC_LABELS[metric] ?? metric;
4533
4586
  }
4587
+ function formatAmenityList(amenities) {
4588
+ if (amenities.length === 1) return amenities[0];
4589
+ if (amenities.length === 2) return `${amenities[0]} and ${amenities[1]}`;
4590
+ return `${amenities.slice(0, -1).join(", ")}, and ${amenities[amenities.length - 1]}`;
4591
+ }
4534
4592
  function analyzeGbp(signals) {
4535
4593
  const drafts = [];
4536
4594
  for (const loc of signals) {
4537
4595
  const base = { locationName: loc.locationName, query: loc.displayName, provider: GBP_INSIGHT_PROVIDER };
4538
4596
  if (loc.lodgingCapable && loc.lodgingEmpty) {
4539
- drafts.push({
4540
- ...base,
4541
- type: "gbp-lodging-gap",
4542
- severity: "high",
4543
- title: `${loc.displayName}: lodging profile has no structured attributes`,
4544
- recommendation: {
4545
- action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile \u2014 the amenity source you directly control",
4546
- reason: "The GBP API exposes only owner-configured attributes, and this profile has none. Google\u2019s rendered listing may still show amenities it synthesizes from Hotel Center, OTAs, and user data \u2014 so the public listing can differ from this profile \u2014 but the structured attributes are what AI answer engines cite and the only amenity data you control."
4547
- }
4548
- });
4597
+ if (loc.placesAmenities.length > 0) {
4598
+ const amenityList = formatAmenityList(loc.placesAmenities);
4599
+ drafts.push({
4600
+ ...base,
4601
+ type: "gbp-listing-discrepancy",
4602
+ severity: "high",
4603
+ title: `${loc.displayName}: public listing shows ${loc.placesAmenities.length} amenit${loc.placesAmenities.length === 1 ? "y" : "ies"} your GBP profile doesn\u2019t`,
4604
+ recommendation: {
4605
+ action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile to match what its public listing already advertises \u2014 the amenity source you directly control",
4606
+ reason: `Google\u2019s rendered listing advertises ${amenityList} (synthesized from Hotel Center / OTAs / Places), but your GBP structured profile has zero populated attributes. The structured attributes are what AI answer engines cite and the only amenity data you control, so the public listing is making promises your profile can\u2019t back.`
4607
+ }
4608
+ });
4609
+ } else {
4610
+ drafts.push({
4611
+ ...base,
4612
+ type: "gbp-lodging-gap",
4613
+ severity: "high",
4614
+ title: `${loc.displayName}: lodging profile has no structured attributes`,
4615
+ recommendation: {
4616
+ action: "Populate the hotel\u2019s structured amenity attributes in Google Business Profile \u2014 the amenity source you directly control",
4617
+ reason: "The GBP API exposes only owner-configured attributes, and this profile has none. Google\u2019s rendered listing may still show amenities it synthesizes from Hotel Center, OTAs, and user data \u2014 so the public listing can differ from this profile \u2014 but the structured attributes are what AI answer engines cite and the only amenity data you control."
4618
+ }
4619
+ });
4620
+ }
4549
4621
  }
4550
4622
  if (loc.placeActionCount > 0 && !loc.hasDirectMerchantCta) {
4551
4623
  drafts.push({
@@ -4616,11 +4688,141 @@ function pickWorstKeywordDrop(loc) {
4616
4688
  return worst;
4617
4689
  }
4618
4690
 
4691
+ // ../integration-google-places/src/constants.ts
4692
+ var PLACES_API_BASE = "https://places.googleapis.com/v1";
4693
+ var PLACES_REQUEST_TIMEOUT_MS = 3e4;
4694
+ var PLACES_PRO_FIELDS = [
4695
+ "id",
4696
+ "types",
4697
+ "primaryType",
4698
+ "primaryTypeDisplayName",
4699
+ "googleMapsUri",
4700
+ "websiteUri",
4701
+ "businessStatus",
4702
+ "accessibilityOptions"
4703
+ ];
4704
+ var PLACES_ATMOSPHERE_FIELDS = [
4705
+ ...PLACES_PRO_FIELDS,
4706
+ "editorialSummary",
4707
+ "servesBreakfast",
4708
+ "servesLunch",
4709
+ "servesDinner",
4710
+ "servesBrunch",
4711
+ "restroom",
4712
+ "goodForChildren",
4713
+ "goodForGroups",
4714
+ "allowsDogs",
4715
+ "parkingOptions",
4716
+ "outdoorSeating",
4717
+ "reservable"
4718
+ ];
4719
+
4720
+ // ../integration-google-places/src/types.ts
4721
+ var PlacesApiError = class extends Error {
4722
+ status;
4723
+ reason;
4724
+ body;
4725
+ constructor(message, status, reason, body) {
4726
+ super(message);
4727
+ this.name = "PlacesApiError";
4728
+ this.status = status;
4729
+ this.reason = reason;
4730
+ this.body = body;
4731
+ }
4732
+ };
4733
+
4734
+ // ../integration-google-places/src/http.ts
4735
+ function extractReason(body) {
4736
+ if (!body || typeof body !== "object") return null;
4737
+ const status = body.error?.status;
4738
+ return typeof status === "string" ? status : null;
4739
+ }
4740
+ function isRetryable(err) {
4741
+ if (!(err instanceof PlacesApiError)) return false;
4742
+ return err.status === 429 || err.status === 503;
4743
+ }
4744
+ async function placesFetchOnce(url, apiKey, fieldMask) {
4745
+ const controller = new AbortController();
4746
+ const timeout = setTimeout(() => controller.abort(), PLACES_REQUEST_TIMEOUT_MS);
4747
+ try {
4748
+ const headers = {
4749
+ "X-Goog-Api-Key": apiKey,
4750
+ "X-Goog-FieldMask": fieldMask,
4751
+ accept: "application/json"
4752
+ };
4753
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
4754
+ const text2 = await res.text();
4755
+ let body;
4756
+ try {
4757
+ body = text2 ? JSON.parse(text2) : void 0;
4758
+ } catch {
4759
+ body = text2;
4760
+ }
4761
+ if (!res.ok) {
4762
+ const payload = body;
4763
+ const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
4764
+ throw new PlacesApiError(message, res.status, extractReason(body), body);
4765
+ }
4766
+ return body;
4767
+ } finally {
4768
+ clearTimeout(timeout);
4769
+ }
4770
+ }
4771
+ async function placesFetchGet(url, apiKey, fieldMask, opts = {}) {
4772
+ return withRetry(() => placesFetchOnce(url, apiKey, fieldMask), {
4773
+ maxRetries: opts.retry?.maxRetries ?? 3,
4774
+ baseDelayMs: opts.retry?.baseDelayMs ?? 500,
4775
+ jitter: true,
4776
+ isRetryable,
4777
+ sleep: opts.retry?.sleep
4778
+ });
4779
+ }
4780
+
4781
+ // ../integration-google-places/src/place-details-client.ts
4782
+ import crypto from "crypto";
4783
+ function buildPlaceDetailsFieldMask(tier) {
4784
+ const fields = tier === "atmosphere" ? PLACES_ATMOSPHERE_FIELDS : PLACES_PRO_FIELDS;
4785
+ return fields.join(",");
4786
+ }
4787
+ async function getPlaceDetails(placeId, apiKey, opts = {}) {
4788
+ const fieldMask = opts.fieldMask ?? buildPlaceDetailsFieldMask(opts.tier ?? "atmosphere");
4789
+ const url = new URL(`${PLACES_API_BASE}/places/${encodeURIComponent(placeId)}`);
4790
+ if (opts.languageCode) url.searchParams.set("languageCode", opts.languageCode);
4791
+ return placesFetchGet(url.toString(), apiKey, fieldMask, opts);
4792
+ }
4793
+ function stableStringify(value) {
4794
+ if (value === void 0) return "null";
4795
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
4796
+ if (value && typeof value === "object") {
4797
+ const entries = Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
4798
+ return `{${entries.join(",")}}`;
4799
+ }
4800
+ return JSON.stringify(value);
4801
+ }
4802
+ function hashPlaceDetails(place) {
4803
+ return crypto.createHash("sha256").update(stableStringify(place)).digest("hex");
4804
+ }
4805
+
4806
+ // ../integration-google-places/src/amenities.ts
4807
+ function extractPlaceAmenities(place) {
4808
+ const out = [];
4809
+ if (place.servesBreakfast) out.push("breakfast");
4810
+ if (place.servesLunch || place.servesDinner || place.servesBrunch) out.push("on-site dining");
4811
+ if (place.allowsDogs) out.push("pet-friendly");
4812
+ if (place.parkingOptions && Object.values(place.parkingOptions).some(Boolean)) out.push("parking");
4813
+ if (place.accessibilityOptions && Object.values(place.accessibilityOptions).some(Boolean)) out.push("wheelchair accessibility");
4814
+ if (place.restroom) out.push("restroom");
4815
+ if (place.goodForChildren) out.push("family-friendly");
4816
+ if (place.outdoorSeating) out.push("outdoor seating");
4817
+ if (place.reservable) out.push("reservations");
4818
+ return out;
4819
+ }
4820
+
4619
4821
  // ../api-routes/src/index.ts
4620
4822
  import fs8 from "fs";
4621
4823
 
4622
4824
  // ../api-routes/src/auth.ts
4623
- import crypto from "crypto";
4825
+ import crypto2 from "crypto";
4624
4826
  import { eq } from "drizzle-orm";
4625
4827
  function requireScope(request, scope) {
4626
4828
  const key = request.apiKey;
@@ -4629,7 +4831,7 @@ function requireScope(request, scope) {
4629
4831
  throw forbidden(`This action requires the "${scope}" scope on your API key.`);
4630
4832
  }
4631
4833
  function hashKey(key) {
4632
- return crypto.createHash("sha256").update(key).digest("hex");
4834
+ return crypto2.createHash("sha256").update(key).digest("hex");
4633
4835
  }
4634
4836
  var SKIP_PATHS = ["/health"];
4635
4837
  function shouldSkipAuth(url) {
@@ -4693,11 +4895,11 @@ async function authPlugin(app, opts = {}) {
4693
4895
  }
4694
4896
 
4695
4897
  // ../api-routes/src/projects.ts
4696
- import crypto3 from "crypto";
4898
+ import crypto4 from "crypto";
4697
4899
  import { eq as eq3, sql as sql3 } from "drizzle-orm";
4698
4900
 
4699
4901
  // ../api-routes/src/helpers.ts
4700
- import crypto2 from "crypto";
4902
+ import crypto3 from "crypto";
4701
4903
  import { eq as eq2, ne, sql as sql2 } from "drizzle-orm";
4702
4904
  function notProbeRun() {
4703
4905
  return ne(runs.trigger, RunTriggers.probe);
@@ -4712,7 +4914,7 @@ function resolveProject(db, name) {
4712
4914
  function writeAuditLog(db, entry) {
4713
4915
  const now = (/* @__PURE__ */ new Date()).toISOString();
4714
4916
  db.insert(auditLog).values({
4715
- id: crypto2.randomUUID(),
4917
+ id: crypto3.randomUUID(),
4716
4918
  projectId: entry.projectId ?? null,
4717
4919
  actor: entry.actor,
4718
4920
  action: entry.action,
@@ -4846,7 +5048,7 @@ async function projectRoutes(app, opts) {
4846
5048
  const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
4847
5049
  return reply.status(200).send(formatProject(updated));
4848
5050
  }
4849
- const id = crypto3.randomUUID();
5051
+ const id = crypto4.randomUUID();
4850
5052
  app.db.transaction((tx) => {
4851
5053
  tx.insert(projects).values({
4852
5054
  id,
@@ -5084,7 +5286,7 @@ function aliasArraysEqual(a, b) {
5084
5286
  }
5085
5287
 
5086
5288
  // ../api-routes/src/queries.ts
5087
- import crypto4 from "crypto";
5289
+ import crypto5 from "crypto";
5088
5290
  import { eq as eq4, inArray, sql as sql4 } from "drizzle-orm";
5089
5291
  function preserveSnapshotQueryText(tx, projectId, queryIds) {
5090
5292
  const candidates = queryIds && queryIds.length > 0 ? tx.select({ id: queries.id, text: queries.query }).from(queries).where(inArray(queries.id, queryIds)).all() : tx.select({ id: queries.id, text: queries.query }).from(queries).where(eq4(queries.projectId, projectId)).all();
@@ -5110,7 +5312,7 @@ async function queryRoutes(app, opts) {
5110
5312
  tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
5111
5313
  for (const q of body.queries) {
5112
5314
  tx.insert(queries).values({
5113
- id: crypto4.randomUUID(),
5315
+ id: crypto5.randomUUID(),
5114
5316
  projectId: project.id,
5115
5317
  query: q,
5116
5318
  provenance: "cli",
@@ -5198,7 +5400,7 @@ async function queryRoutes(app, opts) {
5198
5400
  for (const q of body.queries) {
5199
5401
  if (!existingSet.has(q)) {
5200
5402
  app.db.insert(queries).values({
5201
- id: crypto4.randomUUID(),
5403
+ id: crypto5.randomUUID(),
5202
5404
  projectId: project.id,
5203
5405
  query: q,
5204
5406
  provenance: "cli",
@@ -5277,7 +5479,7 @@ async function queryRoutes(app, opts) {
5277
5479
  tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
5278
5480
  for (const keyword of body.keywords) {
5279
5481
  tx.insert(queries).values({
5280
- id: crypto4.randomUUID(),
5482
+ id: crypto5.randomUUID(),
5281
5483
  projectId: project.id,
5282
5484
  query: keyword,
5283
5485
  provenance: "cli",
@@ -5335,7 +5537,7 @@ async function queryRoutes(app, opts) {
5335
5537
  for (const keyword of body.keywords) {
5336
5538
  if (!existingSet.has(keyword)) {
5337
5539
  app.db.insert(queries).values({
5338
- id: crypto4.randomUUID(),
5540
+ id: crypto5.randomUUID(),
5339
5541
  projectId: project.id,
5340
5542
  query: keyword,
5341
5543
  provenance: "cli",
@@ -5400,7 +5602,7 @@ async function queryRoutes(app, opts) {
5400
5602
  }
5401
5603
 
5402
5604
  // ../api-routes/src/competitors.ts
5403
- import crypto5 from "crypto";
5605
+ import crypto6 from "crypto";
5404
5606
  import { eq as eq5 } from "drizzle-orm";
5405
5607
  function normalizeCompetitor(domain) {
5406
5608
  const reg = registrableDomain(domain);
@@ -5438,7 +5640,7 @@ async function competitorRoutes(app) {
5438
5640
  tx.delete(competitors).where(eq5(competitors.projectId, project.id)).run();
5439
5641
  for (const domain of normalizedCompetitors) {
5440
5642
  tx.insert(competitors).values({
5441
- id: crypto5.randomUUID(),
5643
+ id: crypto6.randomUUID(),
5442
5644
  projectId: project.id,
5443
5645
  domain,
5444
5646
  provenance: "cli",
@@ -5468,7 +5670,7 @@ async function competitorRoutes(app) {
5468
5670
  if (added.length === 0) return;
5469
5671
  for (const domain of added) {
5470
5672
  tx.insert(competitors).values({
5471
- id: crypto5.randomUUID(),
5673
+ id: crypto6.randomUUID(),
5472
5674
  projectId: project.id,
5473
5675
  domain,
5474
5676
  provenance: "cli",
@@ -5523,18 +5725,18 @@ function parseCompetitorBatch(value) {
5523
5725
  }
5524
5726
 
5525
5727
  // ../api-routes/src/runs.ts
5526
- import crypto7 from "crypto";
5728
+ import crypto8 from "crypto";
5527
5729
  import { and as and2, eq as eq7, asc, desc, or as or2, sql as sql5 } from "drizzle-orm";
5528
5730
  import { gte } from "drizzle-orm";
5529
5731
 
5530
5732
  // ../api-routes/src/run-queue.ts
5531
- import crypto6 from "crypto";
5733
+ import crypto7 from "crypto";
5532
5734
  import { and, eq as eq6, or } from "drizzle-orm";
5533
5735
  function queueRunIfProjectIdle(db, params) {
5534
5736
  const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
5535
5737
  const kind = params.kind ?? "answer-visibility";
5536
5738
  const trigger = params.trigger ?? "manual";
5537
- const runId = crypto6.randomUUID();
5739
+ const runId = crypto7.randomUUID();
5538
5740
  return db.transaction((tx) => {
5539
5741
  const activeRun = tx.select().from(runs).where(
5540
5742
  and(
@@ -5629,7 +5831,7 @@ async function runRoutes(app, opts) {
5629
5831
  }
5630
5832
  const inserted = [];
5631
5833
  for (const loc of projectLocations) {
5632
- const runId2 = crypto7.randomUUID();
5834
+ const runId2 = crypto8.randomUUID();
5633
5835
  tx.insert(runs).values({
5634
5836
  id: runId2,
5635
5837
  projectId: project.id,
@@ -5925,7 +6127,7 @@ function loadRunDetail(app, run) {
5925
6127
  }
5926
6128
 
5927
6129
  // ../api-routes/src/apply.ts
5928
- import crypto9 from "crypto";
6130
+ import crypto10 from "crypto";
5929
6131
  import { and as and3, eq as eq8 } from "drizzle-orm";
5930
6132
 
5931
6133
  // ../api-routes/src/schedule-utils.ts
@@ -6017,7 +6219,7 @@ function isValidTimezone(tz) {
6017
6219
  }
6018
6220
 
6019
6221
  // ../api-routes/src/webhooks.ts
6020
- import crypto8 from "crypto";
6222
+ import crypto9 from "crypto";
6021
6223
  import dns from "dns/promises";
6022
6224
  import http from "http";
6023
6225
  import https from "https";
@@ -6069,7 +6271,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
6069
6271
  "User-Agent": "Canonry/0.1.0"
6070
6272
  };
6071
6273
  if (webhookSecret) {
6072
- headers["X-Canonry-Signature"] = "sha256=" + crypto8.createHmac("sha256", webhookSecret).update(body).digest("hex");
6274
+ headers["X-Canonry-Signature"] = "sha256=" + crypto9.createHmac("sha256", webhookSecret).update(body).digest("hex");
6073
6275
  }
6074
6276
  return await new Promise((resolve) => {
6075
6277
  const requestOptions = {
@@ -6284,7 +6486,7 @@ async function applyRoutes(app, opts) {
6284
6486
  entityId: projectId
6285
6487
  });
6286
6488
  } else {
6287
- projectId = crypto9.randomUUID();
6489
+ projectId = crypto10.randomUUID();
6288
6490
  tx.insert(projects).values({
6289
6491
  id: projectId,
6290
6492
  name,
@@ -6316,7 +6518,7 @@ async function applyRoutes(app, opts) {
6316
6518
  tx.delete(queries).where(eq8(queries.projectId, projectId)).run();
6317
6519
  for (const q of configQueries) {
6318
6520
  tx.insert(queries).values({
6319
- id: crypto9.randomUUID(),
6521
+ id: crypto10.randomUUID(),
6320
6522
  projectId,
6321
6523
  query: q,
6322
6524
  provenance: "cli",
@@ -6334,7 +6536,7 @@ async function applyRoutes(app, opts) {
6334
6536
  const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
6335
6537
  for (const domain of normalizedCompetitors) {
6336
6538
  tx.insert(competitors).values({
6337
- id: crypto9.randomUUID(),
6539
+ id: crypto10.randomUUID(),
6338
6540
  projectId,
6339
6541
  domain,
6340
6542
  provenance: "cli",
@@ -6362,7 +6564,7 @@ async function applyRoutes(app, opts) {
6362
6564
  }).where(eq8(schedules.id, existingSched.id)).run();
6363
6565
  } else {
6364
6566
  tx.insert(schedules).values({
6365
- id: crypto9.randomUUID(),
6567
+ id: crypto10.randomUUID(),
6366
6568
  projectId,
6367
6569
  kind: AV_KIND,
6368
6570
  cronExpr: resolvedSchedule.cronExpr,
@@ -6386,11 +6588,11 @@ async function applyRoutes(app, opts) {
6386
6588
  tx.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
6387
6589
  for (const notif of config.spec.notifications) {
6388
6590
  tx.insert(notifications).values({
6389
- id: crypto9.randomUUID(),
6591
+ id: crypto10.randomUUID(),
6390
6592
  projectId,
6391
6593
  channel: notif.channel,
6392
6594
  config: { url: notif.url, events: notif.events },
6393
- webhookSecret: crypto9.randomBytes(32).toString("hex"),
6595
+ webhookSecret: crypto10.randomBytes(32).toString("hex"),
6394
6596
  enabled: true,
6395
6597
  createdAt: now,
6396
6598
  updatedAt: now
@@ -7262,7 +7464,7 @@ async function intelligenceRoutes(app) {
7262
7464
  import { and as and9, desc as desc7, eq as eq14, gte as gte2, inArray as inArray6, lt, lte, ne as ne2, or as or3, sql as sql6 } from "drizzle-orm";
7263
7465
 
7264
7466
  // ../api-routes/src/content.ts
7265
- import crypto10 from "crypto";
7467
+ import crypto11 from "crypto";
7266
7468
  import { and as and8, desc as desc6, eq as eq13 } from "drizzle-orm";
7267
7469
 
7268
7470
  // ../api-routes/src/content-data.ts
@@ -7724,7 +7926,7 @@ async function contentRoutes(app, opts = {}) {
7724
7926
  const { targetRef, addressedUrl, note } = parsed.data;
7725
7927
  const now = (/* @__PURE__ */ new Date()).toISOString();
7726
7928
  app.db.insert(contentTargetDismissals).values({
7727
- id: crypto10.randomUUID(),
7929
+ id: crypto11.randomUUID(),
7728
7930
  projectId: project.id,
7729
7931
  targetRef,
7730
7932
  addressedUrl: addressedUrl ?? null,
@@ -7803,7 +8005,7 @@ async function contentRoutes(app, opts = {}) {
7803
8005
  });
7804
8006
  const now = (/* @__PURE__ */ new Date()).toISOString();
7805
8007
  app.db.insert(recommendationExplanations).values({
7806
- id: crypto10.randomUUID(),
8008
+ id: crypto11.randomUUID(),
7807
8009
  projectId: project.id,
7808
8010
  targetRef,
7809
8011
  promptVersion: result.promptVersion,
@@ -12371,6 +12573,7 @@ var SCHEMA_TABLE = {
12371
12573
  GbpLocationListResponse: gbpLocationListResponseSchema,
12372
12574
  GbpLodgingListResponse: gbpLodgingListResponseSchema,
12373
12575
  GbpPlaceActionListResponse: gbpPlaceActionListResponseSchema,
12576
+ GbpPlaceDetailsListResponse: gbpPlaceDetailsListResponseSchema,
12374
12577
  GbpSummaryDto: gbpSummaryDtoSchema,
12375
12578
  GbpSyncResponse: gbpSyncResponseSchema,
12376
12579
  GoogleConnectionDto: googleConnectionDtoSchema,
@@ -14269,6 +14472,20 @@ var routeCatalog = [
14269
14472
  404: errorResponse("Project not found.")
14270
14473
  }
14271
14474
  },
14475
+ {
14476
+ method: "get",
14477
+ path: "/api/v1/projects/{name}/gbp/places",
14478
+ summary: "List latest Google Places rendered-listing snapshots per location",
14479
+ tags: ["gbp"],
14480
+ parameters: [
14481
+ nameParameter,
14482
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
14483
+ ],
14484
+ responses: {
14485
+ 200: jsonResponse("Place Details snapshots returned.", "GbpPlaceDetailsListResponse"),
14486
+ 404: errorResponse("Project not found.")
14487
+ }
14488
+ },
14272
14489
  {
14273
14490
  method: "get",
14274
14491
  path: "/api/v1/projects/{name}/gbp/summary",
@@ -16328,7 +16545,7 @@ async function telemetryRoutes(app, opts) {
16328
16545
  }
16329
16546
 
16330
16547
  // ../api-routes/src/schedules.ts
16331
- import crypto11 from "crypto";
16548
+ import crypto12 from "crypto";
16332
16549
  import { and as and12, eq as eq17 } from "drizzle-orm";
16333
16550
  function parseKindParam(raw) {
16334
16551
  if (raw === void 0 || raw === null || raw === "") return SchedulableRunKinds["answer-visibility"];
@@ -16408,7 +16625,7 @@ async function scheduleRoutes(app, opts) {
16408
16625
  }).where(eq17(schedules.id, existing.id)).run();
16409
16626
  } else {
16410
16627
  app.db.insert(schedules).values({
16411
- id: crypto11.randomUUID(),
16628
+ id: crypto12.randomUUID(),
16412
16629
  projectId: project.id,
16413
16630
  kind,
16414
16631
  cronExpr,
@@ -16480,7 +16697,7 @@ function formatSchedule(row) {
16480
16697
  }
16481
16698
 
16482
16699
  // ../api-routes/src/notifications.ts
16483
- import crypto12 from "crypto";
16700
+ import crypto13 from "crypto";
16484
16701
  import { eq as eq18 } from "drizzle-orm";
16485
16702
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
16486
16703
  async function notificationRoutes(app, opts = {}) {
@@ -16500,8 +16717,8 @@ async function notificationRoutes(app, opts = {}) {
16500
16717
  throw validationError(`Invalid event(s): ${invalid.join(", ")}. Must be one of: ${VALID_EVENTS.join(", ")}`);
16501
16718
  }
16502
16719
  const now = (/* @__PURE__ */ new Date()).toISOString();
16503
- const id = crypto12.randomUUID();
16504
- const webhookSecret = crypto12.randomBytes(32).toString("hex");
16720
+ const id = crypto13.randomUUID();
16721
+ const webhookSecret = crypto13.randomBytes(32).toString("hex");
16505
16722
  app.db.insert(notifications).values({
16506
16723
  id,
16507
16724
  projectId: project.id,
@@ -16600,7 +16817,7 @@ function formatNotification(row) {
16600
16817
  }
16601
16818
 
16602
16819
  // ../api-routes/src/google.ts
16603
- import crypto15 from "crypto";
16820
+ import crypto16 from "crypto";
16604
16821
  import { eq as eq19, and as and13, desc as desc9, sql as sql8, inArray as inArray9 } from "drizzle-orm";
16605
16822
 
16606
16823
  // ../api-routes/src/gbp-summary.ts
@@ -17056,7 +17273,7 @@ async function inspectUrl(accessToken, inspectionUrl, siteUrl) {
17056
17273
  }
17057
17274
 
17058
17275
  // ../integration-google-analytics/src/ga4-client.ts
17059
- import crypto13 from "crypto";
17276
+ import crypto14 from "crypto";
17060
17277
 
17061
17278
  // ../integration-google-analytics/src/constants.ts
17062
17279
  var GA4_DATA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
@@ -17163,7 +17380,7 @@ function createServiceAccountJwt(clientEmail, privateKey, scope) {
17163
17380
  const headerB64 = encode(header);
17164
17381
  const payloadB64 = encode(payload);
17165
17382
  const signingInput = `${headerB64}.${payloadB64}`;
17166
- const sign = crypto13.createSign("RSA-SHA256");
17383
+ const sign = crypto14.createSign("RSA-SHA256");
17167
17384
  sign.update(signingInput);
17168
17385
  const signature = sign.sign(privateKey, "base64url");
17169
17386
  return `${signingInput}.${signature}`;
@@ -17777,7 +17994,9 @@ var GBP_LOCATIONS_DEFAULT_READ_MASK = [
17777
17994
  "title",
17778
17995
  "storefrontAddress",
17779
17996
  "websiteUri",
17780
- "categories.primaryCategory.displayName"
17997
+ "categories.primaryCategory.displayName",
17998
+ "metadata.placeId",
17999
+ "metadata.mapsUri"
17781
18000
  ].join(",");
17782
18001
 
17783
18002
  // ../integration-google-business-profile/src/types.ts
@@ -17797,7 +18016,7 @@ var GbpApiError = class extends Error {
17797
18016
  };
17798
18017
 
17799
18018
  // ../integration-google-business-profile/src/http.ts
17800
- function extractReason(body) {
18019
+ function extractReason2(body) {
17801
18020
  if (!body || typeof body !== "object") return null;
17802
18021
  const payload = body;
17803
18022
  const details = payload.error?.details;
@@ -17821,7 +18040,7 @@ function extractQuotaLimitValue(body) {
17821
18040
  }
17822
18041
  return null;
17823
18042
  }
17824
- function isRetryable(err) {
18043
+ function isRetryable2(err) {
17825
18044
  if (!(err instanceof GbpApiError)) return false;
17826
18045
  if (err.status === 429) {
17827
18046
  return err.quotaLimitValue !== 0;
@@ -17849,7 +18068,7 @@ async function gbpFetchOnce(url, accessToken, opts) {
17849
18068
  if (!res.ok) {
17850
18069
  const payload = body;
17851
18070
  const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
17852
- throw new GbpApiError(message, res.status, extractReason(body), body, extractQuotaLimitValue(body));
18071
+ throw new GbpApiError(message, res.status, extractReason2(body), body, extractQuotaLimitValue(body));
17853
18072
  }
17854
18073
  return body;
17855
18074
  } finally {
@@ -17861,7 +18080,7 @@ async function gbpFetchGet(url, accessToken, opts = {}) {
17861
18080
  maxRetries: opts.retry?.maxRetries ?? 5,
17862
18081
  baseDelayMs: opts.retry?.baseDelayMs ?? 1e3,
17863
18082
  jitter: true,
17864
- isRetryable,
18083
+ isRetryable: isRetryable2,
17865
18084
  sleep: opts.retry?.sleep
17866
18085
  });
17867
18086
  }
@@ -18001,7 +18220,7 @@ async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
18001
18220
  }
18002
18221
 
18003
18222
  // ../integration-google-business-profile/src/lodging-client.ts
18004
- import crypto14 from "crypto";
18223
+ import crypto15 from "crypto";
18005
18224
  async function getLodging(accessToken, locationName, opts = {}) {
18006
18225
  const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
18007
18226
  try {
@@ -18028,14 +18247,14 @@ function isPopulated(value) {
18028
18247
  return true;
18029
18248
  }
18030
18249
  function hashLodging(lodging) {
18031
- return crypto14.createHash("sha256").update(stableStringify(lodging)).digest("hex");
18250
+ return crypto15.createHash("sha256").update(stableStringify2(lodging)).digest("hex");
18032
18251
  }
18033
- function stableStringify(value) {
18252
+ function stableStringify2(value) {
18034
18253
  if (value === null || typeof value !== "object") return JSON.stringify(value);
18035
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
18254
+ if (Array.isArray(value)) return `[${value.map(stableStringify2).join(",")}]`;
18036
18255
  const obj = value;
18037
18256
  const keys = Object.keys(obj).sort();
18038
- return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
18257
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify2(obj[k])}`).join(",")}}`;
18039
18258
  }
18040
18259
 
18041
18260
  // ../api-routes/src/google.ts
@@ -18050,7 +18269,7 @@ function scopesForConnectionType(type) {
18050
18269
  }
18051
18270
  }
18052
18271
  function signState(payload, secret) {
18053
- return crypto15.createHmac("sha256", secret).update(payload).digest("hex");
18272
+ return crypto16.createHmac("sha256", secret).update(payload).digest("hex");
18054
18273
  }
18055
18274
  function buildSignedState(data, secret) {
18056
18275
  const payload = JSON.stringify(data);
@@ -18061,7 +18280,7 @@ function verifySignedState(encoded, secret) {
18061
18280
  try {
18062
18281
  const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
18063
18282
  const expected = signState(payload, secret);
18064
- if (!crypto15.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
18283
+ if (!crypto16.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
18065
18284
  return JSON.parse(payload);
18066
18285
  } catch {
18067
18286
  return null;
@@ -18331,7 +18550,7 @@ async function googleRoutes(app, opts) {
18331
18550
  throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
18332
18551
  }
18333
18552
  const now = (/* @__PURE__ */ new Date()).toISOString();
18334
- const runId = crypto15.randomUUID();
18553
+ const runId = crypto16.randomUUID();
18335
18554
  app.db.insert(runs).values({
18336
18555
  id: runId,
18337
18556
  projectId: project.id,
@@ -18424,7 +18643,7 @@ async function googleRoutes(app, opts) {
18424
18643
  const mob = ir.mobileUsabilityResult;
18425
18644
  const rich = ir.richResultsResult;
18426
18645
  const now = (/* @__PURE__ */ new Date()).toISOString();
18427
- const id = crypto15.randomUUID();
18646
+ const id = crypto16.randomUUID();
18428
18647
  app.db.insert(gscUrlInspections).values({
18429
18648
  id,
18430
18649
  projectId: project.id,
@@ -18659,7 +18878,7 @@ async function googleRoutes(app, opts) {
18659
18878
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18660
18879
  });
18661
18880
  const now = (/* @__PURE__ */ new Date()).toISOString();
18662
- const runId = crypto15.randomUUID();
18881
+ const runId = crypto16.randomUUID();
18663
18882
  app.db.insert(runs).values({
18664
18883
  id: runId,
18665
18884
  projectId: project.id,
@@ -18685,7 +18904,7 @@ async function googleRoutes(app, opts) {
18685
18904
  throw validationError("No GSC property configured for this connection");
18686
18905
  }
18687
18906
  const now = (/* @__PURE__ */ new Date()).toISOString();
18688
- const runId = crypto15.randomUUID();
18907
+ const runId = crypto16.randomUUID();
18689
18908
  app.db.insert(runs).values({
18690
18909
  id: runId,
18691
18910
  projectId: project.id,
@@ -18847,6 +19066,8 @@ async function googleRoutes(app, opts) {
18847
19066
  primaryCategoryDisplayName: row.primaryCategoryDisplayName ?? null,
18848
19067
  storefrontAddress: row.storefrontAddress ?? null,
18849
19068
  websiteUri: row.websiteUri ?? null,
19069
+ placeId: row.placeId ?? null,
19070
+ mapsUri: row.mapsUri ?? null,
18850
19071
  selected: Boolean(row.selected),
18851
19072
  syncedAt: row.syncedAt ?? null,
18852
19073
  createdAt: row.createdAt,
@@ -18949,11 +19170,13 @@ async function googleRoutes(app, opts) {
18949
19170
  primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
18950
19171
  storefrontAddress: formatStorefrontAddress(remote),
18951
19172
  websiteUri: remote.websiteUri ?? null,
19173
+ placeId: remote.metadata?.placeId ?? null,
19174
+ mapsUri: remote.metadata?.mapsUri ?? null,
18952
19175
  updatedAt: now
18953
19176
  }).where(eq19(gbpLocations.id, existing.id)).run();
18954
19177
  } else {
18955
19178
  tx.insert(gbpLocations).values({
18956
- id: crypto15.randomUUID(),
19179
+ id: crypto16.randomUUID(),
18957
19180
  projectId: project.id,
18958
19181
  accountName,
18959
19182
  locationName: remote.name,
@@ -18961,6 +19184,8 @@ async function googleRoutes(app, opts) {
18961
19184
  primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
18962
19185
  storefrontAddress: formatStorefrontAddress(remote),
18963
19186
  websiteUri: remote.websiteUri ?? null,
19187
+ placeId: remote.metadata?.placeId ?? null,
19188
+ mapsUri: remote.metadata?.mapsUri ?? null,
18964
19189
  selected: selectAllNew,
18965
19190
  createdAt: now,
18966
19191
  updatedAt: now
@@ -19074,7 +19299,7 @@ async function googleRoutes(app, opts) {
19074
19299
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
19075
19300
  }
19076
19301
  const now = (/* @__PURE__ */ new Date()).toISOString();
19077
- const runId = crypto15.randomUUID();
19302
+ const runId = crypto16.randomUUID();
19078
19303
  app.db.insert(runs).values({
19079
19304
  id: runId,
19080
19305
  projectId: project.id,
@@ -19151,6 +19376,26 @@ async function googleRoutes(app, opts) {
19151
19376
  }));
19152
19377
  return { lodging, total: lodging.length };
19153
19378
  });
19379
+ app.get("/projects/:name/gbp/places", async (request) => {
19380
+ const project = resolveProject(app.db, request.params.name);
19381
+ const conditions = [eq19(gbpPlaceDetails.projectId, project.id)];
19382
+ if (request.query.locationName) conditions.push(eq19(gbpPlaceDetails.locationName, request.query.locationName));
19383
+ const rows = app.db.select().from(gbpPlaceDetails).where(and13(...conditions)).orderBy(desc9(gbpPlaceDetails.syncedAt)).all();
19384
+ const latestByLocation = /* @__PURE__ */ new Map();
19385
+ for (const row of rows) {
19386
+ if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
19387
+ }
19388
+ const places = [...latestByLocation.values()].map((r) => ({
19389
+ locationName: r.locationName,
19390
+ placeId: r.placeId,
19391
+ tier: r.tier,
19392
+ // Derived server-side so agents/UI consume the same amenity list.
19393
+ amenities: extractPlaceAmenities(r.attributes),
19394
+ syncedAt: r.syncedAt,
19395
+ place: r.attributes
19396
+ }));
19397
+ return { places, total: places.length };
19398
+ });
19154
19399
  app.get("/projects/:name/gbp/summary", async (request) => {
19155
19400
  const project = resolveProject(app.db, request.params.name);
19156
19401
  const locationName = request.query.locationName ?? null;
@@ -19191,7 +19436,7 @@ async function googleRoutes(app, opts) {
19191
19436
  }
19192
19437
 
19193
19438
  // ../api-routes/src/bing.ts
19194
- import crypto16 from "crypto";
19439
+ import crypto17 from "crypto";
19195
19440
  import { eq as eq20, and as and14, desc as desc10 } from "drizzle-orm";
19196
19441
 
19197
19442
  // ../integration-bing/src/constants.ts
@@ -19577,7 +19822,7 @@ async function bingRoutes(app, opts) {
19577
19822
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
19578
19823
  const now = (/* @__PURE__ */ new Date()).toISOString();
19579
19824
  app.db.insert(bingCoverageSnapshots).values({
19580
- id: crypto16.randomUUID(),
19825
+ id: crypto17.randomUUID(),
19581
19826
  projectId: project.id,
19582
19827
  syncRunId: snapshotRunId,
19583
19828
  date: snapshotDate,
@@ -19648,7 +19893,7 @@ async function bingRoutes(app, opts) {
19648
19893
  throw validationError("url is required");
19649
19894
  }
19650
19895
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
19651
- const runId = crypto16.randomUUID();
19896
+ const runId = crypto17.randomUUID();
19652
19897
  app.db.insert(runs).values({
19653
19898
  id: runId,
19654
19899
  projectId: project.id,
@@ -19669,7 +19914,7 @@ async function bingRoutes(app, opts) {
19669
19914
  discoveryDate: result.DiscoveryDate ?? null
19670
19915
  });
19671
19916
  const now = (/* @__PURE__ */ new Date()).toISOString();
19672
- const id = crypto16.randomUUID();
19917
+ const id = crypto17.randomUUID();
19673
19918
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
19674
19919
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
19675
19920
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -19739,7 +19984,7 @@ async function bingRoutes(app, opts) {
19739
19984
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
19740
19985
  }
19741
19986
  const now = (/* @__PURE__ */ new Date()).toISOString();
19742
- const runId = crypto16.randomUUID();
19987
+ const runId = crypto17.randomUUID();
19743
19988
  app.db.insert(runs).values({
19744
19989
  id: runId,
19745
19990
  projectId: project.id,
@@ -20023,7 +20268,7 @@ async function cdpRoutes(app, opts) {
20023
20268
  }
20024
20269
 
20025
20270
  // ../api-routes/src/ga.ts
20026
- import crypto17 from "crypto";
20271
+ import crypto18 from "crypto";
20027
20272
  import { eq as eq22, desc as desc11, and as and16, sql as sql9 } from "drizzle-orm";
20028
20273
  function gaLog(level, action, ctx) {
20029
20274
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
@@ -20279,7 +20524,7 @@ async function ga4Routes(app, opts) {
20279
20524
  const syncAi = !only || only === "ai";
20280
20525
  const syncSocial = !only || only === "social";
20281
20526
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
20282
- const runId = crypto17.randomUUID();
20527
+ const runId = crypto18.randomUUID();
20283
20528
  app.db.insert(runs).values({
20284
20529
  id: runId,
20285
20530
  projectId: project.id,
@@ -20327,7 +20572,7 @@ async function ga4Routes(app, opts) {
20327
20572
  ).run();
20328
20573
  for (const row of rows) {
20329
20574
  tx.insert(gaTrafficSnapshots).values({
20330
- id: crypto17.randomUUID(),
20575
+ id: crypto18.randomUUID(),
20331
20576
  projectId: project.id,
20332
20577
  date: row.date,
20333
20578
  landingPage: row.landingPage,
@@ -20351,7 +20596,7 @@ async function ga4Routes(app, opts) {
20351
20596
  ).run();
20352
20597
  for (const row of aiReferrals) {
20353
20598
  tx.insert(gaAiReferrals).values({
20354
- id: crypto17.randomUUID(),
20599
+ id: crypto18.randomUUID(),
20355
20600
  projectId: project.id,
20356
20601
  date: row.date,
20357
20602
  source: row.source,
@@ -20377,7 +20622,7 @@ async function ga4Routes(app, opts) {
20377
20622
  ).run();
20378
20623
  for (const row of socialReferrals) {
20379
20624
  tx.insert(gaSocialReferrals).values({
20380
- id: crypto17.randomUUID(),
20625
+ id: crypto18.randomUUID(),
20381
20626
  projectId: project.id,
20382
20627
  date: row.date,
20383
20628
  source: row.source,
@@ -20393,7 +20638,7 @@ async function ga4Routes(app, opts) {
20393
20638
  if (syncSummary) {
20394
20639
  tx.delete(gaTrafficSummaries).where(eq22(gaTrafficSummaries.projectId, project.id)).run();
20395
20640
  tx.insert(gaTrafficSummaries).values({
20396
- id: crypto17.randomUUID(),
20641
+ id: crypto18.randomUUID(),
20397
20642
  projectId: project.id,
20398
20643
  periodStart: summary.periodStart,
20399
20644
  periodEnd: summary.periodEnd,
@@ -20406,7 +20651,7 @@ async function ga4Routes(app, opts) {
20406
20651
  tx.delete(gaTrafficWindowSummaries).where(eq22(gaTrafficWindowSummaries.projectId, project.id)).run();
20407
20652
  for (const ws of windowSummaries) {
20408
20653
  tx.insert(gaTrafficWindowSummaries).values({
20409
- id: crypto17.randomUUID(),
20654
+ id: crypto18.randomUUID(),
20410
20655
  projectId: project.id,
20411
20656
  windowKey: ws.windowKey,
20412
20657
  periodStart: ws.periodStart,
@@ -20977,7 +21222,7 @@ function parseSchemaPageEntry(entry) {
20977
21222
  }
20978
21223
 
20979
21224
  // ../integration-wordpress/src/wordpress-client.ts
20980
- import crypto18 from "crypto";
21225
+ import crypto19 from "crypto";
20981
21226
  function validateUsername(username) {
20982
21227
  if (!username || typeof username !== "string" || username.trim().length === 0) {
20983
21228
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -21190,7 +21435,7 @@ function buildSnippet(content) {
21190
21435
  return `${text2.slice(0, 157)}...`;
21191
21436
  }
21192
21437
  function contentHash(content) {
21193
- return crypto18.createHash("sha256").update(content).digest("hex");
21438
+ return crypto19.createHash("sha256").update(content).digest("hex");
21194
21439
  }
21195
21440
  function buildAmbiguousSlugMessage(slug, pages) {
21196
21441
  const candidates = pages.map((page) => {
@@ -22479,7 +22724,7 @@ async function wordpressRoutes(app, opts) {
22479
22724
  }
22480
22725
 
22481
22726
  // ../api-routes/src/backlinks.ts
22482
- import crypto19 from "crypto";
22727
+ import crypto20 from "crypto";
22483
22728
  import { and as and18, asc as asc2, desc as desc12, eq as eq23, sql as sql10 } from "drizzle-orm";
22484
22729
 
22485
22730
  // ../integration-commoncrawl/src/constants.ts
@@ -23059,7 +23304,7 @@ async function backlinksRoutes(app, opts) {
23059
23304
  const refreshed = app.db.select().from(ccReleaseSyncs).where(eq23(ccReleaseSyncs.id, existing.id)).get();
23060
23305
  return reply.status(200).send(mapSyncRow(refreshed));
23061
23306
  }
23062
- const id = crypto19.randomUUID();
23307
+ const id = crypto20.randomUUID();
23063
23308
  app.db.insert(ccReleaseSyncs).values({
23064
23309
  id,
23065
23310
  release,
@@ -23116,7 +23361,7 @@ async function backlinksRoutes(app, opts) {
23116
23361
  throw validationError("Invalid release id");
23117
23362
  }
23118
23363
  const now = (/* @__PURE__ */ new Date()).toISOString();
23119
- const runId = crypto19.randomUUID();
23364
+ const runId = crypto20.randomUUID();
23120
23365
  app.db.insert(runs).values({
23121
23366
  id: runId,
23122
23367
  projectId: project.id,
@@ -23189,12 +23434,12 @@ async function backlinksRoutes(app, opts) {
23189
23434
  }
23190
23435
 
23191
23436
  // ../api-routes/src/traffic.ts
23192
- import crypto21 from "crypto";
23437
+ import crypto22 from "crypto";
23193
23438
  import { Agent as UndiciAgent } from "undici";
23194
23439
  import { and as and19, desc as desc13, eq as eq24, gte as gte3, lte as lte2, sql as sql11 } from "drizzle-orm";
23195
23440
 
23196
23441
  // ../integration-cloud-run/src/auth.ts
23197
- import crypto20 from "crypto";
23442
+ import crypto21 from "crypto";
23198
23443
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
23199
23444
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
23200
23445
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -23223,7 +23468,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
23223
23468
  const headerB64 = encode(header);
23224
23469
  const payloadB64 = encode(payload);
23225
23470
  const signingInput = `${headerB64}.${payloadB64}`;
23226
- const sign = crypto20.createSign("RSA-SHA256");
23471
+ const sign = crypto21.createSign("RSA-SHA256");
23227
23472
  sign.update(signingInput);
23228
23473
  const signature = sign.sign(privateKey, "base64url");
23229
23474
  return `${signingInput}.${signature}`;
@@ -27089,7 +27334,7 @@ async function runBackfillTask(options) {
27089
27334
  }
27090
27335
  })();
27091
27336
  tx.insert(rawEventSamples).values({
27092
- id: crypto21.randomUUID(),
27337
+ id: crypto22.randomUUID(),
27093
27338
  projectId: project.id,
27094
27339
  sourceId: sourceRow.id,
27095
27340
  ts: sample.observedAt,
@@ -27214,7 +27459,7 @@ async function trafficRoutes(app, opts) {
27214
27459
  }).where(eq24(trafficSources.id, activeSource.id)).run();
27215
27460
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
27216
27461
  } else {
27217
- const newId = crypto21.randomUUID();
27462
+ const newId = crypto22.randomUUID();
27218
27463
  app.db.insert(trafficSources).values({
27219
27464
  id: newId,
27220
27465
  projectId: project.id,
@@ -27295,7 +27540,7 @@ async function trafficRoutes(app, opts) {
27295
27540
  }).where(eq24(trafficSources.id, activeSource.id)).run();
27296
27541
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
27297
27542
  } else {
27298
- const newId = crypto21.randomUUID();
27543
+ const newId = crypto22.randomUUID();
27299
27544
  app.db.insert(trafficSources).values({
27300
27545
  id: newId,
27301
27546
  projectId: project.id,
@@ -27378,7 +27623,7 @@ async function trafficRoutes(app, opts) {
27378
27623
  }).where(eq24(trafficSources.id, activeSource.id)).run();
27379
27624
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
27380
27625
  } else {
27381
- const newId = crypto21.randomUUID();
27626
+ const newId = crypto22.randomUUID();
27382
27627
  app.db.insert(trafficSources).values({
27383
27628
  id: newId,
27384
27629
  projectId: project.id,
@@ -27426,7 +27671,7 @@ async function trafficRoutes(app, opts) {
27426
27671
  const windowEnd = /* @__PURE__ */ new Date();
27427
27672
  const startedAt = windowEnd.toISOString();
27428
27673
  const syncStartedAtMs = windowEnd.getTime();
27429
- const runId = crypto21.randomUUID();
27674
+ const runId = crypto22.randomUUID();
27430
27675
  app.db.insert(runs).values({
27431
27676
  id: runId,
27432
27677
  projectId: project.id,
@@ -27755,7 +28000,7 @@ async function trafficRoutes(app, opts) {
27755
28000
  }
27756
28001
  })();
27757
28002
  tx.insert(rawEventSamples).values({
27758
- id: crypto21.randomUUID(),
28003
+ id: crypto22.randomUUID(),
27759
28004
  projectId: project.id,
27760
28005
  sourceId: sourceRow.id,
27761
28006
  ts: sample.observedAt,
@@ -27980,7 +28225,7 @@ async function trafficRoutes(app, opts) {
27980
28225
  };
27981
28226
  }
27982
28227
  const startedAt = windowEnd.toISOString();
27983
- const runId = crypto21.randomUUID();
28228
+ const runId = crypto22.randomUUID();
27984
28229
  app.db.insert(runs).values({
27985
28230
  id: runId,
27986
28231
  projectId: project.id,
@@ -28260,7 +28505,7 @@ async function trafficRoutes(app, opts) {
28260
28505
  }
28261
28506
 
28262
28507
  // ../api-routes/src/doctor/checks/agent.ts
28263
- import crypto22 from "crypto";
28508
+ import crypto23 from "crypto";
28264
28509
  import fs6 from "fs";
28265
28510
  import path7 from "path";
28266
28511
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -28413,7 +28658,7 @@ function isInstalled(dir) {
28413
28658
  }
28414
28659
  function hashInstalledFile(filePath) {
28415
28660
  try {
28416
- return crypto22.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
28661
+ return crypto23.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
28417
28662
  } catch {
28418
28663
  return void 0;
28419
28664
  }
@@ -29007,6 +29252,84 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
29007
29252
  GBP_AUTH_CHECKS.map((check) => [check.id, check])
29008
29253
  );
29009
29254
 
29255
+ // ../api-routes/src/doctor/checks/places.ts
29256
+ import { eq as eq26 } from "drizzle-orm";
29257
+ var apiKeyCheck = {
29258
+ id: "gbp.places.api-key",
29259
+ category: CheckCategories.auth,
29260
+ scope: CheckScopes.project,
29261
+ title: "Google Places API key",
29262
+ run: (ctx) => {
29263
+ if (!ctx.project) {
29264
+ return { status: CheckStatuses.skipped, code: "gbp.places.no-project", summary: "Project context required.", remediation: null };
29265
+ }
29266
+ const cfg = ctx.getPlacesConfig?.();
29267
+ if (!cfg) {
29268
+ return {
29269
+ status: CheckStatuses.skipped,
29270
+ code: "gbp.places.config-unavailable",
29271
+ summary: "Places config is not available in this deployment.",
29272
+ remediation: null
29273
+ };
29274
+ }
29275
+ if (cfg.tier === "off") {
29276
+ return {
29277
+ status: CheckStatuses.skipped,
29278
+ code: "gbp.places.disabled",
29279
+ summary: "Places enrichment is disabled (tier: off).",
29280
+ remediation: "Set `places.tier` to `atmosphere` or `pro` in ~/.canonry/config.yaml to enable it."
29281
+ };
29282
+ }
29283
+ const conn = ctx.googleConnectionStore?.getConnection(ctx.project.canonicalDomain, "gbp");
29284
+ if (!conn) {
29285
+ return {
29286
+ status: CheckStatuses.skipped,
29287
+ code: "gbp.places.no-gbp-connection",
29288
+ summary: "No Google Business Profile connection; Places enrichment does not apply.",
29289
+ remediation: `Connect GBP first: \`canonry gbp connect ${ctx.project.name}\`.`
29290
+ };
29291
+ }
29292
+ if (!cfg.apiKey) {
29293
+ return {
29294
+ status: CheckStatuses.warn,
29295
+ code: "gbp.places.api-key-missing",
29296
+ summary: "No Places API key configured \u2014 GBP-vs-rendered-listing discrepancies cannot be detected.",
29297
+ remediation: "Set `GOOGLE_PLACES_API_KEY` or `places.apiKey` in ~/.canonry/config.yaml. The amenity cross-reference fits the 1,000 free Atmosphere calls/month for a typical operator book.",
29298
+ details: { tier: cfg.tier }
29299
+ };
29300
+ }
29301
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq26(gbpLocations.projectId, ctx.project.id)).all();
29302
+ const selected = rows.filter((r) => r.selected);
29303
+ const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
29304
+ const details = {
29305
+ tier: cfg.tier,
29306
+ refreshIntervalDays: cfg.refreshIntervalDays,
29307
+ selectedLocations: selected.length,
29308
+ locationsWithPlaceId
29309
+ };
29310
+ if (selected.length > 0 && locationsWithPlaceId === 0) {
29311
+ return {
29312
+ status: CheckStatuses.warn,
29313
+ code: "gbp.places.no-place-ids",
29314
+ summary: `None of the ${selected.length} selected location(s) have a Maps place id, so Places enrichment can't run.`,
29315
+ remediation: `Run \`canonry gbp locations discover ${ctx.project.name}\` to capture place ids from location metadata.`,
29316
+ details
29317
+ };
29318
+ }
29319
+ return {
29320
+ status: CheckStatuses.ok,
29321
+ code: "gbp.places.ready",
29322
+ summary: `Places enrichment ready (tier: ${cfg.tier}). ${locationsWithPlaceId}/${selected.length} selected location(s) have a Maps place id.`,
29323
+ remediation: null,
29324
+ details
29325
+ };
29326
+ }
29327
+ };
29328
+ var PLACES_CHECKS = [apiKeyCheck];
29329
+ var PLACES_CHECK_BY_ID = Object.fromEntries(
29330
+ PLACES_CHECKS.map((check) => [check.id, check])
29331
+ );
29332
+
29010
29333
  // ../api-routes/src/doctor/checks/google-auth.ts
29011
29334
  var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
29012
29335
  async function resolveAccessToken(ctx) {
@@ -29414,7 +29737,7 @@ var RUNTIME_STATE_CHECKS = [
29414
29737
  ];
29415
29738
 
29416
29739
  // ../api-routes/src/doctor/checks/traffic-source.ts
29417
- import { and as and21, eq as eq26, gte as gte4, ne as ne4, sql as sql12 } from "drizzle-orm";
29740
+ import { and as and21, eq as eq27, gte as gte4, ne as ne4, sql as sql12 } from "drizzle-orm";
29418
29741
  var RECENT_DATA_WARN_DAYS = 7;
29419
29742
  var RECENT_DATA_FAIL_DAYS = 30;
29420
29743
  function skippedNoProject3() {
@@ -29429,7 +29752,7 @@ function loadProbes(ctx) {
29429
29752
  if (!ctx.project) return [];
29430
29753
  const rows = ctx.db.select().from(trafficSources).where(
29431
29754
  and21(
29432
- eq26(trafficSources.projectId, ctx.project.id),
29755
+ eq27(trafficSources.projectId, ctx.project.id),
29433
29756
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
29434
29757
  )
29435
29758
  ).all();
@@ -29510,7 +29833,7 @@ var recentDataCheck = {
29510
29833
  const recentCrawlers = Number(
29511
29834
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
29512
29835
  and21(
29513
- eq26(crawlerEventsHourly.projectId, ctx.project.id),
29836
+ eq27(crawlerEventsHourly.projectId, ctx.project.id),
29514
29837
  gte4(crawlerEventsHourly.tsHour, warnCutoff)
29515
29838
  )
29516
29839
  ).get()?.total ?? 0
@@ -29518,7 +29841,7 @@ var recentDataCheck = {
29518
29841
  const recentReferrals = Number(
29519
29842
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
29520
29843
  and21(
29521
- eq26(aiReferralEventsHourly.projectId, ctx.project.id),
29844
+ eq27(aiReferralEventsHourly.projectId, ctx.project.id),
29522
29845
  gte4(aiReferralEventsHourly.tsHour, warnCutoff)
29523
29846
  )
29524
29847
  ).get()?.total ?? 0
@@ -29534,7 +29857,7 @@ var recentDataCheck = {
29534
29857
  const olderCrawlers = Number(
29535
29858
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
29536
29859
  and21(
29537
- eq26(crawlerEventsHourly.projectId, ctx.project.id),
29860
+ eq27(crawlerEventsHourly.projectId, ctx.project.id),
29538
29861
  gte4(crawlerEventsHourly.tsHour, failCutoff)
29539
29862
  )
29540
29863
  ).get()?.total ?? 0
@@ -29542,7 +29865,7 @@ var recentDataCheck = {
29542
29865
  const olderReferrals = Number(
29543
29866
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
29544
29867
  and21(
29545
- eq26(aiReferralEventsHourly.projectId, ctx.project.id),
29868
+ eq27(aiReferralEventsHourly.projectId, ctx.project.id),
29546
29869
  gte4(aiReferralEventsHourly.tsHour, failCutoff)
29547
29870
  )
29548
29871
  ).get()?.total ?? 0
@@ -29793,6 +30116,7 @@ var ALL_CHECKS = [
29793
30116
  ...RUNTIME_STATE_CHECKS,
29794
30117
  ...GOOGLE_AUTH_CHECKS,
29795
30118
  ...GBP_AUTH_CHECKS,
30119
+ ...PLACES_CHECKS,
29796
30120
  ...BING_AUTH_CHECKS,
29797
30121
  ...WORDPRESS_PUBLISH_CHECKS,
29798
30122
  ...GA_AUTH_CHECKS,
@@ -29882,6 +30206,7 @@ async function doctorRoutes(app, opts) {
29882
30206
  wordpressConnectionStore: opts.wordpressConnectionStore,
29883
30207
  ga4CredentialStore: opts.ga4CredentialStore,
29884
30208
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
30209
+ getPlacesConfig: opts.getPlacesConfig,
29885
30210
  redirectUri,
29886
30211
  providerSummary: opts.providerSummary,
29887
30212
  trafficSourceValidators: opts.trafficSourceValidators,
@@ -29906,6 +30231,7 @@ async function doctorRoutes(app, opts) {
29906
30231
  wordpressConnectionStore: opts.wordpressConnectionStore,
29907
30232
  ga4CredentialStore: opts.ga4CredentialStore,
29908
30233
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
30234
+ getPlacesConfig: opts.getPlacesConfig,
29909
30235
  redirectUri,
29910
30236
  providerSummary: opts.providerSummary,
29911
30237
  trafficSourceValidators: opts.trafficSourceValidators,
@@ -29917,8 +30243,8 @@ async function doctorRoutes(app, opts) {
29917
30243
  }
29918
30244
 
29919
30245
  // ../api-routes/src/discovery/routes.ts
29920
- import crypto23 from "crypto";
29921
- import { and as and22, desc as desc14, eq as eq27, gte as gte5, inArray as inArray10 } from "drizzle-orm";
30246
+ import crypto24 from "crypto";
30247
+ import { and as and22, desc as desc14, eq as eq28, gte as gte5, inArray as inArray10 } from "drizzle-orm";
29922
30248
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
29923
30249
  async function discoveryRoutes(app, opts) {
29924
30250
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -29951,8 +30277,8 @@ async function discoveryRoutes(app, opts) {
29951
30277
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
29952
30278
  const decision = app.db.transaction((tx) => {
29953
30279
  const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and22(
29954
- eq27(discoverySessions.projectId, project.id),
29955
- eq27(discoverySessions.icpDescription, icpDescription),
30280
+ eq28(discoverySessions.projectId, project.id),
30281
+ eq28(discoverySessions.icpDescription, icpDescription),
29956
30282
  inArray10(discoverySessions.status, [
29957
30283
  DiscoverySessionStatuses.queued,
29958
30284
  DiscoverySessionStatuses.seeding,
@@ -29963,8 +30289,8 @@ async function discoveryRoutes(app, opts) {
29963
30289
  if (existing && existing.runId) {
29964
30290
  return { reused: true, sessionId: existing.id, runId: existing.runId };
29965
30291
  }
29966
- const sessionId = crypto23.randomUUID();
29967
- const runId = crypto23.randomUUID();
30292
+ const sessionId = crypto24.randomUUID();
30293
+ const runId = crypto24.randomUUID();
29968
30294
  tx.insert(discoverySessions).values({
29969
30295
  id: sessionId,
29970
30296
  projectId: project.id,
@@ -30022,7 +30348,7 @@ async function discoveryRoutes(app, opts) {
30022
30348
  const project = resolveProject(app.db, request.params.name);
30023
30349
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
30024
30350
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
30025
- const rows = app.db.select().from(discoverySessions).where(eq27(discoverySessions.projectId, project.id)).orderBy(desc14(discoverySessions.createdAt)).limit(limit).all();
30351
+ const rows = app.db.select().from(discoverySessions).where(eq28(discoverySessions.projectId, project.id)).orderBy(desc14(discoverySessions.createdAt)).limit(limit).all();
30026
30352
  return reply.send(rows.map(serializeSession));
30027
30353
  }
30028
30354
  );
@@ -30030,11 +30356,11 @@ async function discoveryRoutes(app, opts) {
30030
30356
  "/projects/:name/discover/sessions/:id",
30031
30357
  async (request, reply) => {
30032
30358
  const project = resolveProject(app.db, request.params.name);
30033
- const session = app.db.select().from(discoverySessions).where(eq27(discoverySessions.id, request.params.id)).get();
30359
+ const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
30034
30360
  if (!session || session.projectId !== project.id) {
30035
30361
  throw notFound("Discovery session", request.params.id);
30036
30362
  }
30037
- const probeRows = app.db.select().from(discoveryProbes).where(eq27(discoveryProbes.sessionId, session.id)).all();
30363
+ const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
30038
30364
  const detail = {
30039
30365
  ...serializeSession(session),
30040
30366
  probes: probeRows.map(serializeProbe)
@@ -30046,12 +30372,12 @@ async function discoveryRoutes(app, opts) {
30046
30372
  "/projects/:name/discover/sessions/:id/promote",
30047
30373
  async (request, reply) => {
30048
30374
  const project = resolveProject(app.db, request.params.name);
30049
- const session = app.db.select().from(discoverySessions).where(eq27(discoverySessions.id, request.params.id)).get();
30375
+ const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
30050
30376
  if (!session || session.projectId !== project.id) {
30051
30377
  throw notFound("Discovery session", request.params.id);
30052
30378
  }
30053
- const probeRows = app.db.select().from(discoveryProbes).where(eq27(discoveryProbes.sessionId, session.id)).all();
30054
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq27(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
30379
+ const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
30380
+ const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq28(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
30055
30381
  const seenCompetitors = new Set(existingCompetitors);
30056
30382
  const cited = /* @__PURE__ */ new Set();
30057
30383
  const aspirational = /* @__PURE__ */ new Set();
@@ -30080,7 +30406,7 @@ async function discoveryRoutes(app, opts) {
30080
30406
  );
30081
30407
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
30082
30408
  const project = resolveProject(app.db, request.params.name);
30083
- const session = app.db.select().from(discoverySessions).where(eq27(discoverySessions.id, request.params.id)).get();
30409
+ const session = app.db.select().from(discoverySessions).where(eq28(discoverySessions.id, request.params.id)).get();
30084
30410
  if (!session || session.projectId !== project.id) {
30085
30411
  throw notFound("Discovery session", request.params.id);
30086
30412
  }
@@ -30103,7 +30429,7 @@ async function discoveryRoutes(app, opts) {
30103
30429
  const bucketSet = new Set(buckets);
30104
30430
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
30105
30431
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
30106
- const probeRows = app.db.select().from(discoveryProbes).where(eq27(discoveryProbes.sessionId, session.id)).all();
30432
+ const probeRows = app.db.select().from(discoveryProbes).where(eq28(discoveryProbes.sessionId, session.id)).all();
30107
30433
  const candidateQueries = /* @__PURE__ */ new Set();
30108
30434
  for (const probe of probeRows) {
30109
30435
  if (!probe.bucket) continue;
@@ -30111,7 +30437,7 @@ async function discoveryRoutes(app, opts) {
30111
30437
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
30112
30438
  }
30113
30439
  const existingQueries = new Set(
30114
- app.db.select({ query: queries.query }).from(queries).where(eq27(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
30440
+ app.db.select({ query: queries.query }).from(queries).where(eq28(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
30115
30441
  );
30116
30442
  const promotedQueries = [];
30117
30443
  const skippedQueries = [];
@@ -30127,7 +30453,7 @@ async function discoveryRoutes(app, opts) {
30127
30453
  const skippedCompetitors = [];
30128
30454
  if (includeCompetitors) {
30129
30455
  const existingCompetitors = new Set(
30130
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq27(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
30456
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq28(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
30131
30457
  );
30132
30458
  const competitorMap = parseCompetitorMap(session.competitorMap);
30133
30459
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -30146,7 +30472,7 @@ async function discoveryRoutes(app, opts) {
30146
30472
  app.db.transaction((tx) => {
30147
30473
  for (const query of promotedQueries) {
30148
30474
  tx.insert(queries).values({
30149
- id: crypto23.randomUUID(),
30475
+ id: crypto24.randomUUID(),
30150
30476
  projectId: project.id,
30151
30477
  query,
30152
30478
  provenance,
@@ -30155,7 +30481,7 @@ async function discoveryRoutes(app, opts) {
30155
30481
  }
30156
30482
  for (const domain of promotedCompetitors) {
30157
30483
  tx.insert(competitors).values({
30158
- id: crypto23.randomUUID(),
30484
+ id: crypto24.randomUUID(),
30159
30485
  projectId: project.id,
30160
30486
  domain,
30161
30487
  provenance,
@@ -30229,8 +30555,8 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
30229
30555
  }
30230
30556
 
30231
30557
  // ../api-routes/src/discovery/orchestrate.ts
30232
- import crypto24 from "crypto";
30233
- import { eq as eq28 } from "drizzle-orm";
30558
+ import crypto25 from "crypto";
30559
+ import { eq as eq29 } from "drizzle-orm";
30234
30560
  var DEFAULT_DEDUP_THRESHOLD = 0.85;
30235
30561
  var DEFAULT_MAX_PROBES = 100;
30236
30562
  var ABSOLUTE_MAX_PROBES = 500;
@@ -30285,7 +30611,7 @@ async function executeDiscovery(opts) {
30285
30611
  status: DiscoverySessionStatuses.seeding,
30286
30612
  dedupThreshold,
30287
30613
  startedAt
30288
- }).where(eq28(discoverySessions.id, opts.sessionId)).run();
30614
+ }).where(eq29(discoverySessions.id, opts.sessionId)).run();
30289
30615
  const seedResult = await opts.deps.seed({
30290
30616
  project: opts.project,
30291
30617
  icpDescription: opts.icpDescription,
@@ -30305,7 +30631,7 @@ async function executeDiscovery(opts) {
30305
30631
  seedProvider: seedResult.provider,
30306
30632
  seedCountRaw,
30307
30633
  seedCount
30308
- }).where(eq28(discoverySessions.id, opts.sessionId)).run();
30634
+ }).where(eq29(discoverySessions.id, opts.sessionId)).run();
30309
30635
  const probeRows = [];
30310
30636
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
30311
30637
  for (const query of probedCanonicals) {
@@ -30318,7 +30644,7 @@ async function executeDiscovery(opts) {
30318
30644
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
30319
30645
  buckets[bucket]++;
30320
30646
  opts.db.insert(discoveryProbes).values({
30321
- id: crypto24.randomUUID(),
30647
+ id: crypto25.randomUUID(),
30322
30648
  sessionId: opts.sessionId,
30323
30649
  projectId: opts.project.id,
30324
30650
  query,
@@ -30345,7 +30671,7 @@ async function executeDiscovery(opts) {
30345
30671
  wastedCount: buckets["wasted-surface"],
30346
30672
  competitorMap,
30347
30673
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
30348
- }).where(eq28(discoverySessions.id, opts.sessionId)).run();
30674
+ }).where(eq29(discoverySessions.id, opts.sessionId)).run();
30349
30675
  return {
30350
30676
  buckets,
30351
30677
  competitorMap,
@@ -30359,7 +30685,7 @@ function markSessionFailed(db, sessionId, error) {
30359
30685
  status: DiscoverySessionStatuses.failed,
30360
30686
  error,
30361
30687
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
30362
- }).where(eq28(discoverySessions.id, sessionId)).run();
30688
+ }).where(eq29(discoverySessions.id, sessionId)).run();
30363
30689
  }
30364
30690
  function dedupeStrings(input) {
30365
30691
  const seen = /* @__PURE__ */ new Set();
@@ -30538,6 +30864,7 @@ async function apiRoutes(app, opts) {
30538
30864
  wordpressConnectionStore: opts.wordpressConnectionStore,
30539
30865
  ga4CredentialStore: opts.ga4CredentialStore,
30540
30866
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
30867
+ getPlacesConfig: opts.getPlacesConfig,
30541
30868
  publicUrl: opts.publicUrl,
30542
30869
  providerSummary: opts.providerSummary,
30543
30870
  trafficSourceValidators: buildTrafficSourceValidators(opts),
@@ -30686,7 +31013,7 @@ function buildTrafficSourceValidators(opts) {
30686
31013
  }
30687
31014
 
30688
31015
  // src/intelligence-service.ts
30689
- import crypto25 from "crypto";
31016
+ import crypto26 from "crypto";
30690
31017
 
30691
31018
  // src/logger.ts
30692
31019
  var IS_TTY = process.stdout.isTTY === true;
@@ -30907,8 +31234,8 @@ var IntelligenceService = class {
30907
31234
  analyzeAndPersist(runId, projectId) {
30908
31235
  const recentRuns = this.db.select().from(runs).where(
30909
31236
  and23(
30910
- eq29(runs.projectId, projectId),
30911
- or5(eq29(runs.status, "completed"), eq29(runs.status, "partial")),
31237
+ eq30(runs.projectId, projectId),
31238
+ or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
30912
31239
  // Defensive: RunCoordinator already skips probes before this is
30913
31240
  // called, but if a future call site invokes analyzeAndPersist
30914
31241
  // directly for a probe, probes still must not pollute the
@@ -30990,7 +31317,7 @@ var IntelligenceService = class {
30990
31317
  * Returns the persisted insights so the coordinator can count critical/high.
30991
31318
  */
30992
31319
  analyzeAndPersistGbp(runId, projectId) {
30993
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq29(runs.id, runId)).get();
31320
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq30(runs.id, runId)).get();
30994
31321
  if (!runRow) {
30995
31322
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
30996
31323
  this.persistGbpInsights(runId, projectId, []);
@@ -30999,8 +31326,8 @@ var IntelligenceService = class {
30999
31326
  const windowStart = runRow.startedAt ?? runRow.createdAt;
31000
31327
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
31001
31328
  const selected = this.db.select().from(gbpLocations).where(and23(
31002
- eq29(gbpLocations.projectId, projectId),
31003
- eq29(gbpLocations.selected, true),
31329
+ eq30(gbpLocations.projectId, projectId),
31330
+ eq30(gbpLocations.selected, true),
31004
31331
  gte6(gbpLocations.syncedAt, windowStart),
31005
31332
  lte3(gbpLocations.syncedAt, windowEnd)
31006
31333
  )).all();
@@ -31034,9 +31361,11 @@ var IntelligenceService = class {
31034
31361
  }
31035
31362
  /** Build the per-location signal bundle the GBP analyzer consumes. */
31036
31363
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
31037
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and23(eq29(gbpDailyMetrics.projectId, projectId), eq29(gbpDailyMetrics.locationName, locationName))).all();
31038
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(eq29(gbpPlaceActions.projectId, projectId), eq29(gbpPlaceActions.locationName, locationName))).all();
31039
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and23(eq29(gbpLodgingSnapshots.projectId, projectId), eq29(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc15(gbpLodgingSnapshots.syncedAt)).limit(1).get();
31364
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and23(eq30(gbpDailyMetrics.projectId, projectId), eq30(gbpDailyMetrics.locationName, locationName))).all();
31365
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(eq30(gbpPlaceActions.projectId, projectId), eq30(gbpPlaceActions.locationName, locationName))).all();
31366
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and23(eq30(gbpLodgingSnapshots.projectId, projectId), eq30(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc15(gbpLodgingSnapshots.syncedAt)).limit(1).get();
31367
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and23(eq30(gbpPlaceDetails.projectId, projectId), eq30(gbpPlaceDetails.locationName, locationName))).orderBy(desc15(gbpPlaceDetails.syncedAt)).limit(1).get();
31368
+ const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
31040
31369
  const referenceDate = metricRows.reduce((max, r) => r.date > max ? r.date : max, "") || fallbackDate;
31041
31370
  const summary = buildGbpSummary({
31042
31371
  locationName,
@@ -31057,6 +31386,7 @@ var IntelligenceService = class {
31057
31386
  metricDeltaPct: summary.performance.deltaPct,
31058
31387
  lodgingCapable: summary.lodging.lodgingLocationCount > 0,
31059
31388
  lodgingEmpty: summary.lodging.emptyLodgingCount > 0,
31389
+ placesAmenities,
31060
31390
  placeActionCount: summary.placeActions.total,
31061
31391
  hasDirectMerchantCta: summary.placeActions.hasDirectMerchantCta,
31062
31392
  keywordRecentMonth: trend.recentMonth,
@@ -31067,7 +31397,7 @@ var IntelligenceService = class {
31067
31397
  /** Build the month-over-month keyword series for a location from the
31068
31398
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
31069
31399
  buildGbpKeywordTrend(projectId, locationName) {
31070
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and23(eq29(gbpKeywordMonthly.projectId, projectId), eq29(gbpKeywordMonthly.locationName, locationName))).all();
31400
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and23(eq30(gbpKeywordMonthly.projectId, projectId), eq30(gbpKeywordMonthly.locationName, locationName))).all();
31071
31401
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
31072
31402
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
31073
31403
  const recentMonth = months[0] ?? null;
@@ -31093,12 +31423,12 @@ var IntelligenceService = class {
31093
31423
  */
31094
31424
  persistGbpInsights(runId, projectId, gbpInsights) {
31095
31425
  const previouslyDismissed = /* @__PURE__ */ new Set();
31096
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(eq29(insights.runId, runId)).all();
31426
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(eq30(insights.runId, runId)).all();
31097
31427
  for (const row of existing) {
31098
31428
  if (row.dismissed) previouslyDismissed.add(row.id);
31099
31429
  }
31100
31430
  this.db.transaction((tx) => {
31101
- tx.delete(insights).where(eq29(insights.runId, runId)).run();
31431
+ tx.delete(insights).where(eq30(insights.runId, runId)).run();
31102
31432
  for (const insight of gbpInsights) {
31103
31433
  tx.insert(insights).values({
31104
31434
  id: insight.id,
@@ -31173,7 +31503,7 @@ var IntelligenceService = class {
31173
31503
  * create per run + aggregate). DB is left untouched.
31174
31504
  */
31175
31505
  backfill(projectName, opts, onProgress) {
31176
- const project = this.db.select().from(projects).where(eq29(projects.name, projectName)).get();
31506
+ const project = this.db.select().from(projects).where(eq30(projects.name, projectName)).get();
31177
31507
  if (!project) {
31178
31508
  throw new Error(`Project "${projectName}" not found`);
31179
31509
  }
@@ -31187,8 +31517,8 @@ var IntelligenceService = class {
31187
31517
  }
31188
31518
  const allRuns = this.db.select().from(runs).where(
31189
31519
  and23(
31190
- eq29(runs.projectId, project.id),
31191
- or5(eq29(runs.status, "completed"), eq29(runs.status, "partial")),
31520
+ eq30(runs.projectId, project.id),
31521
+ or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
31192
31522
  // Backfill must not replay probe runs as if they were real sweeps.
31193
31523
  ne5(runs.trigger, RunTriggers.probe)
31194
31524
  )
@@ -31267,7 +31597,7 @@ var IntelligenceService = class {
31267
31597
  return { processed, skipped, totalInsights };
31268
31598
  }
31269
31599
  loadTrackedCompetitors(projectId) {
31270
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq29(competitors.projectId, projectId)).all().map((r) => r.domain);
31600
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq30(competitors.projectId, projectId)).all().map((r) => r.domain);
31271
31601
  }
31272
31602
  /**
31273
31603
  * Wipe transition signals from an analysis result while keeping health.
@@ -31288,15 +31618,15 @@ var IntelligenceService = class {
31288
31618
  }
31289
31619
  persistResult(result, runId, projectId) {
31290
31620
  const previouslyDismissed = /* @__PURE__ */ new Set();
31291
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq29(insights.runId, runId)).all();
31621
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq30(insights.runId, runId)).all();
31292
31622
  for (const row of existingInsights) {
31293
31623
  if (row.dismissed) {
31294
31624
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
31295
31625
  }
31296
31626
  }
31297
31627
  this.db.transaction((tx) => {
31298
- tx.delete(insights).where(eq29(insights.runId, runId)).run();
31299
- tx.delete(healthSnapshots).where(eq29(healthSnapshots.runId, runId)).run();
31628
+ tx.delete(insights).where(eq30(insights.runId, runId)).run();
31629
+ tx.delete(healthSnapshots).where(eq30(healthSnapshots.runId, runId)).run();
31300
31630
  const now = (/* @__PURE__ */ new Date()).toISOString();
31301
31631
  for (const insight of result.insights) {
31302
31632
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -31316,7 +31646,7 @@ var IntelligenceService = class {
31316
31646
  }).run();
31317
31647
  }
31318
31648
  tx.insert(healthSnapshots).values({
31319
- id: crypto25.randomUUID(),
31649
+ id: crypto26.randomUUID(),
31320
31650
  projectId,
31321
31651
  runId,
31322
31652
  overallCitedRate: String(result.health.overallCitedRate),
@@ -31347,14 +31677,14 @@ var IntelligenceService = class {
31347
31677
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
31348
31678
  const regressions = rawInsights.filter((i) => i.type === "regression");
31349
31679
  if (regressions.length === 0) return rawInsights;
31350
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq29(gscSearchData.projectId, projectId)).all();
31680
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq30(gscSearchData.projectId, projectId)).all();
31351
31681
  const gscConnected = gscRows.length > 0;
31352
31682
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
31353
31683
  for (const row of gscRows) {
31354
31684
  const key = row.query.toLowerCase();
31355
31685
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
31356
31686
  }
31357
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq29(projects.id, projectId)).get();
31687
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq30(projects.id, projectId)).get();
31358
31688
  const locationCount = Math.max(
31359
31689
  1,
31360
31690
  (projectRow?.locations ?? []).length
@@ -31362,9 +31692,9 @@ var IntelligenceService = class {
31362
31692
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
31363
31693
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
31364
31694
  and23(
31365
- eq29(runs.projectId, projectId),
31366
- eq29(runs.kind, RunKinds["answer-visibility"]),
31367
- or5(eq29(runs.status, "completed"), eq29(runs.status, "partial")),
31695
+ eq30(runs.projectId, projectId),
31696
+ eq30(runs.kind, RunKinds["answer-visibility"]),
31697
+ or5(eq30(runs.status, "completed"), eq30(runs.status, "partial")),
31368
31698
  // Defensive — see top of file.
31369
31699
  ne5(runs.trigger, RunTriggers.probe)
31370
31700
  )
@@ -31384,7 +31714,7 @@ var IntelligenceService = class {
31384
31714
  const haveHistory = recentRunIds.length > 0;
31385
31715
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
31386
31716
  if (haveHistory) {
31387
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and23(eq29(insights.type, "regression"), inArray11(insights.runId, recentRunIds))).all();
31717
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and23(eq30(insights.type, "regression"), inArray11(insights.runId, recentRunIds))).all();
31388
31718
  const regressionGroups = /* @__PURE__ */ new Map();
31389
31719
  for (const row of priorRows) {
31390
31720
  if (!row.runId) continue;
@@ -31413,7 +31743,7 @@ var IntelligenceService = class {
31413
31743
  });
31414
31744
  }
31415
31745
  buildRunData(runId, projectId, completedAt, location = null) {
31416
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq29(projects.id, projectId)).get();
31746
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq30(projects.id, projectId)).get();
31417
31747
  const projectDomains = projectDomainRow ? effectiveDomains({
31418
31748
  canonicalDomain: projectDomainRow.canonicalDomain,
31419
31749
  ownedDomains: projectDomainRow.ownedDomains
@@ -31429,7 +31759,7 @@ var IntelligenceService = class {
31429
31759
  citedDomains: querySnapshots.citedDomains,
31430
31760
  competitorOverlap: querySnapshots.competitorOverlap,
31431
31761
  snapshotLocation: querySnapshots.location
31432
- }).from(querySnapshots).leftJoin(queries, eq29(querySnapshots.queryId, queries.id)).where(eq29(querySnapshots.runId, runId)).all();
31762
+ }).from(querySnapshots).leftJoin(queries, eq30(querySnapshots.queryId, queries.id)).where(eq30(querySnapshots.runId, runId)).all();
31433
31763
  const snapshots = [];
31434
31764
  let orphanCount = 0;
31435
31765
  for (const r of rows) {
@@ -31501,6 +31831,7 @@ export {
31501
31831
  gbpKeywordMonthly,
31502
31832
  gbpPlaceActions,
31503
31833
  gbpLodgingSnapshots,
31834
+ gbpPlaceDetails,
31504
31835
  createClient,
31505
31836
  parseJsonColumn,
31506
31837
  extractLegacyCredentials,
@@ -31512,6 +31843,8 @@ export {
31512
31843
  determineCitationState,
31513
31844
  computeCompetitorOverlap,
31514
31845
  extractRecommendedCompetitors,
31846
+ getPlaceDetails,
31847
+ hashPlaceDetails,
31515
31848
  queueRunIfProjectIdle,
31516
31849
  resolveWebhookTarget,
31517
31850
  deliverWebhook,