@canonry/canonry 7.62.0 → 7.63.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 (135) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +7 -0
  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 +2 -1
  6. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +6 -3
  7. package/assets/assets/{AgentSetupPage-DtPnVaeF.js → AgentSetupPage-D6lCJ1G1.js} +1 -1
  8. package/assets/assets/{AuditHistoryPanel-Ce_oy4OV.js → AuditHistoryPanel-DlgI3o-U.js} +1 -1
  9. package/assets/assets/{BacklinksPage-CrqPWRd8.js → BacklinksPage-ByiMqbSI.js} +1 -1
  10. package/assets/assets/{HistoryPage-5v0JM7DN.js → HistoryPage-DclVeuLO.js} +1 -1
  11. package/assets/assets/{MeasurementPropertyPage-Bm5Mrd5a.js → MeasurementPropertyPage-DDHybtHa.js} +1 -1
  12. package/assets/assets/{ProjectPage-DaWUh6iw.js → ProjectPage-vBu6oWFi.js} +1 -1
  13. package/assets/assets/{RunsPage-CfdAR2v2.js → RunsPage-CYBJ6pXX.js} +1 -1
  14. package/assets/assets/{SettingsPage-BFooA7JG.js → SettingsPage-w0GmXir0.js} +1 -1
  15. package/assets/assets/{SiteHealthSection-CerUQgAp.js → SiteHealthSection-DDTjGZ05.js} +2 -2
  16. package/assets/assets/ToneBadge-MKA1xeYc.js +60 -0
  17. package/assets/assets/{TrafficPage-C5fhZGJ2.js → TrafficPage-hUWYv-HT.js} +1 -1
  18. package/assets/assets/{TrafficSourceDetailPage-BoqARHYz.js → TrafficSourceDetailPage-CGHgPxs8.js} +1 -1
  19. package/assets/assets/{account-context-D_CDYrs8.js → account-context-qX-Es0RK.js} +1 -1
  20. package/assets/assets/{card-C6z0XyOo.js → card-CzROANT5.js} +1 -1
  21. package/assets/assets/{extract-error-message-By_wcU8g.js → extract-error-message-BY1STLiX.js} +1 -1
  22. package/assets/assets/{format-helpers-D3Ubm8vk.js → format-helpers-BNQR1HNK.js} +1 -1
  23. package/assets/assets/{index-DSVFZqv6.js → index-LmB57HOs.js} +4 -4
  24. package/assets/assets/{query-client-BETh6SO9.js → query-client-Gz37p16p.js} +1 -1
  25. package/assets/assets/{react-query-DzDr4w41.js → react-query-C6DtBAYQ.js} +1 -1
  26. package/assets/assets/{react-sigma_core.esm.min-3kgtOWbQ.js → react-sigma_core.esm.min-CgxuyPPF.js} +1 -1
  27. package/assets/assets/{service-BUaUaize.js → service-DyHZWnMe.js} +1 -1
  28. package/assets/index.html +7 -7
  29. package/dist/{ads-7WUY5CVC.js → ads-FOUJNHP4.js} +3 -3
  30. package/dist/{agent-NOA33LVM.js → agent-UVDVH7LK.js} +7 -7
  31. package/dist/{agent-ask-PKQDFJ7K.js → agent-ask-UEYN2SQX.js} +3 -3
  32. package/dist/{agent-conversations-7HVWPSFB.js → agent-conversations-F54376Q2.js} +3 -3
  33. package/dist/{agent-memory-Y4M2EWKA.js → agent-memory-SOHBTJ4U.js} +3 -3
  34. package/dist/{agent-providers-P67FWELV.js → agent-providers-MMEK2H6V.js} +3 -3
  35. package/dist/{agent-transcript-C2BYWPRT.js → agent-transcript-5IJ3VAEV.js} +3 -3
  36. package/dist/{analytics-XHYWNNW7.js → analytics-LDWO5IW6.js} +3 -3
  37. package/dist/{apply-6OIWDXQP.js → apply-PSGFDJIC.js} +3 -3
  38. package/dist/{backfill-KQPXCUUJ.js → backfill-HZU3BBOH.js} +9 -9
  39. package/dist/{backlinks-ASYIZFLQ.js → backlinks-X6DXIL2O.js} +3 -3
  40. package/dist/{bing-ZLKJ3MIL.js → bing-3HMFUEOO.js} +3 -3
  41. package/dist/{bootstrap-KMVQY5JD.js → bootstrap-ZUSSLXCI.js} +10 -10
  42. package/dist/{cdp-V2HX4ZLE.js → cdp-FH627HJ6.js} +7 -7
  43. package/dist/{chunk-GSHHYR24.js → chunk-2LAL5W6J.js} +3 -3
  44. package/dist/{chunk-X3GWBK7N.js → chunk-5VAC4KQ2.js} +3 -3
  45. package/dist/{chunk-WKZZILBI.js → chunk-5YC6IU44.js} +2 -2
  46. package/dist/{chunk-5NYS3ABL.js → chunk-67262NJL.js} +2 -2
  47. package/dist/{chunk-2LY4FY4N.js → chunk-A57HYHJ3.js} +1 -1
  48. package/dist/{chunk-KLGHY67V.js → chunk-CWMJQ2JV.js} +4 -4
  49. package/dist/{chunk-YQMQWDTI.js → chunk-DBA374EM.js} +72 -29
  50. package/dist/{chunk-U2WPZZPQ.js → chunk-EHTSCLEL.js} +1 -1
  51. package/dist/{chunk-IVZMXHXE.js → chunk-ERJ54U2I.js} +1 -1
  52. package/dist/{chunk-IALK4DQJ.js → chunk-GRWZC3GQ.js} +1 -1
  53. package/dist/{chunk-YQGVA64Q.js → chunk-H22RCHYH.js} +22 -6
  54. package/dist/{chunk-RGD5SDT3.js → chunk-HNZQVIPS.js} +1 -1
  55. package/dist/{chunk-3NICRFTU.js → chunk-I5QSS3CY.js} +351 -333
  56. package/dist/{chunk-UVDBUMYV.js → chunk-IDQZTPRG.js} +2 -2
  57. package/dist/{chunk-B5CSCGVD.js → chunk-L2QPAUTQ.js} +2 -1
  58. package/dist/{chunk-CJC4AQ2M.js → chunk-LBD7YXVD.js} +2 -2
  59. package/dist/{chunk-SDZKHJ6W.js → chunk-LEG7F3DN.js} +20 -2
  60. package/dist/{chunk-MMRJUOE2.js → chunk-NOQCIXM6.js} +1 -1
  61. package/dist/{chunk-Q7PTBHNG.js → chunk-OIHJJHJR.js} +1 -1
  62. package/dist/{chunk-YBDUZBGU.js → chunk-PH7RTAWW.js} +1 -1
  63. package/dist/{chunk-2HNTNKFC.js → chunk-Q4U2GJME.js} +393 -88
  64. package/dist/{chunk-LAILXR2F.js → chunk-QNFX7ZCT.js} +1 -1
  65. package/dist/{chunk-UUGT7UMF.js → chunk-QPTSZDHE.js} +4 -4
  66. package/dist/{chunk-SYQLUOR7.js → chunk-SG2OPR7J.js} +1 -1
  67. package/dist/{chunk-DAKJBAUN.js → chunk-SN5A767O.js} +1 -1
  68. package/dist/{chunk-YJAN2ZDV.js → chunk-SQYYAYH6.js} +3 -3
  69. package/dist/{chunk-3B2GBWIA.js → chunk-TDWNONBU.js} +5 -5
  70. package/dist/{chunk-U6CNJC2Y.js → chunk-TKLE2Y6H.js} +35 -1
  71. package/dist/{chunk-KTQVRQDM.js → chunk-UBP3AAKA.js} +1 -1
  72. package/dist/{chunk-APQ5EAL2.js → chunk-UYPBR3SF.js} +2 -2
  73. package/dist/{chunk-GMP7OZDW.js → chunk-W5TIRS4U.js} +4 -4
  74. package/dist/{chunk-AO4P75DI.js → chunk-WYTRXYUR.js} +5 -5
  75. package/dist/{chunk-7EWDQUNH.js → chunk-XWDCLX55.js} +8 -8
  76. package/dist/{chunk-OJVXKPIN.js → chunk-ZJTU73GW.js} +3 -3
  77. package/dist/{citations-NOCVQPPU.js → citations-BKWLP5S3.js} +3 -3
  78. package/dist/{cli-catalog-WOWYZ7Q2.js → cli-catalog-VRJS66GR.js} +3 -3
  79. package/dist/{cli-commands-4YSZAPKZ.js → cli-commands-6LZ5OIIP.js} +7 -7
  80. package/dist/cli.js +13 -13
  81. package/dist/{competitor-KQK66W3M.js → competitor-VELMM2TJ.js} +3 -3
  82. package/dist/{content-OHOQO5LP.js → content-I525MITL.js} +3 -3
  83. package/dist/{dashboard-W6MII62Z.js → dashboard-XJJ6NZ2L.js} +6 -6
  84. package/dist/{demo-23H25IOO.js → demo-X7LQ24GS.js} +1 -1
  85. package/dist/{demo-server-WYYWTH4H.js → demo-server-MBPBTJXG.js} +11 -11
  86. package/dist/{discover-557WRGEK.js → discover-E2CV5ZP7.js} +3 -3
  87. package/dist/{discover-eval-GGRZZHHI.js → discover-eval-ESRCWRHP.js} +3 -3
  88. package/dist/{doctor-D5JNJKVE.js → doctor-BQDVERII.js} +3 -3
  89. package/dist/{evidence-DHYYVPWW.js → evidence-NUULK5XD.js} +3 -3
  90. package/dist/{export-cmd-M24S3YBY.js → export-cmd-K324RTN4.js} +3 -3
  91. package/dist/{ga-XM6WXZNP.js → ga-PQ36UFTK.js} +3 -3
  92. package/dist/{gbp-M3S5XYPG.js → gbp-KTYA6KHR.js} +3 -3
  93. package/dist/{google-PULCSUOU.js → google-3D4E7TJX.js} +4 -4
  94. package/dist/{google-marketing-AA625HA4.js → google-marketing-4FRO4PRN.js} +1 -1
  95. package/dist/{health-cmd-LVEW6OBD.js → health-cmd-CZJOZUAQ.js} +3 -3
  96. package/dist/{history-32ZYGZGQ.js → history-JNH34GAM.js} +3 -3
  97. package/dist/index.d.ts +6 -0
  98. package/dist/index.js +24 -24
  99. package/dist/{init-4YT2HMKY.js → init-JRGRHLYU.js} +11 -11
  100. package/dist/{insights-RK3MJ4UM.js → insights-STYJSZBB.js} +3 -3
  101. package/dist/intelligence-service-7LCTJQDL.js +15 -0
  102. package/dist/{journeys-S4CPNZYU.js → journeys-POTSSBOM.js} +3 -3
  103. package/dist/{keys-WCXE7WXV.js → keys-BPCSPX2O.js} +3 -3
  104. package/dist/{keyword-HHVLC4HI.js → keyword-6SML434W.js} +3 -3
  105. package/dist/{logs-VYESWGAN.js → logs-WTKKH4UJ.js} +3 -3
  106. package/dist/{mcp-4LRFIOLC.js → mcp-HTAHMVRC.js} +2 -2
  107. package/dist/mcp.js +9 -9
  108. package/dist/{measurement-plan-AL3EHWRK.js → measurement-plan-LJHKKRZ7.js} +5 -5
  109. package/dist/{notify-LUDG5PWU.js → notify-EARS4LMG.js} +3 -3
  110. package/dist/{organic-evidence-CIOHMKLD.js → organic-evidence-W7ZQQKPL.js} +3 -3
  111. package/dist/{overview-NAZJ6PJG.js → overview-C7KQD4NR.js} +4 -4
  112. package/dist/{project-N3MU5V6T.js → project-YU2FIL4B.js} +3 -3
  113. package/dist/{query-XMI2FV6G.js → query-SYFLWHFA.js} +3 -3
  114. package/dist/{research-MQ6QA5TV.js → research-UOUXMFBV.js} +3 -3
  115. package/dist/{results-export-OFEZXCJK.js → results-export-VYPG2Z6S.js} +3 -3
  116. package/dist/{run-YQS3LCFT.js → run-GZD3MQGS.js} +4 -4
  117. package/dist/{schedule-DF6HCMXN.js → schedule-PAA2NLGF.js} +3 -3
  118. package/dist/{search-MRDJKVOL.js → search-6RWCUEEY.js} +3 -3
  119. package/dist/{serve-5A4K242B.js → serve-FTAFEEJD.js} +25 -25
  120. package/dist/{session-5OXLS7NP.js → session-E4V5MRUE.js} +12 -12
  121. package/dist/{settings-NIF34QPH.js → settings-TTA775EJ.js} +3 -3
  122. package/dist/{skills-JOSNWG7B.js → skills-GA35KKOV.js} +2 -2
  123. package/dist/{snapshot-32DLVRIJ.js → snapshot-QVQVAMMN.js} +3 -3
  124. package/dist/{src-F63SHN5T.js → src-CTHVFVLF.js} +2 -2
  125. package/dist/{status-IERHHP4C.js → status-E7B726S6.js} +4 -4
  126. package/dist/{technical-aeo-D7CA3CK6.js → technical-aeo-SMPN3QDQ.js} +3 -3
  127. package/dist/{telemetry-DRURFCCJ.js → telemetry-CTBFPMWD.js} +4 -4
  128. package/dist/{tools-3IZMSHSY.js → tools-XKGEC56T.js} +11 -11
  129. package/dist/{traffic-AME5NDMN.js → traffic-XACOSYYM.js} +36 -7
  130. package/dist/{users-4CQ6ATAG.js → users-WJC7SGRL.js} +3 -3
  131. package/dist/{visibility-stats-R5JBISBX.js → visibility-stats-H3ZNFV5M.js} +3 -3
  132. package/dist/{wordpress-GFK4GR3O.js → wordpress-72YMWF7F.js} +3 -3
  133. package/package.json +1 -1
  134. package/assets/assets/ToneBadge-DKvZcONW.js +0 -60
  135. package/dist/intelligence-service-UNKFCG5N.js +0 -15
