@canonry/canonry 7.61.2 → 7.62.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +9 -2
  2. package/assets/agent-workspace/skills/aero/references/ai-traffic-tracking.md +152 -0
  3. package/assets/agent-workspace/skills/canonry/SKILL.md +1 -0
  4. package/assets/agent-workspace/skills/canonry/references/ai-traffic-tracking.md +145 -0
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +8 -4
  6. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +14 -3
  7. package/assets/assets/{AgentSetupPage-CCMNQwRE.js → AgentSetupPage-DtPnVaeF.js} +1 -1
  8. package/assets/assets/{AuditHistoryPanel-B8mIRI3I.js → AuditHistoryPanel-Ce_oy4OV.js} +1 -1
  9. package/assets/assets/{BacklinksPage-BNLyIR97.js → BacklinksPage-CrqPWRd8.js} +1 -1
  10. package/assets/assets/{HistoryPage-DSr9KE16.js → HistoryPage-5v0JM7DN.js} +1 -1
  11. package/assets/assets/{MeasurementPropertyPage-BTg-SFW7.js → MeasurementPropertyPage-Bm5Mrd5a.js} +1 -1
  12. package/assets/assets/{ProjectPage-SqE0fkEq.js → ProjectPage-DaWUh6iw.js} +1 -1
  13. package/assets/assets/{RunsPage-gJR17ho1.js → RunsPage-CfdAR2v2.js} +1 -1
  14. package/assets/assets/{SettingsPage-DY5f_oN4.js → SettingsPage-BFooA7JG.js} +1 -1
  15. package/assets/assets/{SiteHealthSection-BYx91hP-.js → SiteHealthSection-CerUQgAp.js} +2 -2
  16. package/assets/assets/{ToneBadge-C3S7aVf3.js → ToneBadge-DKvZcONW.js} +1 -1
  17. package/assets/assets/{TrafficPage-CCXH5nhh.js → TrafficPage-C5fhZGJ2.js} +1 -1
  18. package/assets/assets/{TrafficSourceDetailPage-DTjCsu4-.js → TrafficSourceDetailPage-BoqARHYz.js} +1 -1
  19. package/assets/assets/{account-context-DozTQM44.js → account-context-D_CDYrs8.js} +1 -1
  20. package/assets/assets/{card-D8MU7RYV.js → card-C6z0XyOo.js} +1 -1
  21. package/assets/assets/{extract-error-message-CsVYPTyt.js → extract-error-message-By_wcU8g.js} +1 -1
  22. package/assets/assets/{format-helpers-CWBooMJJ.js → format-helpers-D3Ubm8vk.js} +1 -1
  23. package/assets/assets/{index-D5o-caTl.js → index-DSVFZqv6.js} +4 -4
  24. package/assets/assets/{query-client-DbxlHfUm.js → query-client-BETh6SO9.js} +1 -1
  25. package/assets/assets/{react-query-D4ZSMg7h.js → react-query-DzDr4w41.js} +1 -1
  26. package/assets/assets/{react-sigma_core.esm.min-CSRp4hew.js → react-sigma_core.esm.min-3kgtOWbQ.js} +1 -1
  27. package/assets/assets/{service-CYzJmCXU.js → service-BUaUaize.js} +1 -1
  28. package/assets/index.html +7 -7
  29. package/dist/{ads-Z2SNR2F5.js → ads-7WUY5CVC.js} +3 -3
  30. package/dist/{agent-UUDYXXRD.js → agent-NOA33LVM.js} +7 -7
  31. package/dist/{agent-ask-KCZKPBY7.js → agent-ask-PKQDFJ7K.js} +3 -3
  32. package/dist/{agent-conversations-CDQA7HOU.js → agent-conversations-7HVWPSFB.js} +3 -3
  33. package/dist/{agent-memory-MER75L4L.js → agent-memory-Y4M2EWKA.js} +3 -3
  34. package/dist/{agent-providers-RH7W5HYA.js → agent-providers-P67FWELV.js} +3 -3
  35. package/dist/{agent-transcript-VU2VHSZ6.js → agent-transcript-C2BYWPRT.js} +3 -3
  36. package/dist/{analytics-BDVOFX6O.js → analytics-XHYWNNW7.js} +3 -3
  37. package/dist/{apply-XJULCLGT.js → apply-6OIWDXQP.js} +3 -3
  38. package/dist/{backfill-7I5BMA6R.js → backfill-KQPXCUUJ.js} +9 -9
  39. package/dist/{backlinks-HCSL6KKT.js → backlinks-ASYIZFLQ.js} +3 -3
  40. package/dist/{bing-AEUPBLV6.js → bing-ZLKJ3MIL.js} +3 -3
  41. package/dist/{bootstrap-J73OTQWI.js → bootstrap-KMVQY5JD.js} +10 -10
  42. package/dist/{cdp-2UQER2VR.js → cdp-V2HX4ZLE.js} +7 -7
  43. package/dist/{chunk-5STJ5LXF.js → chunk-2HNTNKFC.js} +157 -57
  44. package/dist/{chunk-CZSHCZHW.js → chunk-2LY4FY4N.js} +1 -1
  45. package/dist/{chunk-COUDJ44G.js → chunk-3B2GBWIA.js} +5 -5
  46. package/dist/{chunk-YHTU4XBR.js → chunk-5NYS3ABL.js} +2 -2
  47. package/dist/{chunk-H6QJN6RQ.js → chunk-7EWDQUNH.js} +8 -8
  48. package/dist/{chunk-XEM6TO4X.js → chunk-AO4P75DI.js} +5 -5
  49. package/dist/{chunk-GAA7JGB7.js → chunk-APQ5EAL2.js} +2 -2
  50. package/dist/{chunk-VHHFAAMI.js → chunk-B5CSCGVD.js} +1 -1
  51. package/dist/{chunk-SQ6AOE57.js → chunk-CJC4AQ2M.js} +2 -2
  52. package/dist/{chunk-VCSJHXW6.js → chunk-DAKJBAUN.js} +1 -1
  53. package/dist/{chunk-B5KK54RE.js → chunk-GSHHYR24.js} +3 -3
  54. package/dist/{chunk-KLB3CTOQ.js → chunk-I3CMCTNE.js} +4 -4
  55. package/dist/{chunk-P5DZ56CR.js → chunk-IALK4DQJ.js} +1 -1
  56. package/dist/{chunk-FWRHDY4R.js → chunk-IVZMXHXE.js} +1 -1
  57. package/dist/{chunk-EKY6GHJ4.js → chunk-KLGHY67V.js} +4 -4
  58. package/dist/{chunk-XCA546YC.js → chunk-KTQVRQDM.js} +1 -1
  59. package/dist/{chunk-KX5IRN55.js → chunk-LAILXR2F.js} +1 -1
  60. package/dist/{chunk-RWHKTH75.js → chunk-LFXKB5FZ.js} +333 -333
  61. package/dist/{chunk-MRNMDQRB.js → chunk-MMRJUOE2.js} +1 -1
  62. package/dist/{chunk-OWEIOEOY.js → chunk-O6VSPM5S.js} +20 -20
  63. package/dist/{chunk-5PYPNDIE.js → chunk-OJVXKPIN.js} +3 -3
  64. package/dist/{chunk-7VEXNHRX.js → chunk-Q7PTBHNG.js} +1 -1
  65. package/dist/{chunk-HD7AZIOP.js → chunk-RGD5SDT3.js} +17 -1
  66. package/dist/{chunk-SZXYGJMI.js → chunk-SDZKHJ6W.js} +2 -2
  67. package/dist/{chunk-34HGKT6I.js → chunk-SYQLUOR7.js} +1 -1
  68. package/dist/{chunk-G3ZNUINX.js → chunk-U2WPZZPQ.js} +1 -1
  69. package/dist/{chunk-IG6ISQYD.js → chunk-U6CNJC2Y.js} +6310 -6261
  70. package/dist/{chunk-ZB3YVCBL.js → chunk-UUGT7UMF.js} +4 -4
  71. package/dist/{chunk-SA4CG775.js → chunk-UVDBUMYV.js} +2 -2
  72. package/dist/{chunk-W5FR6AJP.js → chunk-WKZZILBI.js} +2 -2
  73. package/dist/{chunk-3A7LI5KI.js → chunk-X3GWBK7N.js} +3 -3
  74. package/dist/{chunk-5AV2OUCJ.js → chunk-YBDUZBGU.js} +1 -1
  75. package/dist/{chunk-TYWTIYYM.js → chunk-YJAN2ZDV.js} +3 -3
  76. package/dist/{chunk-RL7LPPFU.js → chunk-YQGVA64Q.js} +3 -3
  77. package/dist/{citations-3X5EJNJ3.js → citations-NOCVQPPU.js} +3 -3
  78. package/dist/{cli-catalog-F3IOEPJK.js → cli-catalog-WOWYZ7Q2.js} +3 -3
  79. package/dist/{cli-commands-NVNMM4IE.js → cli-commands-VSS42S5I.js} +7 -7
  80. package/dist/cli.js +13 -13
  81. package/dist/{competitor-CUPGN4NZ.js → competitor-KQK66W3M.js} +3 -3
  82. package/dist/{content-FYQT7V5A.js → content-OHOQO5LP.js} +3 -3
  83. package/dist/{dashboard-4AEKE4XK.js → dashboard-W6MII62Z.js} +6 -6
  84. package/dist/{demo-GLYZQOTJ.js → demo-23H25IOO.js} +1 -1
  85. package/dist/{demo-server-24RESPMP.js → demo-server-WYYWTH4H.js} +11 -11
  86. package/dist/{discover-4IGXLRRE.js → discover-557WRGEK.js} +3 -3
  87. package/dist/{discover-eval-DGLZO5MD.js → discover-eval-GGRZZHHI.js} +3 -3
  88. package/dist/{doctor-3MFTUIEU.js → doctor-D5JNJKVE.js} +3 -3
  89. package/dist/{evidence-ANE4SFP5.js → evidence-DHYYVPWW.js} +3 -3
  90. package/dist/{export-cmd-X6S5F463.js → export-cmd-M24S3YBY.js} +3 -3
  91. package/dist/{ga-2UKFAVDJ.js → ga-XM6WXZNP.js} +3 -3
  92. package/dist/{gbp-Q3V3SQMV.js → gbp-M3S5XYPG.js} +3 -3
  93. package/dist/{google-D4O4JCM4.js → google-PULCSUOU.js} +4 -4
  94. package/dist/{google-marketing-6L5327YK.js → google-marketing-AA625HA4.js} +1 -1
  95. package/dist/{health-cmd-7TPQHGKK.js → health-cmd-LVEW6OBD.js} +3 -3
  96. package/dist/{history-NNUOPNA7.js → history-32ZYGZGQ.js} +3 -3
  97. package/dist/index.d.ts +56 -9
  98. package/dist/index.js +24 -24
  99. package/dist/{init-U7MG23AF.js → init-VJDDRCBV.js} +11 -11
  100. package/dist/{insights-DDAP3MRC.js → insights-RK3MJ4UM.js} +3 -3
  101. package/dist/intelligence-service-UNKFCG5N.js +15 -0
  102. package/dist/{journeys-I7H25PPF.js → journeys-S4CPNZYU.js} +3 -3
  103. package/dist/{keys-YUJNNNV3.js → keys-WCXE7WXV.js} +3 -3
  104. package/dist/{keyword-25DDYT45.js → keyword-HHVLC4HI.js} +3 -3
  105. package/dist/{logs-FKPJ4L5R.js → logs-VYESWGAN.js} +3 -3
  106. package/dist/{mcp-ZPLVNLL3.js → mcp-4LRFIOLC.js} +2 -2
  107. package/dist/mcp.js +9 -9
  108. package/dist/{measurement-plan-CDY63CLU.js → measurement-plan-AL3EHWRK.js} +5 -5
  109. package/dist/{notify-67IIWOSF.js → notify-LUDG5PWU.js} +3 -3
  110. package/dist/{organic-evidence-P2BQK2BV.js → organic-evidence-CIOHMKLD.js} +3 -3
  111. package/dist/{overview-WHSTSNS2.js → overview-NAZJ6PJG.js} +4 -4
  112. package/dist/{project-7AMXFZMD.js → project-N3MU5V6T.js} +3 -3
  113. package/dist/{query-RLKE6JHY.js → query-XMI2FV6G.js} +3 -3
  114. package/dist/{research-PDOE77HZ.js → research-MQ6QA5TV.js} +3 -3
  115. package/dist/{results-export-5VKTQLOR.js → results-export-OFEZXCJK.js} +3 -3
  116. package/dist/{run-QXCB4AWO.js → run-YQS3LCFT.js} +4 -4
  117. package/dist/{schedule-NDM2GBBW.js → schedule-DF6HCMXN.js} +3 -3
  118. package/dist/{search-DDRSJUWZ.js → search-MRDJKVOL.js} +3 -3
  119. package/dist/{serve-U6ZKXTKN.js → serve-OCAFOFS2.js} +25 -25
  120. package/dist/{session-PPYDP5UU.js → session-5OXLS7NP.js} +12 -12
  121. package/dist/{settings-Q2ZOEA7H.js → settings-NIF34QPH.js} +3 -3
  122. package/dist/{skills-PKSEX44J.js → skills-JOSNWG7B.js} +2 -2
  123. package/dist/{snapshot-URNBVDTA.js → snapshot-32DLVRIJ.js} +3 -3
  124. package/dist/{src-MAMJVGSJ.js → src-F63SHN5T.js} +2 -2
  125. package/dist/{status-DDXVX4KE.js → status-IERHHP4C.js} +4 -4
  126. package/dist/{technical-aeo-OS6KQXAP.js → technical-aeo-D7CA3CK6.js} +3 -3
  127. package/dist/{telemetry-G7GX2LIY.js → telemetry-DRURFCCJ.js} +4 -4
  128. package/dist/{tools-OOAF75Y5.js → tools-3IZMSHSY.js} +11 -11
  129. package/dist/{traffic-A4QW7455.js → traffic-AME5NDMN.js} +27 -5
  130. package/dist/{users-4WPDDCCR.js → users-4CQ6ATAG.js} +3 -3
  131. package/dist/{visibility-stats-5BY7GVUV.js → visibility-stats-R5JBISBX.js} +3 -3
  132. package/dist/{wordpress-T5EYN2WX.js → wordpress-GFK4GR3O.js} +3 -3
  133. package/package.json +1 -1
  134. package/dist/intelligence-service-ZJ5X6J3A.js +0 -15