@@ -183,6 +183,12 @@ 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.
@@ -685,7 +685,7 @@ Available events: `citation.lost`, `citation.gained`, `run.completed`, `run.fail
685
685
 
686
686
  `insight.critical` and `insight.high` fire when the intelligence engine generates critical- or high-severity insights after a sweep completes.
687
687
 
688
- **Health alerts** (`health.degraded`, `health.recovered`) report on the measurement itself, not on findings, and reach every enabled webhook whether or not it subscribes to them. A doctor pass runs every 6 hours per project. `health.degraded` is sent only when something is new or worse: the project stops being healthy, the worst status rises, or a check starts failing, gets more severe, or reports a different code at the same severity. A pass that finds the same problems, or fewer, sends nothing, so a check clearing, or easing from fail to warn, never produces a second "degraded" alert. An alert that no webhook accepted is sent again on the next pass. `health.recovered` is sent when every check passes again. The payload's `health.failing` lists every failing check; `health.added` lists the codes that are new or worse and `health.cleared` the codes of the checks that stopped failing since the last delivered alert. Discord and Slack cards show them as `New (n)` and `Cleared (n)`, ahead of `Also failing`. Silent advisories (`report.*`, `content.winnability.coverage`, `traffic.source.cache-blindspot`) appear in `cnry doctor` with `notificationPolicy: "silent"` but never page. An optional integration the project does not use is `skipped`, not failed (`gbp.auth.not-connected`, `bing.auth.not-connected`), so it does not page either. Run `cnry doctor --project <project>` to see the current state at any time.
688
+ **Health alerts** (`health.degraded`, `health.recovered`) report on the measurement itself, not on findings, and reach every enabled webhook whether or not it subscribes to them. A doctor pass runs every 6 hours per project. `health.degraded` is sent only when something is new or worse: the project stops being healthy, the worst status rises, or a check starts failing, gets more severe, or reports a different code at the same severity. A pass that finds the same problems, or fewer, sends nothing, so a check clearing, or easing from fail to warn, never produces a second "degraded" alert. An alert that no webhook accepted is sent again on the next pass. `health.recovered` is sent when every check passes again. The payload's `health.failing` lists every failing check; `health.added` lists the codes that are new or worse and `health.cleared` the codes of the checks that stopped failing since the last delivered alert. Each `failing` entry carries its own `remediation` and, when the check wrote plain wording, `alert` (`label`, `line`, `next`). Discord and Slack cards lead with the first new or worse problem: a short label in the title, one line on what happened, and a `Next` step when there is one. Other open problems follow under `Also open` as labels, new ones first and marked "(new)", three at most with the rest counted, and what cleared follows under `Fixed`. Check ids and codes stay in the payload and behind the dashboard link. Silent advisories (`report.*`, `content.winnability.coverage`, `traffic.source.cache-blindspot`) appear in `cnry doctor` with `notificationPolicy: "silent"` but never page. An optional integration the project does not use is `skipped`, not failed (`gbp.auth.not-connected`, `bing.auth.not-connected`), so it does not page either. Run `cnry doctor --project <project>` to see the current state at any time.
689
689
 
690
690
  **Urgent health alerts** (`health.urgent`, `health.urgent-resolved`) carry only the problems a person has to act on now, for a channel people watch. A check marks its own result `urgent` (it shows as `urgent: true` in `cnry doctor --format json`):
691
691
 
@@ -994,6 +994,7 @@ cnry traffic status <project> --format json
994
994
  cnry traffic analytics <project> --period 30 --format json # complete 7|14|30|90-day totals, prior-period changes and breakdowns
995
995
  cnry traffic events <project> --source <source-id> --format json
996
996
  cnry traffic referral-assessment <project> --start-date 2026-08-01 --end-date 2026-08-31 --burst-threshold 100 --ratio-threshold 3 --limit 100 --format json
997
+ cnry traffic declare-gap <project> --source <source-id> # record an unrecovered skip as lost (WordPress, or past provider retention); clears the doctor check
997
998
 
998
999
  cnry doctor --project <project> --check 'traffic.source.*' --format json
999
1000
  cnry schedule show <project> --kind traffic-sync --format json
@@ -232,9 +232,12 @@ bounded-window-capable Canonry traffic logger before syncing again.
232
232
  Do not run generic replace-mode backfill for a WordPress source. The plugin
233
233
  prunes retained events and cannot prove that it covers every bucket a replace
234
234
  transaction would delete, even when no continuation is pending. Canonry
235
- rejects that generic path universally. Keep existing rollups intact, use a
236
- retention-aware repair, and explicitly declare any unrecoverable portion before
237
- reporting a historical trend.
235
+ rejects that generic path universally. Keep existing rollups intact. Once a
236
+ skipped span cannot be recovered, record it with
237
+ `cnry traffic declare-gap <project> --source <id>`: the span goes into the audit
238
+ log, the unrecovered marker clears, and the `traffic.sync-lag.unrecovered-skip`
239
+ doctor check stops failing. Mention the gap before reporting a historical trend
240
+ across it.
238
241
 
239
242
  After upgrading from the former unbounded WordPress sync, a source with an old
240
243
  continuation cursor is intentionally rejected rather than guessed at. Keep its
@@ -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-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
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-MKA1xeYc.js";import{r as _}from"./account-context-qX-Es0RK.js";import{t as v}from"./async-handler-Bj3TnIRw.js";import{X as y,et as b}from"./index-LmB57HOs.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-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
+ 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-MKA1xeYc.js";import{Jt as s,Y as c,i as l}from"./react-query-C6DtBAYQ.js";import{t as u}from"./card-CzROANT5.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-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
+ 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-MKA1xeYc.js";import{t as b}from"./async-handler-Bj3TnIRw.js";import{t as x}from"./card-CzROANT5.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-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};
1
+ import{r as e}from"./vendor-markdown-9SSeLNHZ.js";import{t}from"./AuditHistoryPanel-DlgI3o-U.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};