@@ -177,12 +177,18 @@ estimate; a candidate burst is not confirmed automation. Read the Cloudflare
177
177
  `traffic sync`. Queue pull freshness requires an enabled `traffic-sync`
178
178
  schedule. Run the `traffic.source.*` doctor checks. Inspect
179
179
  `traffic.source.queue-backlog` before you quote current Queue data. If more than
180
- 1,000 messages remain, report that the drain hit its time limit before the Queue
181
- was empty.
180
+ 1,000 messages remain, report the cause the check names from the last sync's
181
+ stop reason (`cnry runs <project> --kind traffic-sync --format json`).
182
182
  Get approval before you run a manual sync or change the schedule. The full
183
183
  command reference is in the co-installed
184
184
  `canonry/references/canonry-cli.md`.
185
185
 
186
+ When someone asks whether AI search sent visitors, whether they came back, or
187
+ whether they bought, booked or became leads, read `references/ai-traffic-tracking.md`
188
+ first. Report crawled, read, clicked, came back and converted as separate
189
+ numbers, and never report zero conversions until you have checked that the
190
+ conversion is tracked and reaches GA4.
191
+
186
192
  **Diagnosing a stuck Vercel/Cloud Run source:** if `cnry traffic status` shows `status=error` with a recent `lastError` of `refusing to advance` or `ExceedsBillingLimitError`, the source's `lastSyncedAt` has aged past the upstream retention boundary and every sync now throws. Recovery: `cnry traffic reset <project> --source <id> --advance-to-now`. This advances `lastSyncedAt` to NOW and resumes going-forward syncs — historical events in the gap are unrecoverable from the sync path; run `cnry traffic backfill --days N` separately if any of that history is needed (capped at retention).
187
193
 
188
194
  ## Judgment Rules
@@ -260,6 +266,7 @@ Detailed playbooks live alongside this file. Read them on demand when the task m
260
266
  | `references/aeo-discovery.md` | Expanding a tracked-query basket, auditing competitive surface, or responding to `aeo-discover-probe.completed` |
261
267
  | `references/memory-patterns.md` | Deciding whether to remember a fact in agent memory or re-query canonry |
262
268
  | `references/reporting.md` | Producing a client-facing weekly or monthly summary |
269
+ | `references/ai-traffic-tracking.md` | Answering whether AI traffic led to visits, return visits, leads, bookings or sales, or whether tracking is broken; generated from the Canonry reference |
263
270
  | `references/wordpress-elementor-mcp.md` | Editing WordPress pages with the Elementor MCP integration |
264
271
 
265
272
  Aero (canonry's built-in agent) exposes `list_skill_docs` / `read_skill_doc` tools that walk this directory programmatically. External agents (Claude Code, Codex) can read the files directly.
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: ai-traffic-tracking
3
+ description: "Crawled, read, clicked, came back, converted: which instrument measures each step, the checks to run before reporting a zero, attribution limits, and user stories such as whether anyone from AI search bought or booked. Read when asked about AI traffic, returning AI visitors, conversions, or broken tracking."
4
+ ---
5
+
6
+ <!-- Generated from skills/canonry/references/ai-traffic-tracking.md by pnpm guide:sync. Do not edit. -->
7
+
8
+ # Tracking AI traffic to visits and conversions
9
+
10
+ Read this before you answer whether AI search sent visitors, whether those
11
+ visitors came back, or whether they bought, booked or became leads. Read it too
12
+ when someone says "our tracking is broken" or "AI referrals dropped".
13
+
14
+ Everything here is a stored read unless it says otherwise. Changing a setting,
15
+ connecting a source, a backfill or a GA sync each need approval for that exact
16
+ action.
17
+
18
+ ## The funnel and what measures each step
19
+
20
+ Each step is usually 10 to 100 times smaller than the one above it. Most AI
21
+ answers are read inside the assistant, so a site can be read often and clicked
22
+ rarely. Report every step separately; never pool them.
23
+
24
+ | Step | What it is | Instrument | Read with |
25
+ |---|---|---|---|
26
+ | Crawled | AI and search bots fetch pages to index them | Server-side traffic, `crawler` | `cnry traffic events <p> --kind crawler` (`canonry_traffic_events`) |
27
+ | Read for a user | An assistant fetches a page live while answering someone (ChatGPT-User, Claude-User, Perplexity-User) | Server-side traffic, `ai-user-fetch` | `cnry traffic events <p> --kind ai-user-fetch` |
28
+ | Clicked | A person follows a link from an AI answer | Server-side `ai-referral` hits; GA4 AI sessions | `cnry traffic events <p> --kind ai-referral`; `cnry ga ai-referral-history <p>` (`canonry_ga_ai_referral_history`) |
29
+ | Came back | That person's later visits, from any channel | AI visitor journeys (server logs) | `cnry journeys summary\|list\|show <p>` (`canonry_journeys_summary`, `canonry_journeys_list`, `canonry_journey_get`) |
30
+ | Converted | Lead, booking or purchase | GA4 events named in `measurement.leadEventNames` | `cnry ga measurement-analysis <p> --window 90d --host-scope all` (`canonry_ga_measurement_analysis`) |
31
+
32
+ Server-side hits are requests and GA4 counts sessions, and each instrument
33
+ misses some visits the other sees, so the two never match exactly. Compare
34
+ their trend, not their totals.
35
+
36
+ ## Check the instrument before you report a zero
37
+
38
+ A zero means nothing until you know the instrument could have seen it. Work
39
+ through these in order and say which one applies.
40
+
41
+ 1. **Is the conversion tracked at all?** Read the project's
42
+ `measurement.leadEventNames` (`cnry project show <p> --format json`). The
43
+ default is only `generate_lead`. Online stores report sales as `purchase`,
44
+ and booking engines often do too. If lead events are zero across every
45
+ channel, not only AI, the instrument is blind: report "conversions are not
46
+ measured", never "AI drove no conversions". Adding an event is a setting
47
+ change (`cnry project update <p> --add-lead-event purchase`) and needs
48
+ approval; the daily refresh then fills about 30 days.
49
+ 2. **Does GA4 receive the event?** Read the stored GTM snapshot
50
+ (`cnry gtm snapshots <p>`, then `cnry gtm snapshot <p> <id>`;
51
+ `canonry_gtm_snapshot_get`). If the booking or purchase trigger fires only
52
+ Google Ads tags (`awct`) and no GA4 event tag, GA4 records no conversions.
53
+ The fix is a GA4 Event tag on the same trigger, made by someone with
54
+ publish rights in the site's GTM container. Canonry's GTM access is
55
+ read-only.
56
+ 3. **Do two instruments agree?** Compare Canonry's GA4 AI sessions with GA4's
57
+ own `AI Assistant` channel (`cnry ga acquisition <p> --window 30d
58
+ --group-by channel`), and with server-side `ai-referral` hits. Agreement
59
+ between independent instruments rules out an ingestion fault. GA4 may file
60
+ AI shopping links under another channel when they carry a different
61
+ `utm_medium` (for example `feed`); the source still reads `chatgpt.com`.
62
+ 4. **Paid or organic?** An AI-referral spike can be an ads test. GA4 rows carry
63
+ `trafficClass`, and server-side referrals split paid from organic. Never
64
+ compare a paid month with organic months, and say "the ads ended" rather
65
+ than "AI traffic dropped" when the paid line is what fell.
66
+ 5. **Is the source reading the right site?** Confirm the traffic source's
67
+ service or host actually serves the domain (for example, check which backend
68
+ a load balancer routes the domain to) before you claim a wiring gap.
69
+ 6. **Is the source partial?**
70
+ - The default Cloudflare Worker forwards only AI-referred and bot requests,
71
+ so journeys miss later visits until the source is reconnected with
72
+ `--forward-page-views` and the Worker redeployed. Journey reads list such
73
+ sources in `partialSources`.
74
+ - The WordPress logger runs inside WordPress, so it cannot see page views a
75
+ cache serves before WordPress runs.
76
+ - Vercel logs carry no IP, so journey keys use the user agent only
77
+ (`keyBasis`); people with the same device and browser version merge,
78
+ which undercounts people and overstates return rates.
79
+ - A hosted checkout or booking engine on another domain is outside the
80
+ site's server logs.
81
+ 7. **Is a Queue backlog hiding recent traffic?** For Cloudflare Queue pull,
82
+ read `cnry traffic status <p>` and the last sync's `syncSummary`
83
+ (`cnry runs <p> --kind traffic-sync --format json`) before quoting recent
84
+ days. Recent days look low while a backlog drains.
85
+ 8. **Is it noise?** Unverified "AI user" fetches of `/.env`, admin paths or
86
+ 404s are scanners using AI bot names. Quote verified fetches
87
+ (`verificationStatus: verified`) and pages that returned 200.
88
+
89
+ ## What attribution can and cannot say
90
+
91
+ - GA4 credits a conversion to the session it happened in. A person who first
92
+ came from ChatGPT and books days later from Google counts as Google. Journeys
93
+ keep the earlier AI touch for 30 days, but they see a conversion only when
94
+ its page (for example a booking confirmation URL) appears in the server logs.
95
+ - A return from an outside payment or booking step can take the credit in GA4
96
+ unless that domain is listed under unwanted referrals in the GA4 web stream
97
+ settings. That is a GA4 admin change for the site owner.
98
+ - A visitor key is a keyed hash of IP and user agent, scoped to the project.
99
+ Never print or ask for visitor IPs.
100
+ - Since iOS 26, Safari freezes the OS version in its user agent; device labels
101
+ use Safari's own version instead, so treat the iOS version as approximate.
102
+
103
+ ## User stories
104
+
105
+ **"Did anyone from AI search buy, book or become a lead?"**
106
+ 1. Run the instrument checks above, steps 1 and 2 first.
107
+ 2. If conversions are measured: `cnry ga measurement-analysis <p> --window 90d
108
+ --host-scope all` and read `leads.aiEngines` per engine and period. Quote
109
+ event counts with their sessions; the lead rate is null when the evidence is
110
+ mixed.
111
+ 3. Add journeys for the cross-session view: `cnry journeys list <p> --returned
112
+ true`, then `show` for visitors whose later visits reach a conversion page.
113
+ 4. Say what is unmeasured. Example: "ChatGPT sent 120 sessions in 30 days.
114
+ Bookings are not sent to GA4, so we cannot say how many booked."
115
+
116
+ **"A customer says they found us through ChatGPT. Can we trace them?"**
117
+ 1. Get the date and time of their purchase, booking or form, and the device if
118
+ known.
119
+ 2. `cnry journeys list <p> --engine ChatGPT --since <a few days before>` and
120
+ `cnry journeys show <p> <visitor-key>` for visitors active near that time.
121
+ 3. A user-agent match is "likely", never proof, unless the key also includes
122
+ the IP. Reading a provider's raw logs beyond what Canonry stored is a live
123
+ read and needs approval.
124
+
125
+ **"AI referrals dropped. Is our tracking broken?"**
126
+ 1. Pull the monthly series from both instruments (server-side `ai-referral` and
127
+ GA4 AI sessions) and split paid from organic.
128
+ 2. If both fell together, the traffic changed, not the tracking. Check for an
129
+ ended ads test, a lost citation or mention (`references/aeo-analysis.md`), or
130
+ a site change.
131
+ 3. If only one fell, check that instrument: sync errors and stop reasons
132
+ (`cnry traffic status`, `cnry runs <p> --kind traffic-sync`), the GA4 tag,
133
+ or the source's service or host.
134
+
135
+ **"Which of our pages does AI read?"**
136
+ `cnry traffic events <p> --kind ai-user-fetch --since <date>`: keep verified
137
+ fetches with status 200, group by path, and leave out `robots.txt`. These are
138
+ the pages assistants open while answering people, even when nobody clicks.
139
+
140
+ **"Do AI visitors come back?"**
141
+ `cnry journeys summary <p>`: `returnRate`, `returnedVia` and
142
+ `medianDaysToReturn`. Report `keyBasis` and `partialSources` with the numbers;
143
+ a user-agent-only or partial source changes what the rate means.
144
+
145
+ ## Reporting language
146
+
147
+ - Lead with the step, the count and the window: "Assistants read the site 300
148
+ times in 30 days (verified); 10 people clicked through; conversions are not
149
+ measured."
150
+ - Keep crawled, read, clicked, came back and converted as separate numbers.
151
+ - Name the blind spot whenever a number is zero or low because an instrument
152
+ cannot see it.
@@ -258,3 +258,4 @@ Read only references relevant to the requested task. They are not required for M
258
258
  - [Server-side traffic setup and diagnosis](references/server-side-traffic.md)
259
259
  - [Google Business Profile prerequisites](references/google-business-profile.md)
260
260
  - [Google Ads and GTM evidence boundaries](references/google-marketing.md)
261
+ - [Tracking AI traffic to visits and conversions](references/ai-traffic-tracking.md)
@@ -0,0 +1,145 @@
1
+ # Tracking AI traffic to visits and conversions
2
+
3
+ Read this before you answer whether AI search sent visitors, whether those
4
+ visitors came back, or whether they bought, booked or became leads. Read it too
5
+ when someone says "our tracking is broken" or "AI referrals dropped".
6
+
7
+ Everything here is a stored read unless it says otherwise. Changing a setting,
8
+ connecting a source, a backfill or a GA sync each need approval for that exact
9
+ action.
10
+
11
+ ## The funnel and what measures each step
12
+
13
+ Each step is usually 10 to 100 times smaller than the one above it. Most AI
14
+ answers are read inside the assistant, so a site can be read often and clicked
15
+ rarely. Report every step separately; never pool them.
16
+
17
+ | Step | What it is | Instrument | Read with |
18
+ |---|---|---|---|
19
+ | Crawled | AI and search bots fetch pages to index them | Server-side traffic, `crawler` | `cnry traffic events <p> --kind crawler` (`canonry_traffic_events`) |
20
+ | Read for a user | An assistant fetches a page live while answering someone (ChatGPT-User, Claude-User, Perplexity-User) | Server-side traffic, `ai-user-fetch` | `cnry traffic events <p> --kind ai-user-fetch` |
21
+ | Clicked | A person follows a link from an AI answer | Server-side `ai-referral` hits; GA4 AI sessions | `cnry traffic events <p> --kind ai-referral`; `cnry ga ai-referral-history <p>` (`canonry_ga_ai_referral_history`) |
22
+ | Came back | That person's later visits, from any channel | AI visitor journeys (server logs) | `cnry journeys summary\|list\|show <p>` (`canonry_journeys_summary`, `canonry_journeys_list`, `canonry_journey_get`) |
23
+ | Converted | Lead, booking or purchase | GA4 events named in `measurement.leadEventNames` | `cnry ga measurement-analysis <p> --window 90d --host-scope all` (`canonry_ga_measurement_analysis`) |
24
+
25
+ Server-side hits are requests and GA4 counts sessions, and each instrument
26
+ misses some visits the other sees, so the two never match exactly. Compare
27
+ their trend, not their totals.
28
+
29
+ ## Check the instrument before you report a zero
30
+
31
+ A zero means nothing until you know the instrument could have seen it. Work
32
+ through these in order and say which one applies.
33
+
34
+ 1. **Is the conversion tracked at all?** Read the project's
35
+ `measurement.leadEventNames` (`cnry project show <p> --format json`). The
36
+ default is only `generate_lead`. Online stores report sales as `purchase`,
37
+ and booking engines often do too. If lead events are zero across every
38
+ channel, not only AI, the instrument is blind: report "conversions are not
39
+ measured", never "AI drove no conversions". Adding an event is a setting
40
+ change (`cnry project update <p> --add-lead-event purchase`) and needs
41
+ approval; the daily refresh then fills about 30 days.
42
+ 2. **Does GA4 receive the event?** Read the stored GTM snapshot
43
+ (`cnry gtm snapshots <p>`, then `cnry gtm snapshot <p> <id>`;
44
+ `canonry_gtm_snapshot_get`). If the booking or purchase trigger fires only
45
+ Google Ads tags (`awct`) and no GA4 event tag, GA4 records no conversions.
46
+ The fix is a GA4 Event tag on the same trigger, made by someone with
47
+ publish rights in the site's GTM container. Canonry's GTM access is
48
+ read-only.
49
+ 3. **Do two instruments agree?** Compare Canonry's GA4 AI sessions with GA4's
50
+ own `AI Assistant` channel (`cnry ga acquisition <p> --window 30d
51
+ --group-by channel`), and with server-side `ai-referral` hits. Agreement
52
+ between independent instruments rules out an ingestion fault. GA4 may file
53
+ AI shopping links under another channel when they carry a different
54
+ `utm_medium` (for example `feed`); the source still reads `chatgpt.com`.
55
+ 4. **Paid or organic?** An AI-referral spike can be an ads test. GA4 rows carry
56
+ `trafficClass`, and server-side referrals split paid from organic. Never
57
+ compare a paid month with organic months, and say "the ads ended" rather
58
+ than "AI traffic dropped" when the paid line is what fell.
59
+ 5. **Is the source reading the right site?** Confirm the traffic source's
60
+ service or host actually serves the domain (for example, check which backend
61
+ a load balancer routes the domain to) before you claim a wiring gap.
62
+ 6. **Is the source partial?**
63
+ - The default Cloudflare Worker forwards only AI-referred and bot requests,
64
+ so journeys miss later visits until the source is reconnected with
65
+ `--forward-page-views` and the Worker redeployed. Journey reads list such
66
+ sources in `partialSources`.
67
+ - The WordPress logger runs inside WordPress, so it cannot see page views a
68
+ cache serves before WordPress runs.
69
+ - Vercel logs carry no IP, so journey keys use the user agent only
70
+ (`keyBasis`); people with the same device and browser version merge,
71
+ which undercounts people and overstates return rates.
72
+ - A hosted checkout or booking engine on another domain is outside the
73
+ site's server logs.
74
+ 7. **Is a Queue backlog hiding recent traffic?** For Cloudflare Queue pull,
75
+ read `cnry traffic status <p>` and the last sync's `syncSummary`
76
+ (`cnry runs <p> --kind traffic-sync --format json`) before quoting recent
77
+ days. Recent days look low while a backlog drains.
78
+ 8. **Is it noise?** Unverified "AI user" fetches of `/.env`, admin paths or
79
+ 404s are scanners using AI bot names. Quote verified fetches
80
+ (`verificationStatus: verified`) and pages that returned 200.
81
+
82
+ ## What attribution can and cannot say
83
+
84
+ - GA4 credits a conversion to the session it happened in. A person who first
85
+ came from ChatGPT and books days later from Google counts as Google. Journeys
86
+ keep the earlier AI touch for 30 days, but they see a conversion only when
87
+ its page (for example a booking confirmation URL) appears in the server logs.
88
+ - A return from an outside payment or booking step can take the credit in GA4
89
+ unless that domain is listed under unwanted referrals in the GA4 web stream
90
+ settings. That is a GA4 admin change for the site owner.
91
+ - A visitor key is a keyed hash of IP and user agent, scoped to the project.
92
+ Never print or ask for visitor IPs.
93
+ - Since iOS 26, Safari freezes the OS version in its user agent; device labels
94
+ use Safari's own version instead, so treat the iOS version as approximate.
95
+
96
+ ## User stories
97
+
98
+ **"Did anyone from AI search buy, book or become a lead?"**
99
+ 1. Run the instrument checks above, steps 1 and 2 first.
100
+ 2. If conversions are measured: `cnry ga measurement-analysis <p> --window 90d
101
+ --host-scope all` and read `leads.aiEngines` per engine and period. Quote
102
+ event counts with their sessions; the lead rate is null when the evidence is
103
+ mixed.
104
+ 3. Add journeys for the cross-session view: `cnry journeys list <p> --returned
105
+ true`, then `show` for visitors whose later visits reach a conversion page.
106
+ 4. Say what is unmeasured. Example: "ChatGPT sent 120 sessions in 30 days.
107
+ Bookings are not sent to GA4, so we cannot say how many booked."
108
+
109
+ **"A customer says they found us through ChatGPT. Can we trace them?"**
110
+ 1. Get the date and time of their purchase, booking or form, and the device if
111
+ known.
112
+ 2. `cnry journeys list <p> --engine ChatGPT --since <a few days before>` and
113
+ `cnry journeys show <p> <visitor-key>` for visitors active near that time.
114
+ 3. A user-agent match is "likely", never proof, unless the key also includes
115
+ the IP. Reading a provider's raw logs beyond what Canonry stored is a live
116
+ read and needs approval.
117
+
118
+ **"AI referrals dropped. Is our tracking broken?"**
119
+ 1. Pull the monthly series from both instruments (server-side `ai-referral` and
120
+ GA4 AI sessions) and split paid from organic.
121
+ 2. If both fell together, the traffic changed, not the tracking. Check for an
122
+ ended ads test, a lost citation or mention (`references/aeo-analysis.md`), or
123
+ a site change.
124
+ 3. If only one fell, check that instrument: sync errors and stop reasons
125
+ (`cnry traffic status`, `cnry runs <p> --kind traffic-sync`), the GA4 tag,
126
+ or the source's service or host.
127
+
128
+ **"Which of our pages does AI read?"**
129
+ `cnry traffic events <p> --kind ai-user-fetch --since <date>`: keep verified
130
+ fetches with status 200, group by path, and leave out `robots.txt`. These are
131
+ the pages assistants open while answering people, even when nobody clicks.
132
+
133
+ **"Do AI visitors come back?"**
134
+ `cnry journeys summary <p>`: `returnRate`, `returnedVia` and
135
+ `medianDaysToReturn`. Report `keyBasis` and `partialSources` with the numbers;
136
+ a user-agent-only or partial source changes what the rate means.
137
+
138
+ ## Reporting language
139
+
140
+ - Lead with the step, the count and the window: "Assistants read the site 300
141
+ times in 30 days (verified); 10 people clicked through; conversions are not
142
+ measured."
143
+ - Keep crawled, read, clicked, came back and converted as separate numbers.
144
+ - Name the blind spot whenever a number is zero or low because an instrument
145
+ cannot see it.
@@ -1006,10 +1006,14 @@ the explicit activation command. Activation pauses sibling sources and moves the
1006
1006
  one `traffic-sync` schedule for the project. Direct push rejects `traffic sync` and
1007
1007
  does not use this schedule.
1008
1008
 
1009
- Queue pull drains for up to 4 minutes in one sync. The doctor warns when more
1010
- than 1,000 messages remain afterwards: the drain hit its limit. If the operator
1011
- approves a manual drain, run a manual sync. If the backlog recurs, get approval
1012
- to shorten the schedule interval or raise the time limit.
1009
+ Queue pull drains for up to 4 minutes in one sync. Each run records why it
1010
+ stopped: read `syncSummary.stopReason` with
1011
+ `cnry runs <project> --kind traffic-sync --format json`. The doctor warns when
1012
+ more than 1,000 messages remain afterwards and names the cause. After
1013
+ `deadline` or `batch-cap` the drain hit its limit: if the operator approves a
1014
+ manual drain, run a manual sync, and if the backlog recurs, get approval to
1015
+ shorten the schedule interval or raise the time limit. After `empty-pulls`
1016
+ Cloudflare still counted messages it would not return; later syncs pull them.
1013
1017
 
1014
1018
  Read the [server-side traffic guide](server-side-traffic.md) for token safety,
1015
1019
  route checks, activation order, smoke tests, rollback, and troubleshooting.
@@ -932,10 +932,21 @@ Cloudflare reports a backlog, up to a 4-minute time limit per sync
932
932
  while a backlog waits, so a sync stops only when Cloudflare reports no backlog,
933
933
  two pulls in a row come back empty, or the limit is reached. If a pull fails
934
934
  after earlier batches were saved, the run ends `partial` and keeps them.
935
+
936
+ Each sync run records what it did and why it stopped in `syncSummary`: pulls,
937
+ empty pulls, messages pulled and acknowledged, redelivered (duplicate) events,
938
+ the backlog Cloudflare reported on the first and last pull, and `stopReason`
939
+ (`backlog-empty`, `empty-pulls`, `unleased-only`, `deadline`, `batch-cap` or
940
+ `error`). Read it with `cnry runs <project> --kind traffic-sync --format json`;
941
+ `cnry traffic status` shows the latest one.
942
+
935
943
  Doctor treats a residual of up to 1,000 messages as normal (they arrived during
936
- the drain) and warns above that: the drain hit its limit before the Queue was
937
- empty. Shorten the `traffic-sync` interval or raise the time limit, or messages
938
- will pass the Queue's retention and be dropped.
944
+ the drain) and warns above that, with the cause taken from the last stop
945
+ reason. After `deadline` or `batch-cap` the drain hit its limit: shorten the
946
+ `traffic-sync` interval or raise the time limit, or messages will pass the
947
+ Queue's retention and be dropped. After `empty-pulls` or `unleased-only`
948
+ Cloudflare still counted messages it would not return (leased or not yet
949
+ visible); later syncs pull them, and raising the time limit does not help.
939
950
 
940
951
  ## Syncing data
941
952
 
@@ -1,4 +1,4 @@
1
- import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{I as t,K as n,R as r,rt as i}from"./vendor-lucide-4KkDquAu.js";import{r as a}from"./vendor-markdown-9SSeLNHZ.js";import{Ct as o,En as s,Xt as c,Zt as l,_r as u,r as d,st as f,t as p,vr as m,wt as h,yr as g}from"./ToneBadge-C3S7aVf3.js";import{r as _}from"./account-context-DozTQM44.js";import{t as v}from"./async-handler-Bj3TnIRw.js";import{X as y,et as b}from"./index-D5o-caTl.js";var x=e(i(),1),S=a(),C=`https://github.com/Canonry/canonry/blob/main/docs/mcp.md`,w=`https://github.com/Canonry/canonry/blob/main/docs/plugins.md`,T=`https://help.openai.com/en/articles/12584461-connecting-custom-mcp-servers-in-chatgpt`,E=`https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp`,D={[m[`public-url-required`]]:`Configure this instance’s public URL before connecting a hosted client.`,[m[`https-required`]]:`Configure an HTTPS public URL for this instance.`,[m[`public-host-required`]]:`Use a public HTTPS address that your client can reach. Localhost and private-network addresses do not work for hosted connectors.`,[m[`public-path-mismatch`]]:`Make the publicUrl path match basePath, and preserve that prefix when the proxy forwards requests for hosted OAuth.`,[m[`oauth-unavailable`]]:`The sign-in routes are unavailable. Check this instance’s OAuth configuration.`,[m[`account-required`]]:`Create a named Canonry account on this instance’s server with canonry user create. A shared dashboard password does not provide an OAuth account.`,[m[`transport-unavailable`]]:`Restore the instance’s HTTP MCP service before connecting a hosted client.`},O={codex:{label:`Codex`,command:`codex plugin marketplace add Canonry/canonry
1
+ import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{I as t,K as n,R as r,rt as i}from"./vendor-lucide-4KkDquAu.js";import{r as a}from"./vendor-markdown-9SSeLNHZ.js";import{Ct as o,En as s,Xt as c,Zt as l,_r as u,r as d,st as f,t as p,vr as m,wt as h,yr as g}from"./ToneBadge-DKvZcONW.js";import{r as _}from"./account-context-D_CDYrs8.js";import{t as v}from"./async-handler-Bj3TnIRw.js";import{X as y,et as b}from"./index-DSVFZqv6.js";var x=e(i(),1),S=a(),C=`https://github.com/Canonry/canonry/blob/main/docs/mcp.md`,w=`https://github.com/Canonry/canonry/blob/main/docs/plugins.md`,T=`https://help.openai.com/en/articles/12584461-connecting-custom-mcp-servers-in-chatgpt`,E=`https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp`,D={[m[`public-url-required`]]:`Configure this instance’s public URL before connecting a hosted client.`,[m[`https-required`]]:`Configure an HTTPS public URL for this instance.`,[m[`public-host-required`]]:`Use a public HTTPS address that your client can reach. Localhost and private-network addresses do not work for hosted connectors.`,[m[`public-path-mismatch`]]:`Make the publicUrl path match basePath, and preserve that prefix when the proxy forwards requests for hosted OAuth.`,[m[`oauth-unavailable`]]:`The sign-in routes are unavailable. Check this instance’s OAuth configuration.`,[m[`account-required`]]:`Create a named Canonry account on this instance’s server with canonry user create. A shared dashboard password does not provide an OAuth account.`,[m[`transport-unavailable`]]:`Restore the instance’s HTTP MCP service before connecting a hosted client.`},O={codex:{label:`Codex`,command:`codex plugin marketplace add Canonry/canonry
2
2
  codex plugin add canonry@canonry`},claude:{label:`Claude Code`,command:`claude plugin marketplace add Canonry/canonry
3
3
  claude plugin install canonry@canonry`},other:{label:`Other MCP clients`,command:`canonry-mcp`}};function k({label:e,value:t,showText:i=!1,secondary:a=!1,action:o=`agent_setup.copy`}){let c=(0,x.useId)(),[l,u]=(0,x.useState)(!1),[f,p]=(0,x.useState)(!1);async function m(){try{await navigator.clipboard.writeText(t),s(o),u(!0),p(!1)}catch{u(!1),p(!0)}}return(0,S.jsxs)(`div`,{className:`min-w-0 space-y-3`,children:[i?(0,S.jsx)(`pre`,{className:`overflow-x-auto rounded-md border border-default bg-bg p-3 text-sm text-heading`,children:(0,S.jsx)(`code`,{children:t})}):null,(0,S.jsxs)(d,{type:`button`,className:`min-h-11`,variant:a?`outline`:`default`,onClick:v(m),children:[l?(0,S.jsx)(n,{className:`size-4`,"aria-hidden":`true`}):(0,S.jsx)(r,{className:`size-4`,"aria-hidden":`true`}),e]}),l?(0,S.jsx)(`p`,{role:`status`,className:`text-sm text-secondary`,children:`Copied.`}):null,!i||f?(0,S.jsxs)(`details`,{open:f||void 0,className:`text-sm text-secondary`,children:[(0,S.jsx)(`summary`,{className:`min-h-11 w-fit cursor-pointer py-3`,children:f?`Copy unavailable. Select the text below.`:`Preview text`}),(0,S.jsxs)(`label`,{htmlFor:c,className:`sr-only`,children:[e,` text`]}),(0,S.jsx)(`textarea`,{id:c,readOnly:!0,rows:f&&i?3:8,value:t,className:`mt-2 w-full rounded-md border border-default bg-bg p-3 text-sm leading-6 text-heading`})]}):null]})}function A(){return o()||h()?(0,S.jsxs)(`div`,{className:`page-container`,children:[(0,S.jsx)(`h1`,{className:`page-title`,children:`Agent setup`}),(0,S.jsx)(`p`,{className:`mt-3 text-sm text-secondary`,children:`Open Agent setup in your own Canonry instance to connect your agent.`}),l()?(0,S.jsx)(`a`,{className:`mt-4 inline-block text-sm underline`,href:C,target:`_blank`,rel:`noopener noreferrer`,children:`Read the connection guide`}):null]}):(0,S.jsx)(j,{})}function j(){let e=b(),{isAdmin:n}=_(),r=c(),i=l(),a=y(!e,e?.health),o=a.data?.apiStatus,s=o?.state===`ok`?o.mcp:void 0,d=s?.status===g.available&&s.hosted?.status===u.ready?s.hosted:void 0,m=s?.hosted?.status===u.unavailable?s.hosted.reason:void 0,h=`${window.location.origin}${f()}`,v=`This server has not confirmed a public URL and OAuth sign-in. Ask your administrator to check the connection configuration or update Canonry.`;a.isPending?v=`Checking the instance’s hosted connection settings.`:m?v=n&&r?D[m]:`Ask your Canonry administrator to finish hosted connection setup for this instance.`:o?.state===`error`?v=`The instance’s connection settings could not be checked. Try again after the service is available.`:s?.status===g[`not-supported`]&&(v=`This deployment does not provide hosted MCP connections. Ask your administrator for a supported connection.`);let x=d?`Help me connect this existing Canonry instance to ChatGPT or Claude Desktop using its remote MCP URL: ${d.url}
4
4
 
@@ -1 +1 @@
1
- import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{rt as t}from"./vendor-lucide-4KkDquAu.js";import{r as n}from"./vendor-markdown-9SSeLNHZ.js";import{b as r}from"./vendor-tanstack-pEsPGDvT.js";import{pt as i,r as a,t as o}from"./ToneBadge-C3S7aVf3.js";import{Jt as s,Y as c,i as l}from"./react-query-D4ZSMg7h.js";import{t as u}from"./card-D8MU7RYV.js";var d=e(t(),1),f=n(),p=100;function m(e){if(e===`all`)return;let t=Number.parseInt(e,10);return new Date(Date.now()-t*24*60*60*1e3).toISOString()}function h(e){let t=e.userAgent?.toLowerCase()??``;return t.includes(`canonry-mcp`)?`MCP`:t.includes(`canonry-cli`)?`CLI`:t.includes(`mozilla/`)?`Dashboard`:e.actor===`scheduler`?`Scheduler`:e.actor===`agent`?`Agent`:e.actor===`mcp`?`MCP`:e.actor===`cli`?`CLI`:e.actor}function g(e){return/delete|remove|revoke|disconnect|failed|refused/.test(e)?`negative`:/create|add|connect|completed/.test(e)?`positive`:/update|replace|sync|apply/.test(e)?`caution`:`neutral`}function _(e){return[e.action,e.actor,e.entityType,e.entityId,e.actorSession,e.userAgent,e.diff==null?``:JSON.stringify(e.diff)].filter(Boolean).join(` `).toLowerCase()}function v({projectName:e}){let[t,n]=(0,d.useState)(0),[v,y]=(0,d.useState)(`90d`),[b,x]=(0,d.useState)(``),[S,C]=(0,d.useState)(``),w=(0,d.useMemo)(()=>m(v),[v]),T={limit:p,offset:t*p,since:w,actor:b||void 0},E=r({...c({client:i,path:{name:e??``},query:T}),enabled:!!e}),D=r({...l({client:i,query:T}),enabled:!e}),O=r({...s({client:i}),enabled:!e}),k=e?E:D,A=k.data??[],j=(0,d.useMemo)(()=>new Map((O.data??[]).map(e=>[e.id,e.displayName||e.name])),[O.data]),M=S.trim().toLowerCase(),N=(0,d.useMemo)(()=>M?A.filter(e=>_(e).includes(M)):A,[A,M]),P=e=>{n(0),e()};return k.isLoading?(0,f.jsx)(`p`,{className:`supporting-copy mt-6`,role:`status`,children:`Loading change history…`}):(0,f.jsxs)(`section`,{className:`mt-6`,"aria-labelledby":`change-history-heading`,children:[(0,f.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,f.jsxs)(`div`,{children:[(0,f.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Audit trail`}),(0,f.jsx)(`h2`,{id:`change-history-heading`,children:`Change history`}),(0,f.jsx)(`p`,{className:`supporting-copy mt-1 max-w-2xl`,children:`Configuration and operator actions. Execution outcomes remain in Runs.`})]}),(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>void k.refetch(),disabled:k.isFetching,children:k.isFetching?`Refreshing…`:`Refresh`})]}),(0,f.jsxs)(`div`,{className:`mt-4 grid gap-2 sm:grid-cols-3`,children:[(0,f.jsx)(`input`,{type:`search`,"aria-label":`Search change history`,value:S,onChange:e=>C(e.target.value),placeholder:`Search actions, entities, or details`,className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong placeholder-mono-600 focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`}),(0,f.jsxs)(`select`,{"aria-label":`Filter change history by actor`,value:b,onChange:e=>P(()=>x(e.target.value)),className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`,children:[(0,f.jsx)(`option`,{value:``,children:`All actors`}),(0,f.jsx)(`option`,{value:`api`,children:`API and dashboard`}),(0,f.jsx)(`option`,{value:`cli`,children:`CLI`}),(0,f.jsx)(`option`,{value:`scheduler`,children:`Scheduler`}),(0,f.jsx)(`option`,{value:`agent`,children:`Agent`}),(0,f.jsx)(`option`,{value:`mcp`,children:`MCP`})]}),(0,f.jsxs)(`select`,{"aria-label":`Filter change history by date range`,value:v,onChange:e=>P(()=>y(e.target.value)),className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`,children:[(0,f.jsx)(`option`,{value:`30d`,children:`Last 30 days`}),(0,f.jsx)(`option`,{value:`90d`,children:`Last 90 days`}),(0,f.jsx)(`option`,{value:`365d`,children:`Last year`}),(0,f.jsx)(`option`,{value:`all`,children:`All retained history`})]})]}),k.isError?(0,f.jsxs)(u,{className:`surface-card empty-card mt-4`,children:[(0,f.jsx)(`h3`,{children:`Change history unavailable`}),(0,f.jsx)(`p`,{children:k.error instanceof Error?k.error.message:`Could not load the audit trail.`})]}):N.length===0?(0,f.jsxs)(u,{className:`surface-card empty-card mt-4`,children:[(0,f.jsx)(`h3`,{children:`No changes match these filters`}),(0,f.jsx)(`p`,{children:`Widen the date range or clear the actor and search filters.`})]}):(0,f.jsx)(`div`,{className:`evidence-table-wrap mt-4`,children:(0,f.jsxs)(`table`,{className:`evidence-table`,children:[(0,f.jsx)(`thead`,{children:(0,f.jsxs)(`tr`,{children:[(0,f.jsx)(`th`,{children:`When`}),e?null:(0,f.jsx)(`th`,{children:`Scope`}),(0,f.jsx)(`th`,{children:`Action`}),(0,f.jsx)(`th`,{children:`Entity`}),(0,f.jsx)(`th`,{children:`Origin`}),(0,f.jsx)(`th`,{children:`Details`})]})}),(0,f.jsx)(`tbody`,{children:N.map(t=>(0,f.jsxs)(`tr`,{children:[(0,f.jsx)(`td`,{className:`whitespace-nowrap tabular-nums text-muted`,children:new Date(t.createdAt).toLocaleString()}),e?null:(0,f.jsx)(`td`,{className:`text-secondary`,children:t.projectId?j.get(t.projectId)??`Deleted project · ${t.projectId}`:`Instance settings`}),(0,f.jsx)(`td`,{children:(0,f.jsx)(o,{tone:g(t.action),children:t.action})}),(0,f.jsxs)(`td`,{children:[(0,f.jsx)(`span`,{className:`text-strong`,children:t.entityType}),t.entityId?(0,f.jsx)(`span`,{className:`mt-0.5 block max-w-48 truncate font-mono text-[11px] text-faint`,title:t.entityId,children:t.entityId}):null]}),(0,f.jsxs)(`td`,{children:[(0,f.jsx)(`span`,{className:`text-secondary`,children:h(t)}),t.actorSession?(0,f.jsxs)(`span`,{className:`mt-0.5 block max-w-40 truncate font-mono text-[11px] text-faint`,title:t.actorSession,children:[`Session `,t.actorSession]}):null]}),(0,f.jsx)(`td`,{children:t.diff==null?(0,f.jsx)(`span`,{className:`text-faint`,children:`No field diff`}):(0,f.jsxs)(`details`,{children:[(0,f.jsx)(`summary`,{className:`min-h-11 cursor-pointer py-3 text-sm text-secondary hover:text-strong`,children:`View diff`}),(0,f.jsx)(`pre`,{className:`max-h-72 max-w-xl overflow-auto whitespace-pre-wrap break-words rounded-md bg-bg-elevated p-3 text-xs text-secondary`,children:JSON.stringify(t.diff,null,2)})]})})]},t.id))})]})}),!k.isError&&(t>0||A.length===p)?(0,f.jsxs)(`div`,{className:`mt-4 flex items-center justify-between gap-3`,children:[(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,disabled:t===0,onClick:()=>n(e=>Math.max(0,e-1)),children:`Previous`}),(0,f.jsxs)(`span`,{className:`text-xs tabular-nums text-muted`,children:[`Page `,t+1]}),(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,disabled:A.length<p,onClick:()=>n(e=>e+1),children:`Next`})]}):null]})}export{v as t};
1
+ import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{rt as t}from"./vendor-lucide-4KkDquAu.js";import{r as n}from"./vendor-markdown-9SSeLNHZ.js";import{b as r}from"./vendor-tanstack-pEsPGDvT.js";import{pt as i,r as a,t as o}from"./ToneBadge-DKvZcONW.js";import{Jt as s,Y as c,i as l}from"./react-query-DzDr4w41.js";import{t as u}from"./card-C6z0XyOo.js";var d=e(t(),1),f=n(),p=100;function m(e){if(e===`all`)return;let t=Number.parseInt(e,10);return new Date(Date.now()-t*24*60*60*1e3).toISOString()}function h(e){let t=e.userAgent?.toLowerCase()??``;return t.includes(`canonry-mcp`)?`MCP`:t.includes(`canonry-cli`)?`CLI`:t.includes(`mozilla/`)?`Dashboard`:e.actor===`scheduler`?`Scheduler`:e.actor===`agent`?`Agent`:e.actor===`mcp`?`MCP`:e.actor===`cli`?`CLI`:e.actor}function g(e){return/delete|remove|revoke|disconnect|failed|refused/.test(e)?`negative`:/create|add|connect|completed/.test(e)?`positive`:/update|replace|sync|apply/.test(e)?`caution`:`neutral`}function _(e){return[e.action,e.actor,e.entityType,e.entityId,e.actorSession,e.userAgent,e.diff==null?``:JSON.stringify(e.diff)].filter(Boolean).join(` `).toLowerCase()}function v({projectName:e}){let[t,n]=(0,d.useState)(0),[v,y]=(0,d.useState)(`90d`),[b,x]=(0,d.useState)(``),[S,C]=(0,d.useState)(``),w=(0,d.useMemo)(()=>m(v),[v]),T={limit:p,offset:t*p,since:w,actor:b||void 0},E=r({...c({client:i,path:{name:e??``},query:T}),enabled:!!e}),D=r({...l({client:i,query:T}),enabled:!e}),O=r({...s({client:i}),enabled:!e}),k=e?E:D,A=k.data??[],j=(0,d.useMemo)(()=>new Map((O.data??[]).map(e=>[e.id,e.displayName||e.name])),[O.data]),M=S.trim().toLowerCase(),N=(0,d.useMemo)(()=>M?A.filter(e=>_(e).includes(M)):A,[A,M]),P=e=>{n(0),e()};return k.isLoading?(0,f.jsx)(`p`,{className:`supporting-copy mt-6`,role:`status`,children:`Loading change history…`}):(0,f.jsxs)(`section`,{className:`mt-6`,"aria-labelledby":`change-history-heading`,children:[(0,f.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,f.jsxs)(`div`,{children:[(0,f.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Audit trail`}),(0,f.jsx)(`h2`,{id:`change-history-heading`,children:`Change history`}),(0,f.jsx)(`p`,{className:`supporting-copy mt-1 max-w-2xl`,children:`Configuration and operator actions. Execution outcomes remain in Runs.`})]}),(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>void k.refetch(),disabled:k.isFetching,children:k.isFetching?`Refreshing…`:`Refresh`})]}),(0,f.jsxs)(`div`,{className:`mt-4 grid gap-2 sm:grid-cols-3`,children:[(0,f.jsx)(`input`,{type:`search`,"aria-label":`Search change history`,value:S,onChange:e=>C(e.target.value),placeholder:`Search actions, entities, or details`,className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong placeholder-mono-600 focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`}),(0,f.jsxs)(`select`,{"aria-label":`Filter change history by actor`,value:b,onChange:e=>P(()=>x(e.target.value)),className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`,children:[(0,f.jsx)(`option`,{value:``,children:`All actors`}),(0,f.jsx)(`option`,{value:`api`,children:`API and dashboard`}),(0,f.jsx)(`option`,{value:`cli`,children:`CLI`}),(0,f.jsx)(`option`,{value:`scheduler`,children:`Scheduler`}),(0,f.jsx)(`option`,{value:`agent`,children:`Agent`}),(0,f.jsx)(`option`,{value:`mcp`,children:`MCP`})]}),(0,f.jsxs)(`select`,{"aria-label":`Filter change history by date range`,value:v,onChange:e=>P(()=>y(e.target.value)),className:`min-h-11 rounded-md border border-base bg-bg px-3 text-sm text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-600`,children:[(0,f.jsx)(`option`,{value:`30d`,children:`Last 30 days`}),(0,f.jsx)(`option`,{value:`90d`,children:`Last 90 days`}),(0,f.jsx)(`option`,{value:`365d`,children:`Last year`}),(0,f.jsx)(`option`,{value:`all`,children:`All retained history`})]})]}),k.isError?(0,f.jsxs)(u,{className:`surface-card empty-card mt-4`,children:[(0,f.jsx)(`h3`,{children:`Change history unavailable`}),(0,f.jsx)(`p`,{children:k.error instanceof Error?k.error.message:`Could not load the audit trail.`})]}):N.length===0?(0,f.jsxs)(u,{className:`surface-card empty-card mt-4`,children:[(0,f.jsx)(`h3`,{children:`No changes match these filters`}),(0,f.jsx)(`p`,{children:`Widen the date range or clear the actor and search filters.`})]}):(0,f.jsx)(`div`,{className:`evidence-table-wrap mt-4`,children:(0,f.jsxs)(`table`,{className:`evidence-table`,children:[(0,f.jsx)(`thead`,{children:(0,f.jsxs)(`tr`,{children:[(0,f.jsx)(`th`,{children:`When`}),e?null:(0,f.jsx)(`th`,{children:`Scope`}),(0,f.jsx)(`th`,{children:`Action`}),(0,f.jsx)(`th`,{children:`Entity`}),(0,f.jsx)(`th`,{children:`Origin`}),(0,f.jsx)(`th`,{children:`Details`})]})}),(0,f.jsx)(`tbody`,{children:N.map(t=>(0,f.jsxs)(`tr`,{children:[(0,f.jsx)(`td`,{className:`whitespace-nowrap tabular-nums text-muted`,children:new Date(t.createdAt).toLocaleString()}),e?null:(0,f.jsx)(`td`,{className:`text-secondary`,children:t.projectId?j.get(t.projectId)??`Deleted project · ${t.projectId}`:`Instance settings`}),(0,f.jsx)(`td`,{children:(0,f.jsx)(o,{tone:g(t.action),children:t.action})}),(0,f.jsxs)(`td`,{children:[(0,f.jsx)(`span`,{className:`text-strong`,children:t.entityType}),t.entityId?(0,f.jsx)(`span`,{className:`mt-0.5 block max-w-48 truncate font-mono text-[11px] text-faint`,title:t.entityId,children:t.entityId}):null]}),(0,f.jsxs)(`td`,{children:[(0,f.jsx)(`span`,{className:`text-secondary`,children:h(t)}),t.actorSession?(0,f.jsxs)(`span`,{className:`mt-0.5 block max-w-40 truncate font-mono text-[11px] text-faint`,title:t.actorSession,children:[`Session `,t.actorSession]}):null]}),(0,f.jsx)(`td`,{children:t.diff==null?(0,f.jsx)(`span`,{className:`text-faint`,children:`No field diff`}):(0,f.jsxs)(`details`,{children:[(0,f.jsx)(`summary`,{className:`min-h-11 cursor-pointer py-3 text-sm text-secondary hover:text-strong`,children:`View diff`}),(0,f.jsx)(`pre`,{className:`max-h-72 max-w-xl overflow-auto whitespace-pre-wrap break-words rounded-md bg-bg-elevated p-3 text-xs text-secondary`,children:JSON.stringify(t.diff,null,2)})]})})]},t.id))})]})}),!k.isError&&(t>0||A.length===p)?(0,f.jsxs)(`div`,{className:`mt-4 flex items-center justify-between gap-3`,children:[(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,disabled:t===0,onClick:()=>n(e=>Math.max(0,e-1)),children:`Previous`}),(0,f.jsxs)(`span`,{className:`text-xs tabular-nums text-muted`,children:[`Page `,t+1]}),(0,f.jsx)(a,{type:`button`,variant:`outline`,size:`sm`,disabled:A.length<p,onClick:()=>n(e=>e+1),children:`Next`})]}):null]})}export{v as t};
@@ -1 +1 @@
1
- import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{I as t,L as n,S as r,o as i,rt as a,s as o}from"./vendor-lucide-4KkDquAu.js";import{r as s}from"./vendor-markdown-9SSeLNHZ.js";import{At as c,Ct as l,H as u,I as d,L as f,U as p,_t as m,a as h,ln as g,q as _,r as v,t as y}from"./ToneBadge-C3S7aVf3.js";import{t as b}from"./async-handler-Bj3TnIRw.js";import{t as x}from"./card-D8MU7RYV.js";import{t as S}from"./Hint-C0TmIzHX.js";var C=e(a(),1),w=s(),T=`https://commoncrawl.org/web-graphs`;function E(e){return e==null?`—`:e>=0xe8d4a51000?`${(e/0xe8d4a51000).toFixed(1)} TB`:e>=1e9?`${(e/1e9).toFixed(1)} GB`:e>=1e6?`${(e/1e6).toFixed(1)} MB`:e>=1e3?`${(e/1e3).toFixed(1)} KB`:`${e} B`}function D(e){if(!e)return`—`;let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/6e4);if(n<1)return`just now`;if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function O(e){switch(e){case`ready`:return`positive`;case`failed`:return`negative`;case`downloading`:case`querying`:case`queued`:return`caution`}}function k(){let[e,a]=(0,C.useState)(null),[s,k]=(0,C.useState)(null),[A,j]=(0,C.useState)([]),[M,N]=(0,C.useState)([]),[P,F]=(0,C.useState)(null),[I,L]=(0,C.useState)(!0),[R,z]=(0,C.useState)(!1),[B,V]=(0,C.useState)(!1),[H,U]=(0,C.useState)(``),[W,G]=(0,C.useState)(!1),[K,q]=(0,C.useState)(null),[J,Y]=(0,C.useState)(null),X=(0,C.useCallback)(async()=>{L(!0),q(null);try{let[e,t,n,r,i]=await Promise.all([d(),p().catch(()=>null),_().catch(()=>[]),f().catch(()=>[]),u().catch(()=>null)]);a(e),k(t),j(n),N(r),F(i)}catch(e){q(e instanceof Error?e.message:`Failed to load backlinks status`)}finally{L(!1)}},[]);(0,C.useEffect)(()=>{X()},[X]);async function Z(){z(!0),q(null),Y(null);try{let e=await m();Y(e.alreadyPresent?`DuckDB already installed (${e.version}).`:`Installed DuckDB ${e.version}.`),await X()}catch(e){q(e instanceof Error?e.message:`Failed to install DuckDB`)}finally{z(!1)}}async function Q(){let e=H.trim()||void 0;V(!0),q(null),Y(null);try{let t=await g(e);Y(e?`Queued sync for ${t.release}. Download + query runs in the background.`:`Queued sync for auto-discovered release ${t.release}. Download + query runs in the background.`),U(``),G(!1),await X()}catch(e){e instanceof h&&e.code===`MISSING_DEPENDENCY`?q(`DuckDB is not installed. Install it first.`):q(e instanceof Error?e.message:`Failed to trigger sync`)}finally{V(!1)}}async function $(e){q(null),Y(null);try{await c(e),Y(`Pruned cached release ${e}.`),await X()}catch(e){q(e instanceof Error?e.message:`Failed to prune release`)}}let ee=s?.status===`ready`&&M.every(e=>e.release!==s.release);return(0,w.jsxs)(`div`,{className:`page-container`,children:[(0,w.jsx)(`div`,{className:`page-header`,children:(0,w.jsxs)(`div`,{className:`page-header-left`,children:[(0,w.jsx)(`h1`,{className:`page-title`,children:`Backlink data`}),(0,w.jsx)(`p`,{className:`page-subtitle`,children:`Manage the shared backlink source and review its latest data.`})]})}),K&&(0,w.jsx)(x,{className:`surface-card p-4 mb-4 border-negative-800/60`,children:(0,w.jsx)(`p`,{className:`text-sm text-negative`,children:K})}),J&&(0,w.jsx)(x,{className:`surface-card p-4 mb-4 border-positive-800/60`,children:(0,w.jsx)(`p`,{className:`text-sm text-positive`,children:J})}),(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Readiness`}),(0,w.jsx)(`h2`,{children:`Backlink source`})]}),e?.duckdbInstalled?(0,w.jsx)(y,{tone:`positive`,children:`Installed`}):(0,w.jsx)(y,{tone:`caution`,children:`Not installed`})]}),(0,w.jsx)(x,{className:`surface-card p-4`,children:I?(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`Checking…`}):e?.duckdbInstalled?(0,w.jsx)(`p`,{className:`text-sm text-secondary`,children:`Ready to sync a Common Crawl release.`}):(0,w.jsxs)(`div`,{className:`flex items-center justify-between gap-4`,children:[(0,w.jsx)(`p`,{className:`text-sm text-secondary`,children:`Install the local query engine to enable release syncs.`}),!l()&&(0,w.jsxs)(v,{type:`button`,size:`sm`,disabled:R,onClick:b(Z),children:[(0,w.jsx)(n,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),R?`Installing…`:`Install DuckDB`]})]})})]}),(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Latest sync`}),(0,w.jsxs)(`h2`,{className:`flex items-center gap-2`,children:[`Release sync`,(0,w.jsx)(S,{label:`What is a release sync?`,children:`A release sync downloads one Common Crawl dump (~16 GB) and extracts backlinks for every project in this workspace in one pass. This is the heavy job — subsequent per-project re-runs skip the download and just re-query the cached files.`})]})]}),s&&(0,w.jsx)(y,{tone:O(s.status),children:s.status})]}),(0,w.jsxs)(x,{className:`surface-card p-5`,children:[s?(0,w.jsxs)(`div`,{className:`space-y-2 text-sm`,children:[(0,w.jsxs)(`p`,{className:`text-strong`,children:[`Release `,(0,w.jsx)(`code`,{className:`text-neutral`,children:s.release})]}),s.phaseDetail&&(0,w.jsx)(`p`,{className:`text-secondary`,children:s.phaseDetail}),(0,w.jsxs)(`div`,{className:`grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-muted pt-2`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Projects`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:s.projectsProcessed??`—`})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsxs)(`p`,{className:`text-faint uppercase tracking-wide flex items-center gap-1`,children:[`Rows`,(0,w.jsx)(S,{label:`What are rows?`,children:`Total number of (project, referring domain) pairs persisted in SQLite from this sync, across every project in the workspace.`})]}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:s.domainsDiscovered??`—`})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Started`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:D(s.downloadStartedAt??s.createdAt)})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Finished`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:D(s.queryFinishedAt)})]})]}),s.error&&(0,w.jsx)(`p`,{className:`text-sm text-negative-400 pt-2`,children:s.error})]}):(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`No release sync has run in this workspace yet.`}),ee&&(0,w.jsx)(`div`,{className:`mt-4 rounded border border-caution-800/60 bg-caution-950/20 p-3`,children:(0,w.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,w.jsx)(i,{className:`h-4 w-4 text-caution-400 shrink-0 mt-0.5`,"aria-hidden":!0}),(0,w.jsxs)(`div`,{className:`text-sm text-neutral leading-relaxed`,children:[(0,w.jsx)(`p`,{className:`font-medium text-caution-200`,children:`Cached files for this release are missing.`}),(0,w.jsx)(`p`,{className:`mt-1 text-secondary`,children:`Sync this release again, or sync the newest release, before refreshing individual projects.`})]})]})}),(0,w.jsxs)(`div`,{className:`mt-4 rounded border border-base bg-bg-elevated/40 p-3`,children:[(0,w.jsxs)(`div`,{className:`flex items-start justify-between gap-3 mb-3`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-[10px] uppercase tracking-wide text-muted`,children:`Auto-detected release`}),P?(0,w.jsxs)(`p`,{className:`text-sm text-strong mt-0.5`,children:[(0,w.jsx)(`code`,{className:`text-heading`,children:P.release}),(0,w.jsxs)(`span`,{className:`ml-2 text-xs text-muted`,children:[`Vertex `,E(P.vertexBytes),`, edges `,E(P.edgesBytes)]})]}):(0,w.jsx)(`p`,{className:`text-sm text-muted mt-0.5`,children:I?`Checking Common Crawl…`:`Could not find a release. Choose one below.`}),(0,w.jsxs)(`a`,{href:T,target:`_blank`,rel:`noopener noreferrer`,className:`mt-1 inline-flex items-center gap-1 text-xs text-secondary hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,children:[`Browse all Common Crawl web-graph releases`,(0,w.jsx)(t,{className:`h-3 w-3`,"aria-hidden":!0})]})]}),!l()&&(0,w.jsxs)(`div`,{className:`flex items-center gap-2 shrink-0`,children:[(0,w.jsxs)(v,{type:`button`,size:`sm`,disabled:B||!e?.duckdbInstalled||!P&&!H.trim(),onClick:b(Q),children:[(0,w.jsx)(r,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),B?`Queuing…`:`Run sync`]}),(0,w.jsxs)(S,{label:`What does Run sync do?`,children:[(0,w.jsxs)(`span`,{className:`block`,children:[`Downloads the auto-detected (or chosen) Common Crawl release (~16 GB) to`,` `,(0,w.jsx)(`code`,{className:`text-neutral`,children:`~/.canonry/cache/commoncrawl/`}),`, then runs a single DuckDB query that extracts referring domains for every project in this workspace.`]}),(0,w.jsxs)(`span`,{className:`mt-2 block text-secondary`,children:[`First time for a release: `,(0,w.jsx)(`span`,{className:`text-strong`,children:`~10–20 min download + ~5 min query`}),`. Re-running the same release later: `,(0,w.jsx)(`span`,{className:`text-strong`,children:`skips download, just re-queries`}),` (~5 min).`]})]})]})]}),l()?null:W?(0,w.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,w.jsx)(`input`,{type:`text`,className:`flex-1 min-w-[240px] rounded border border-strong bg-transparent px-2.5 py-1.5 text-sm text-strong placeholder-mono-600 focus:border-mono-500 focus:outline-none`,placeholder:`cc-main-2026-jan-feb-mar`,value:H,onChange:e=>U(e.target.value),disabled:B,autoFocus:!0}),(0,w.jsx)(`button`,{type:`button`,className:`text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,onClick:()=>{U(``),G(!1)},disabled:B,children:`Cancel`})]}):(0,w.jsx)(`button`,{type:`button`,className:`text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,onClick:()=>G(!0),disabled:B,children:`Use a different release →`})]}),!e?.duckdbInstalled&&(0,w.jsx)(`p`,{className:`text-sm text-secondary mt-2`,children:`Install DuckDB first to enable sync.`})]})]}),(0,w.jsxs)(`details`,{className:`page-section-divider group`,children:[(0,w.jsx)(`summary`,{className:`cursor-pointer text-sm font-medium text-secondary hover:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,children:`Technical details and cache`}),(0,w.jsxs)(`div`,{className:`mt-4`,children:[(0,w.jsx)(`p`,{className:`text-sm text-secondary mb-3`,children:`Cached releases support project re-extracts. Pruning a release does not remove saved backlink results.`}),(0,w.jsx)(x,{className:`surface-card overflow-hidden`,children:(0,w.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsxs)(`tr`,{className:`border-b border-base text-left text-xs uppercase tracking-wide text-faint`,children:[(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Release`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Sync status`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Size`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Last used`}),!l()&&(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium sr-only`,children:`Actions`})]})}),(0,w.jsxs)(`tbody`,{children:[M.map(e=>(0,w.jsxs)(`tr`,{className:`border-b border-mono-900 last:border-0`,children:[(0,w.jsx)(`td`,{className:`px-4 py-2 text-strong`,children:(0,w.jsx)(`code`,{children:e.release})}),(0,w.jsx)(`td`,{className:`px-4 py-2`,children:e.syncStatus?(0,w.jsx)(y,{tone:O(e.syncStatus),children:e.syncStatus}):(0,w.jsx)(`span`,{className:`text-faint`,children:`—`})}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:E(e.bytes)}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-secondary`,children:D(e.lastUsedAt)}),!l()&&(0,w.jsx)(`td`,{className:`px-4 py-2 text-right`,children:(0,w.jsxs)(`div`,{className:`inline-flex items-center gap-1`,children:[(0,w.jsxs)(v,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>{$(e.release)},children:[(0,w.jsx)(o,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),`Prune`]}),(0,w.jsx)(S,{label:`What does Prune do?`,placement:`top`,children:`Deletes the ~16 GB cache for this release from disk. Backlink results already in SQLite remain untouched. To re-run extracts against this release, you’d have to sync it again (another ~16 GB download).`})]})})]},e.release)),M.length===0&&(0,w.jsx)(`tr`,{children:(0,w.jsx)(`td`,{className:`px-4 py-4 text-sm text-muted`,colSpan:l()?4:5,children:`No cached releases on this machine. If you ran a sync from a different machine (or deleted the cache), the backlink data is still in the database — but you’ll need to re-sync a release to run new extracts.`})})]})]})}),(0,w.jsx)(`p`,{className:`mt-3 text-xs text-muted`,children:`Common Crawl files are stored locally. DuckDB reads them; Canonry stores results in its normal database.`})]})]}),A.length>1&&(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsx)(`div`,{className:`section-head section-head-inline`,children:(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`History`}),(0,w.jsx)(`h2`,{children:`Past release syncs`})]})}),(0,w.jsx)(x,{className:`surface-card overflow-hidden`,children:(0,w.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsxs)(`tr`,{className:`border-b border-base text-left text-xs uppercase tracking-wide text-faint`,children:[(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Release`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Status`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Projects`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Rows`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Finished`})]})}),(0,w.jsx)(`tbody`,{children:A.map(e=>(0,w.jsxs)(`tr`,{className:`border-b border-mono-900 last:border-0`,children:[(0,w.jsx)(`td`,{className:`px-4 py-2 text-strong`,children:(0,w.jsx)(`code`,{children:e.release})}),(0,w.jsx)(`td`,{className:`px-4 py-2`,children:(0,w.jsx)(y,{tone:O(e.status),children:e.status})}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:e.projectsProcessed??`—`}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:e.domainsDiscovered??`—`}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-secondary`,children:D(e.queryFinishedAt??e.updatedAt)})]},e.id))})]})})]})]})}export{k as BacklinksPage};
1
+ import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{I as t,L as n,S as r,o as i,rt as a,s as o}from"./vendor-lucide-4KkDquAu.js";import{r as s}from"./vendor-markdown-9SSeLNHZ.js";import{At as c,Ct as l,H as u,I as d,L as f,U as p,_t as m,a as h,ln as g,q as _,r as v,t as y}from"./ToneBadge-DKvZcONW.js";import{t as b}from"./async-handler-Bj3TnIRw.js";import{t as x}from"./card-C6z0XyOo.js";import{t as S}from"./Hint-C0TmIzHX.js";var C=e(a(),1),w=s(),T=`https://commoncrawl.org/web-graphs`;function E(e){return e==null?`—`:e>=0xe8d4a51000?`${(e/0xe8d4a51000).toFixed(1)} TB`:e>=1e9?`${(e/1e9).toFixed(1)} GB`:e>=1e6?`${(e/1e6).toFixed(1)} MB`:e>=1e3?`${(e/1e3).toFixed(1)} KB`:`${e} B`}function D(e){if(!e)return`—`;let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/6e4);if(n<1)return`just now`;if(n<60)return`${n}m ago`;let r=Math.floor(n/60);return r<24?`${r}h ago`:`${Math.floor(r/24)}d ago`}function O(e){switch(e){case`ready`:return`positive`;case`failed`:return`negative`;case`downloading`:case`querying`:case`queued`:return`caution`}}function k(){let[e,a]=(0,C.useState)(null),[s,k]=(0,C.useState)(null),[A,j]=(0,C.useState)([]),[M,N]=(0,C.useState)([]),[P,F]=(0,C.useState)(null),[I,L]=(0,C.useState)(!0),[R,z]=(0,C.useState)(!1),[B,V]=(0,C.useState)(!1),[H,U]=(0,C.useState)(``),[W,G]=(0,C.useState)(!1),[K,q]=(0,C.useState)(null),[J,Y]=(0,C.useState)(null),X=(0,C.useCallback)(async()=>{L(!0),q(null);try{let[e,t,n,r,i]=await Promise.all([d(),p().catch(()=>null),_().catch(()=>[]),f().catch(()=>[]),u().catch(()=>null)]);a(e),k(t),j(n),N(r),F(i)}catch(e){q(e instanceof Error?e.message:`Failed to load backlinks status`)}finally{L(!1)}},[]);(0,C.useEffect)(()=>{X()},[X]);async function Z(){z(!0),q(null),Y(null);try{let e=await m();Y(e.alreadyPresent?`DuckDB already installed (${e.version}).`:`Installed DuckDB ${e.version}.`),await X()}catch(e){q(e instanceof Error?e.message:`Failed to install DuckDB`)}finally{z(!1)}}async function Q(){let e=H.trim()||void 0;V(!0),q(null),Y(null);try{let t=await g(e);Y(e?`Queued sync for ${t.release}. Download + query runs in the background.`:`Queued sync for auto-discovered release ${t.release}. Download + query runs in the background.`),U(``),G(!1),await X()}catch(e){e instanceof h&&e.code===`MISSING_DEPENDENCY`?q(`DuckDB is not installed. Install it first.`):q(e instanceof Error?e.message:`Failed to trigger sync`)}finally{V(!1)}}async function $(e){q(null),Y(null);try{await c(e),Y(`Pruned cached release ${e}.`),await X()}catch(e){q(e instanceof Error?e.message:`Failed to prune release`)}}let ee=s?.status===`ready`&&M.every(e=>e.release!==s.release);return(0,w.jsxs)(`div`,{className:`page-container`,children:[(0,w.jsx)(`div`,{className:`page-header`,children:(0,w.jsxs)(`div`,{className:`page-header-left`,children:[(0,w.jsx)(`h1`,{className:`page-title`,children:`Backlink data`}),(0,w.jsx)(`p`,{className:`page-subtitle`,children:`Manage the shared backlink source and review its latest data.`})]})}),K&&(0,w.jsx)(x,{className:`surface-card p-4 mb-4 border-negative-800/60`,children:(0,w.jsx)(`p`,{className:`text-sm text-negative`,children:K})}),J&&(0,w.jsx)(x,{className:`surface-card p-4 mb-4 border-positive-800/60`,children:(0,w.jsx)(`p`,{className:`text-sm text-positive`,children:J})}),(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Readiness`}),(0,w.jsx)(`h2`,{children:`Backlink source`})]}),e?.duckdbInstalled?(0,w.jsx)(y,{tone:`positive`,children:`Installed`}):(0,w.jsx)(y,{tone:`caution`,children:`Not installed`})]}),(0,w.jsx)(x,{className:`surface-card p-4`,children:I?(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`Checking…`}):e?.duckdbInstalled?(0,w.jsx)(`p`,{className:`text-sm text-secondary`,children:`Ready to sync a Common Crawl release.`}):(0,w.jsxs)(`div`,{className:`flex items-center justify-between gap-4`,children:[(0,w.jsx)(`p`,{className:`text-sm text-secondary`,children:`Install the local query engine to enable release syncs.`}),!l()&&(0,w.jsxs)(v,{type:`button`,size:`sm`,disabled:R,onClick:b(Z),children:[(0,w.jsx)(n,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),R?`Installing…`:`Install DuckDB`]})]})})]}),(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsxs)(`div`,{className:`section-head section-head-inline`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`Latest sync`}),(0,w.jsxs)(`h2`,{className:`flex items-center gap-2`,children:[`Release sync`,(0,w.jsx)(S,{label:`What is a release sync?`,children:`A release sync downloads one Common Crawl dump (~16 GB) and extracts backlinks for every project in this workspace in one pass. This is the heavy job — subsequent per-project re-runs skip the download and just re-query the cached files.`})]})]}),s&&(0,w.jsx)(y,{tone:O(s.status),children:s.status})]}),(0,w.jsxs)(x,{className:`surface-card p-5`,children:[s?(0,w.jsxs)(`div`,{className:`space-y-2 text-sm`,children:[(0,w.jsxs)(`p`,{className:`text-strong`,children:[`Release `,(0,w.jsx)(`code`,{className:`text-neutral`,children:s.release})]}),s.phaseDetail&&(0,w.jsx)(`p`,{className:`text-secondary`,children:s.phaseDetail}),(0,w.jsxs)(`div`,{className:`grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-muted pt-2`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Projects`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:s.projectsProcessed??`—`})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsxs)(`p`,{className:`text-faint uppercase tracking-wide flex items-center gap-1`,children:[`Rows`,(0,w.jsx)(S,{label:`What are rows?`,children:`Total number of (project, referring domain) pairs persisted in SQLite from this sync, across every project in the workspace.`})]}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:s.domainsDiscovered??`—`})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Started`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:D(s.downloadStartedAt??s.createdAt)})]}),(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-faint uppercase tracking-wide`,children:`Finished`}),(0,w.jsx)(`p`,{className:`text-neutral mt-0.5`,children:D(s.queryFinishedAt)})]})]}),s.error&&(0,w.jsx)(`p`,{className:`text-sm text-negative-400 pt-2`,children:s.error})]}):(0,w.jsx)(`p`,{className:`text-sm text-muted`,children:`No release sync has run in this workspace yet.`}),ee&&(0,w.jsx)(`div`,{className:`mt-4 rounded border border-caution-800/60 bg-caution-950/20 p-3`,children:(0,w.jsxs)(`div`,{className:`flex items-start gap-2`,children:[(0,w.jsx)(i,{className:`h-4 w-4 text-caution-400 shrink-0 mt-0.5`,"aria-hidden":!0}),(0,w.jsxs)(`div`,{className:`text-sm text-neutral leading-relaxed`,children:[(0,w.jsx)(`p`,{className:`font-medium text-caution-200`,children:`Cached files for this release are missing.`}),(0,w.jsx)(`p`,{className:`mt-1 text-secondary`,children:`Sync this release again, or sync the newest release, before refreshing individual projects.`})]})]})}),(0,w.jsxs)(`div`,{className:`mt-4 rounded border border-base bg-bg-elevated/40 p-3`,children:[(0,w.jsxs)(`div`,{className:`flex items-start justify-between gap-3 mb-3`,children:[(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`text-[10px] uppercase tracking-wide text-muted`,children:`Auto-detected release`}),P?(0,w.jsxs)(`p`,{className:`text-sm text-strong mt-0.5`,children:[(0,w.jsx)(`code`,{className:`text-heading`,children:P.release}),(0,w.jsxs)(`span`,{className:`ml-2 text-xs text-muted`,children:[`Vertex `,E(P.vertexBytes),`, edges `,E(P.edgesBytes)]})]}):(0,w.jsx)(`p`,{className:`text-sm text-muted mt-0.5`,children:I?`Checking Common Crawl…`:`Could not find a release. Choose one below.`}),(0,w.jsxs)(`a`,{href:T,target:`_blank`,rel:`noopener noreferrer`,className:`mt-1 inline-flex items-center gap-1 text-xs text-secondary hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,children:[`Browse all Common Crawl web-graph releases`,(0,w.jsx)(t,{className:`h-3 w-3`,"aria-hidden":!0})]})]}),!l()&&(0,w.jsxs)(`div`,{className:`flex items-center gap-2 shrink-0`,children:[(0,w.jsxs)(v,{type:`button`,size:`sm`,disabled:B||!e?.duckdbInstalled||!P&&!H.trim(),onClick:b(Q),children:[(0,w.jsx)(r,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),B?`Queuing…`:`Run sync`]}),(0,w.jsxs)(S,{label:`What does Run sync do?`,children:[(0,w.jsxs)(`span`,{className:`block`,children:[`Downloads the auto-detected (or chosen) Common Crawl release (~16 GB) to`,` `,(0,w.jsx)(`code`,{className:`text-neutral`,children:`~/.canonry/cache/commoncrawl/`}),`, then runs a single DuckDB query that extracts referring domains for every project in this workspace.`]}),(0,w.jsxs)(`span`,{className:`mt-2 block text-secondary`,children:[`First time for a release: `,(0,w.jsx)(`span`,{className:`text-strong`,children:`~10–20 min download + ~5 min query`}),`. Re-running the same release later: `,(0,w.jsx)(`span`,{className:`text-strong`,children:`skips download, just re-queries`}),` (~5 min).`]})]})]})]}),l()?null:W?(0,w.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,w.jsx)(`input`,{type:`text`,className:`flex-1 min-w-[240px] rounded border border-strong bg-transparent px-2.5 py-1.5 text-sm text-strong placeholder-mono-600 focus:border-mono-500 focus:outline-none`,placeholder:`cc-main-2026-jan-feb-mar`,value:H,onChange:e=>U(e.target.value),disabled:B,autoFocus:!0}),(0,w.jsx)(`button`,{type:`button`,className:`text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,onClick:()=>{U(``),G(!1)},disabled:B,children:`Cancel`})]}):(0,w.jsx)(`button`,{type:`button`,className:`text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,onClick:()=>G(!0),disabled:B,children:`Use a different release →`})]}),!e?.duckdbInstalled&&(0,w.jsx)(`p`,{className:`text-sm text-secondary mt-2`,children:`Install DuckDB first to enable sync.`})]})]}),(0,w.jsxs)(`details`,{className:`page-section-divider group`,children:[(0,w.jsx)(`summary`,{className:`cursor-pointer text-sm font-medium text-secondary hover:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded`,children:`Technical details and cache`}),(0,w.jsxs)(`div`,{className:`mt-4`,children:[(0,w.jsx)(`p`,{className:`text-sm text-secondary mb-3`,children:`Cached releases support project re-extracts. Pruning a release does not remove saved backlink results.`}),(0,w.jsx)(x,{className:`surface-card overflow-hidden`,children:(0,w.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsxs)(`tr`,{className:`border-b border-base text-left text-xs uppercase tracking-wide text-faint`,children:[(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Release`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Sync status`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Size`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Last used`}),!l()&&(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium sr-only`,children:`Actions`})]})}),(0,w.jsxs)(`tbody`,{children:[M.map(e=>(0,w.jsxs)(`tr`,{className:`border-b border-mono-900 last:border-0`,children:[(0,w.jsx)(`td`,{className:`px-4 py-2 text-strong`,children:(0,w.jsx)(`code`,{children:e.release})}),(0,w.jsx)(`td`,{className:`px-4 py-2`,children:e.syncStatus?(0,w.jsx)(y,{tone:O(e.syncStatus),children:e.syncStatus}):(0,w.jsx)(`span`,{className:`text-faint`,children:`—`})}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:E(e.bytes)}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-secondary`,children:D(e.lastUsedAt)}),!l()&&(0,w.jsx)(`td`,{className:`px-4 py-2 text-right`,children:(0,w.jsxs)(`div`,{className:`inline-flex items-center gap-1`,children:[(0,w.jsxs)(v,{type:`button`,variant:`outline`,size:`sm`,onClick:()=>{$(e.release)},children:[(0,w.jsx)(o,{className:`h-4 w-4 mr-1.5`,"aria-hidden":!0}),`Prune`]}),(0,w.jsx)(S,{label:`What does Prune do?`,placement:`top`,children:`Deletes the ~16 GB cache for this release from disk. Backlink results already in SQLite remain untouched. To re-run extracts against this release, you’d have to sync it again (another ~16 GB download).`})]})})]},e.release)),M.length===0&&(0,w.jsx)(`tr`,{children:(0,w.jsx)(`td`,{className:`px-4 py-4 text-sm text-muted`,colSpan:l()?4:5,children:`No cached releases on this machine. If you ran a sync from a different machine (or deleted the cache), the backlink data is still in the database — but you’ll need to re-sync a release to run new extracts.`})})]})]})}),(0,w.jsx)(`p`,{className:`mt-3 text-xs text-muted`,children:`Common Crawl files are stored locally. DuckDB reads them; Canonry stores results in its normal database.`})]})]}),A.length>1&&(0,w.jsxs)(`section`,{className:`page-section-divider`,children:[(0,w.jsx)(`div`,{className:`section-head section-head-inline`,children:(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`p`,{className:`eyebrow eyebrow-soft`,children:`History`}),(0,w.jsx)(`h2`,{children:`Past release syncs`})]})}),(0,w.jsx)(x,{className:`surface-card overflow-hidden`,children:(0,w.jsxs)(`table`,{className:`w-full text-sm`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsxs)(`tr`,{className:`border-b border-base text-left text-xs uppercase tracking-wide text-faint`,children:[(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Release`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Status`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Projects`}),(0,w.jsx)(`th`,{className:`px-4 py-2 text-right font-medium`,children:`Rows`}),(0,w.jsx)(`th`,{className:`px-4 py-2 font-medium`,children:`Finished`})]})}),(0,w.jsx)(`tbody`,{children:A.map(e=>(0,w.jsxs)(`tr`,{className:`border-b border-mono-900 last:border-0`,children:[(0,w.jsx)(`td`,{className:`px-4 py-2 text-strong`,children:(0,w.jsx)(`code`,{children:e.release})}),(0,w.jsx)(`td`,{className:`px-4 py-2`,children:(0,w.jsx)(y,{tone:O(e.status),children:e.status})}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:e.projectsProcessed??`—`}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-right text-secondary tabular-nums`,children:e.domainsDiscovered??`—`}),(0,w.jsx)(`td`,{className:`px-4 py-2 text-secondary`,children:D(e.queryFinishedAt??e.updatedAt)})]},e.id))})]})})]})]})}export{k as BacklinksPage};
@@ -1 +1 @@
1
- import{r as e}from"./vendor-markdown-9SSeLNHZ.js";import{t}from"./AuditHistoryPanel-B8mIRI3I.js";var n=e();function r(){return(0,n.jsxs)(`div`,{className:`page-container`,children:[(0,n.jsx)(`div`,{className:`page-header`,children:(0,n.jsxs)(`div`,{className:`page-header-left`,children:[(0,n.jsx)(`h1`,{className:`page-title`,children:`History`}),(0,n.jsx)(`p`,{className:`page-subtitle`,children:`Changes to projects and settings.`})]})}),(0,n.jsx)(t,{})]})}export{r as HistoryPage};
1
+ import{r as e}from"./vendor-markdown-9SSeLNHZ.js";import{t}from"./AuditHistoryPanel-Ce_oy4OV.js";var n=e();function r(){return(0,n.jsxs)(`div`,{className:`page-container`,children:[(0,n.jsx)(`div`,{className:`page-header`,children:(0,n.jsxs)(`div`,{className:`page-header-left`,children:[(0,n.jsx)(`h1`,{className:`page-title`,children:`History`}),(0,n.jsx)(`p`,{className:`page-subtitle`,children:`Changes to projects and settings.`})]})}),(0,n.jsx)(t,{})]})}export{r as HistoryPage};