@canonry/canonry 4.159.0 → 4.161.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 (30) hide show
  1. package/README.md +1 -1
  2. package/assets/agent-workspace/skills/aero/SKILL.md +16 -1
  3. package/assets/agent-workspace/skills/canonry/SKILL.md +17 -5
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +66 -0
  5. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +231 -62
  6. package/assets/assets/{AuditHistoryPanel-BiloGcQA.js → AuditHistoryPanel-C_1twF00.js} +1 -1
  7. package/assets/assets/{BacklinksPage-CPM1O2bC.js → BacklinksPage-CfAuMCaN.js} +1 -1
  8. package/assets/assets/{HistoryPage-1YOjtdJy.js → HistoryPage-DrRxOL3j.js} +1 -1
  9. package/assets/assets/{MeasurementPropertyPage-CVDTY_gC.js → MeasurementPropertyPage-BFtxx5Jw.js} +1 -1
  10. package/assets/assets/{ProjectPage-DnGXMx1n.js → ProjectPage-Djvhx4HD.js} +9 -9
  11. package/assets/assets/{RunRow-fIaadH4h.js → RunRow-DVZXibR2.js} +1 -1
  12. package/assets/assets/{RunsPage-DfifdM5z.js → RunsPage-CAUMy7By.js} +1 -1
  13. package/assets/assets/{SettingsPage-efA2EEmS.js → SettingsPage-CcSOmQAl.js} +1 -1
  14. package/assets/assets/{SiteHealthSection-BROtQGfu.js → SiteHealthSection-DYjEWd1I.js} +3 -3
  15. package/assets/assets/{TrafficPage-Wk43Bm_q.js → TrafficPage-BWM33jnJ.js} +1 -1
  16. package/assets/assets/{TrafficSourceDetailPage-BhBvoLWu.js → TrafficSourceDetailPage-DvEZ_JGY.js} +1 -1
  17. package/assets/assets/{extract-error-message-YGzZFvWs.js → extract-error-message-CnKOJkYB.js} +1 -1
  18. package/assets/assets/{index-DmsIZQAm.js → index-CQGAqmDx.js} +22 -22
  19. package/assets/assets/{react-sigma_core.esm.min-DM9XptbM.js → react-sigma_core.esm.min-CHCAIMBW.js} +1 -1
  20. package/assets/index.html +1 -1
  21. package/dist/{chunk-ZL3VY435.js → chunk-EXDO7EPL.js} +104 -9
  22. package/dist/{chunk-ZEG43TY2.js → chunk-HCGTKQCG.js} +21 -1
  23. package/dist/{chunk-5AFWHKKI.js → chunk-J27J2DF6.js} +2095 -703
  24. package/dist/{chunk-BZPBVOAM.js → chunk-W6OWEPZG.js} +70 -30
  25. package/dist/cli.js +308 -124
  26. package/dist/index.d.ts +28 -14
  27. package/dist/index.js +4 -4
  28. package/dist/{intelligence-service-DHY5CYWX.js → intelligence-service-HKDIVTZB.js} +2 -2
  29. package/dist/mcp.js +2 -2
  30. package/package.json +8 -7
package/README.md CHANGED
@@ -94,7 +94,7 @@ Configure during `cnry init`, in the dashboard `/settings`, or as env vars.
94
94
  | **Aero — built-in agent** | [skills/aero/SKILL.md](skills/aero/SKILL.md) |
95
95
  | **Agent Plugin — portable core + Codex / Claude adapters** | [docs/plugins.md](docs/plugins.md) |
96
96
  | **MCP — Claude Desktop / Cursor / Codex** | [docs/mcp.md](docs/mcp.md) |
97
- | **Integrations** | [GSC](docs/google-search-console-setup.md) · [GA4](docs/google-analytics-setup.md) · [Bing](docs/bing-webmaster-setup.md) · [Google Business Profile](skills/canonry/references/google-business-profile.md) · [WordPress](docs/wordpress-setup.md) · [Server-side traffic (Cloudflare direct push, Cloud Run, Vercel, WordPress)](skills/canonry/references/server-side-traffic.md) |
97
+ | **Integrations** | [GSC](docs/google-search-console-setup.md) · [GA4](docs/google-analytics-setup.md) · [Bing](docs/bing-webmaster-setup.md) · [Google Business Profile](skills/canonry/references/google-business-profile.md) · [WordPress](docs/wordpress-setup.md) · [Server-side traffic (Cloudflare direct push or Queue pull, Cloud Run, Vercel, WordPress)](skills/canonry/references/server-side-traffic.md) |
98
98
  | **Deployment** — Docker, Railway, Render, systemd, Tailscale | [docs/deployment.md](docs/deployment.md) |
99
99
  | **API** — 118+ endpoints | `GET /api/v1/openapi.json` (no auth) |
100
100
  | **Standalone skills bundle** for Claude Code / Codex | `cnry skills install` ([details](skills/canonry/SKILL.md)) |
@@ -16,7 +16,22 @@ Persist only *user-scoped* context (operator preferences, communication style) i
16
16
 
17
17
  **Two signals, not one.** Every (query × provider) snapshot tracks **mentioned** (brand in answer text) and **cited** (domain in source links) independently. Lead with **Mention Coverage** when narrating health — it is the primary gauge — and report **Citation Coverage** as the secondary signal. Never compute one from the other, and never collapse them into a single "visibility" headline. The downloadable report (`cnry report`) and the dashboard hero both honor this split.
18
18
 
19
- When a project has GA4 connected, traffic is a first-class signal alongside mentions and citations. Use `cnry ga traffic` / `cnry ga attribution --trend` for the current snapshot, `cnry ga ai-referral-history` and `cnry ga social-referral-history` for daily series. Reads query a local DB synced by `cnry ga sync` — confirm `cnry ga status` shows a recent `lastSyncedAt` before quoting numbers; if stale, ask for explicit approval before re-syncing. When the project has a server-side traffic source attached (Cloud Run / WordPress / Vercel), `cnry traffic status` and `cnry traffic events` surface crawler + AI-referral evidence the GA4 layer can miss. Full command reference and return shapes live in the co-installed `canonry/references/canonry-cli.md`.
19
+ When a project has GA4 connected, traffic is a first-class signal alongside
20
+ mentions and citations. Use `cnry ga traffic` and `cnry ga attribution --trend`
21
+ for the current snapshot. Use the GA referral-history commands for daily series.
22
+ Before you quote GA4 data, make sure that `cnry ga status` has a recent
23
+ `lastSyncedAt`. If it is stale, get approval before you run `cnry ga sync`.
24
+
25
+ For Cloud Run, WordPress, Vercel, or Cloudflare, use `cnry traffic status` and
26
+ `cnry traffic events` for crawler and AI-referral evidence. Read the Cloudflare
27
+ `deliveryMode` before you recommend an action. Direct push does not use
28
+ `traffic sync`. Queue pull freshness requires an enabled `traffic-sync`
29
+ schedule. Run the `traffic.source.*` doctor checks. Inspect
30
+ `traffic.source.queue-backlog` before you quote current Queue data. If more than
31
+ 1,000 messages remain, report that one default tick cannot drain the backlog.
32
+ Get approval before you run a manual sync or change the schedule. The full
33
+ command reference is in the co-installed
34
+ `canonry/references/canonry-cli.md`.
20
35
 
21
36
  **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).
22
37
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canonry
3
- description: "Set up and operate Canonry AEO projects: inspect mention and citation coverage, diagnose regressions, run technical audits, and act through the Canonry CLI or MCP tools. Examples: inspect project acme coverage (run + report), diagnose query drift via attribution (report.html + visibility-attribution), submit GSC sitemaps (gsc-sitemap-submission batched)."
3
+ description: "Set up and operate Canonry AEO projects. Inspect mention and citation coverage, diagnose regressions, and run technical audits. Connect Cloudflare direct-push or queue-pull traffic. Act through the Canonry CLI or MCP tools. Examples: inspect project acme coverage (run + report), diagnose query drift via attribution (report.html + visibility-attribution), or submit GSC sitemaps (gsc-sitemap-submission batched)."
4
4
  compatibility: Requires Node.js 22.14+ and globally installed @canonry/canonry; canonry-mcp must be on PATH.
5
5
  metadata:
6
6
  agent: >-
@@ -131,10 +131,22 @@ GA4 is a first-class signal alongside citation tracking. Connect once with `cnry
131
131
 
132
132
  When the project ships behind a server you control, connect Cloud Run, WordPress,
133
133
  or Vercel. `cnry traffic sync` pulls and classifies their logs into hourly
134
- buckets. For a Cloudflare site, `cnry traffic connect cloudflare <project> ...`
135
- deploys a direct-push Worker from the credential-owning host. Cloudflare direct
136
- push does not use `traffic sync`. Use `cnry traffic events / sources / status`
137
- for every adapter. See `references/server-side-traffic.md` for setup.
134
+ buckets. For Cloudflare, choose `direct-push` or `queue-pull` on the local,
135
+ credential-owning host. Direct push requires a public Canonry HTTPS receiver.
136
+ It does not use `traffic sync`. Queue pull sends events to a Cloudflare Queue,
137
+ then Canonry drains it through `traffic sync` and the `traffic-sync` schedule.
138
+
139
+ If the connect response reports `activationRequired`, attach the Worker route
140
+ first. Then run `cnry traffic activate <project> --source <id>`. Activation
141
+ pauses all sibling sources. It also moves the one `traffic-sync` schedule to
142
+ Queue pull. A first source becomes active automatically.
143
+
144
+ Each default Queue tick drains at most 1,000 messages. Use `cnry traffic status`
145
+ and the `traffic.source.queue-backlog` doctor check to read the backlog. If more
146
+ than 1,000 messages remain, get approval before a manual sync or a shorter
147
+ schedule interval. Use `cnry traffic events`, `cnry traffic sources`, and
148
+ `cnry traffic status` for every adapter. See
149
+ `references/server-side-traffic.md` for setup and smoke tests.
138
150
 
139
151
  **Vercel gotcha:** a freshly connected Vercel source captures only going-forward traffic — `lastSyncedAt` is seeded to NOW to avoid the 30-day default window exceeding Vercel's ~14-day request-logs retention (which would otherwise throw on every first sync). Use `cnry traffic backfill <project> --source <id> --days N` for historical recovery. If an idle Vercel/Cloud Run source has been failing long enough that `lastSyncedAt` aged past retention, unstick it with `cnry traffic reset <project> --source <id> --advance-to-now`.
140
152
 
@@ -564,6 +564,72 @@ cnry wordpress onboard <project> --url <url> --user <user> # full onboarding wo
564
564
 
565
565
  **Onboard** runs: connect → audit → set-meta → schema deploy → Google submit → Bing submit. Use `--skip-schema` or `--skip-submit` to skip steps. `--profile <file>` provides business data and page-to-schema mapping for schema deployment.
566
566
 
567
+ ## Server-Side Traffic
568
+
569
+ Cloudflare supports direct push and Queue pull. Run Cloudflare connect only from
570
+ the local host that owns the Canonry configuration and Wrangler profile. This
571
+ operation is not available through MCP because it uses local credentials.
572
+
573
+ ```bash
574
+ # Direct push to a stable public Canonry HTTPS receiver:
575
+ cnry traffic connect cloudflare <project> \
576
+ --delivery-mode direct-push \
577
+ --zone-id <zone-id> --account-id <account-id> \
578
+ --deploy --confirm-route --confirm-fail-open
579
+
580
+ # Create the Queue and enable its HTTP pull consumer first:
581
+ wrangler queues create canonry-traffic-<project>
582
+ # Workers Paid only: use this command to change the four-day default.
583
+ wrangler queues update canonry-traffic-<project> \
584
+ --message-retention-period-secs <seconds>
585
+ wrangler queues info canonry-traffic-<project>
586
+ wrangler queues consumer http add canonry-traffic-<project>
587
+
588
+ # Queue pull keeps the API token in the local Canonry credential store:
589
+ cnry traffic connect cloudflare <project> \
590
+ --delivery-mode queue-pull \
591
+ --zone-id <zone-id> --account-id <account-id> \
592
+ --queue-id <queue-id> --queue-name canonry-traffic-<project> \
593
+ --api-token-file <mode-0600-token-file> \
594
+ --retention-seconds <actual-queue-retention-seconds> \
595
+ --deploy --confirm-route --confirm-fail-open
596
+ ```
597
+
598
+ Workers Free retention is fixed at `86400` seconds. Workers Paid defaults to
599
+ `345600` seconds. If you change paid retention with `wrangler queues update`,
600
+ pass the same value to Canonry. The Canonry flag does not change the Queue.
601
+
602
+ Both commands deploy an unattached Worker. Attach the exact site route in the
603
+ Cloudflare Dashboard. Then set its request-limit failure mode to **Fail open**.
604
+ Do not put the Queue API token on the command line or in an agent transcript.
605
+
606
+ ```bash
607
+ # If connect reports activationRequired, activate after the route is live:
608
+ cnry traffic activate <project> --source <source-id>
609
+ cnry traffic sync <project> --source <source-id> # pull adapters, including Cloudflare Queue pull
610
+ cnry traffic sources <project> --format json
611
+ cnry traffic status <project> --format json
612
+ cnry traffic events <project> --source <source-id> --format json
613
+
614
+ cnry doctor --project <project> --check 'traffic.source.*' --format json
615
+ cnry schedule show <project> --kind traffic-sync --format json
616
+ cnry schedule set <project> --kind traffic-sync \
617
+ --source <source-id> --cron "*/10 * * * *"
618
+ ```
619
+
620
+ A first source becomes active automatically. A staged source stays paused until
621
+ the explicit activation command. Activation pauses sibling sources and moves the
622
+ one `traffic-sync` schedule for the project. Direct push rejects `traffic sync` and
623
+ does not use this schedule.
624
+
625
+ Queue pull drains at most 1,000 messages in one default sync. The doctor warns
626
+ when the remaining backlog is more than 1,000 messages. If the operator approves
627
+ a manual drain, run a manual sync. If the backlog recurs, get approval to shorten
628
+ the schedule interval.
629
+
630
+ Read the [server-side traffic guide](server-side-traffic.md) for token safety,
631
+ route checks, activation order, smoke tests, rollback, and troubleshooting.
632
+
567
633
  ## Google Analytics 4
568
634
 
569
635
  GA4 integration uses service account authentication (no OAuth). The service account must have Viewer access on the GA4 property. `ga sync` writes to four DB tables (`gaTrafficSnapshots`, `gaAiReferrals`, `gaSocialReferrals`, `gaTrafficSummaries`); every subsequent read command queries the local store rather than re-fetching from GA4, so reads are fast and quotaless. AI-referral rows are tracked across 10 known providers (chatgpt, perplexity, claude, gemini, openai, anthropic, copilot, phind, you.com, meta.ai), three GA4 attribution dimensions (`session` / `first_user` / `manual_utm`), and joined to landing pages. Social referrals are split Organic vs Paid via GA4's `sessionDefaultChannelGroup`. All commands support `--format json`.
@@ -39,11 +39,12 @@ Adapters today:
39
39
  | [`cloud-run`](#connecting-a-cloud-run-source) | Pull | GCP Cloud Run request logs via Logging API | Any service running on Cloud Run |
40
40
  | [`wordpress`](#connecting-a-wordpress-source) | Pull | Canonry Traffic Logger REST endpoint | WordPress sites where you control wp-admin |
41
41
  | [`vercel`](#connecting-a-vercel-source) | Pull | Vercel project logs via the Vercel API | Sites deployed on Vercel |
42
- | [`cloudflare`](#connecting-a-cloudflare-source-direct-push) | Direct push | A zone Worker selects and sends edge events | Sites whose public traffic passes through Cloudflare |
42
+ | [`cloudflare`](#connecting-a-cloudflare-source) | Direct push or Queue pull | A zone Worker selects edge events; Canonry either receives them directly or pulls them from Cloudflare Queues | Sites whose public traffic passes through Cloudflare |
43
43
 
44
44
  Future adapters slot in by implementing the same contract.
45
- Cloud Run, WordPress, and Vercel pull logs during `traffic sync`.
46
- Cloudflare sends selected events from its Worker and does not use `traffic sync`.
45
+ Cloud Run, WordPress, Vercel, and Cloudflare Queue sources pull during
46
+ `traffic sync`. Cloudflare direct push sends selected events from its Worker
47
+ and does not use `traffic sync`.
47
48
 
48
49
  ## Connecting a Cloud Run source
49
50
 
@@ -195,31 +196,38 @@ Run `cnry traffic backfill <project> --source <id> --days N` (capped at
195
196
  history. It's an explicit operator action; the connect flow never pulls
196
197
  it implicitly.
197
198
 
198
- ## Connecting a Cloudflare source (direct push)
199
+ ## Connecting a Cloudflare source
199
200
 
200
- Cloudflare direct push runs a small ES-module Worker on the site's exact
201
- zone route. It applies a broad AI filter and sends selected edge events
202
- to Canonry in `ctx.waitUntil()`. The origin response remains independent
203
- from filtering, scheduling, and delivery errors. The event shape is also
204
- the contract for the later Queue pull transport.
201
+ Both Cloudflare modes run the same small ES-module Worker on the site's exact
202
+ zone route. It applies a broad AI filter and delivers selected edge events in
203
+ `ctx.waitUntil()`. The origin response remains independent from filtering,
204
+ scheduling, and delivery errors. Both modes use the same
205
+ `CloudflareEdgeEventBatch`, normalizer, classifier, receipts, and rollups.
205
206
 
206
- The shipped adapter does not pull Cloudflare analytics or request logs.
207
- Queue pull has a reserved transport mode, but it is not available yet.
207
+ - `direct-push` sends a signed batch to a public Canonry HTTPS receiver.
208
+ - `queue-pull` sends the batch to a Cloudflare Queue. The single-team Canonry
209
+ server pulls and acknowledges it through the Cloudflare Queues HTTP API.
210
+
211
+ The adapter does not pull Cloudflare analytics or request logs.
208
212
 
209
213
  ```mermaid
210
214
  flowchart LR
211
215
  request["Site request"] --> worker["Canonry Cloudflare Worker"]
212
216
  worker --> origin["Existing origin or Pages site"]
213
217
  origin --> response["Unchanged site response"]
214
- worker -. "signed selected event via ctx.waitUntil" .-> ingest["Canonry public HTTPS ingest"]
215
- ingest --> classify["Shared traffic classifier"]
218
+ worker -. "direct-push" .-> ingest["Canonry public HTTPS ingest"]
219
+ worker -. "queue-pull" .-> queue["Cloudflare Queue"]
220
+ queue --> pull["Canonry scheduled pull"]
221
+ ingest --> classify["Shared receipt and traffic pipeline"]
222
+ pull --> classify
216
223
  classify --> rollups["Hourly crawler, user-fetch, and referral rollups"]
217
224
  ```
218
225
 
219
226
  ### Setup flow
220
227
 
221
228
  1. Create or select a Canonry project with one exact canonical hostname.
222
- 2. Give the Canonry server a stable public HTTPS URL outside the site route.
229
+ 2. Choose delivery: direct push needs a stable public Canonry HTTPS URL outside
230
+ the site route; Queue pull needs a pre-created Queue and HTTP pull consumer.
223
231
  3. Authenticate Wrangler with the Cloudflare account that owns the zone.
224
232
  4. Inspect existing Worker routes for the exact canonical hostname.
225
233
  5. Run the local Cloudflare connect command with `--deploy`.
@@ -228,20 +236,23 @@ flowchart LR
228
236
 
229
237
  ### Current support boundary
230
238
 
231
- Direct push currently supports a local `canonry serve` instance. The CLI
232
- and server must read the same `~/.canonry/config.yaml` credential store.
233
- You can expose that local server through a stable HTTPS tunnel.
239
+ Both modes currently target one local, single-team `canonry serve` instance.
240
+ The Queue consumer is not a multi-tenant Cloudflare control plane. The CLI and
241
+ server must read the same `~/.canonry/config.yaml` credential store. Direct
242
+ push requires a stable public HTTPS receiver; Queue pull does not.
234
243
 
235
244
  The Cloud Run `apps/api` service has no Cloudflare credential store.
236
- Therefore, `apps/api` cannot retain the per-source bearer and HMAC values
237
- that deployment requires.
245
+ Therefore, `apps/api` cannot retain direct-push bearer/HMAC values or the
246
+ Queue API token.
238
247
 
239
- CAUTION: Use one authoritative server-traffic source for the canonical
240
- site. Overlapping adapters capture the same requests and double-count
241
- project totals. This is a current limitation. Queue support must define
242
- source coverage before it permits a transport change.
248
+ CAUTION: Use one authoritative server-traffic source for the canonical site.
249
+ Connecting any adapter while a sibling source is active creates or keeps a
250
+ paused staged source. `canonry traffic activate` validates the target's local
251
+ credential, atomically pauses every sibling, and moves or removes the one
252
+ traffic-sync schedule. Overlapping adapters outside this cutover can still
253
+ double-count project totals.
243
254
 
244
- ### Expose the Canonry receiver
255
+ ### Expose the Canonry receiver (direct push only)
245
256
 
246
257
  Complete the local dashboard setup and set its password before you expose
247
258
  Canonry. A tunnel makes the loopback server reachable from the public Internet.
@@ -342,18 +353,20 @@ Worker and keeps the origin available. See [Cloudflare Workers limits](https://d
342
353
  Free-plan classification uses user-agent, referer, and UTM evidence. Granular
343
354
  `request.cf.botManagement` scores require the Enterprise Bot Management add-on.
344
355
 
345
- ### Generate or deploy
356
+ ### Generate or deploy direct push
346
357
 
347
358
  ```bash
348
359
  # Generate secret-free worker.js and wrangler.toml in
349
360
  # ./canonry-cloudflare-<project-slug>:
350
361
  cnry traffic connect cloudflare <project> \
362
+ --delivery-mode direct-push \
351
363
  --zone-id <cloudflare-zone-id> \
352
364
  --account-id <cloudflare-account-id>
353
365
 
354
366
  # Deploy only the Worker after the route preflight.
355
367
  # Both safety acknowledgements are mandatory:
356
368
  cnry traffic connect cloudflare <project> \
369
+ --delivery-mode direct-push \
357
370
  --zone-id <cloudflare-zone-id> \
358
371
  --account-id <cloudflare-account-id> \
359
372
  --deploy --confirm-route --confirm-fail-open
@@ -413,6 +426,112 @@ Cloudflare connect is absent from MCP because deployment reads local
413
426
  secrets. An agent can guide the CLI command. It must not request, print,
414
427
  or send either secret through chat.
415
428
 
429
+ ### Generate or deploy Queue pull
430
+
431
+ Queue pull reuses the same edge filter without requiring a public Canonry
432
+ receiver. Create the Queue first. Then use `wrangler queues info` to get its ID.
433
+ Create a Cloudflare API token with **Account Queues Edit** for the owning
434
+ account. Put only the token in a mode-0600 file. Keep that file off shell argv.
435
+
436
+ ```bash
437
+ wrangler queues create canonry-traffic-<project>
438
+
439
+ # Workers Paid only: use this command to change the four-day default.
440
+ wrangler queues update canonry-traffic-<project> \
441
+ --message-retention-period-secs <seconds>
442
+
443
+ wrangler queues info canonry-traffic-<project>
444
+ wrangler queues consumer http add canonry-traffic-<project>
445
+
446
+ cnry traffic connect cloudflare <project> \
447
+ --delivery-mode queue-pull \
448
+ --zone-id <cloudflare-zone-id> \
449
+ --account-id <cloudflare-account-id> \
450
+ --queue-id <cloudflare-queue-id> \
451
+ --queue-name canonry-traffic-<project> \
452
+ --api-token-file <path-to-mode-0600-token-file> \
453
+ --retention-seconds <actual-queue-retention-seconds>
454
+
455
+ cnry traffic connect cloudflare <project> \
456
+ --delivery-mode queue-pull \
457
+ --zone-id <cloudflare-zone-id> \
458
+ --account-id <cloudflare-account-id> \
459
+ --queue-id <cloudflare-queue-id> \
460
+ --queue-name canonry-traffic-<project> \
461
+ --api-token-file <path-to-token-file> \
462
+ --retention-seconds <actual-queue-retention-seconds> \
463
+ --deploy --confirm-route --confirm-fail-open
464
+ ```
465
+
466
+ Read the Queue ID from the `wrangler queues info` output. Pass that exact value
467
+ to `--queue-id`.
468
+
469
+ The `queues update` command sets Cloudflare retention on Workers Paid. The
470
+ Canonry `--retention-seconds` option only records the actual value. It does not
471
+ change the Queue. Use `86400` for Workers Free, where retention is fixed at one
472
+ day. Workers Paid defaults to `345600` (four days). Paid accounts accept values
473
+ from 60 seconds through 14 days. If you change the paid Queue, use the same
474
+ value in both commands. See Cloudflare's [Queue limits](https://developers.cloudflare.com/queues/platform/limits/).
475
+
476
+ The HTTP-consumer command is required. The producer binding does not enable
477
+ HTTP pull. Cloudflare does not support an HTTP pull consumer in
478
+ `wrangler.toml`. Remove any existing Worker consumer before you enable HTTP
479
+ pull. One Queue cannot use both consumer types at the same time. See
480
+ Cloudflare's [pull-consumer setup](https://developers.cloudflare.com/queues/configuration/pull-consumers/).
481
+
482
+ The token stays only in Canonry's local credential store. It is not written to
483
+ the source row, Worker, TOML, command output, MCP, or Worker bindings. Wrangler
484
+ authentication remains the operator's active Wrangler profile. Canonry does
485
+ not reuse the Queue pull token for deployment.
486
+
487
+ The generated TOML contains one `[[queues.producers]]` binding named
488
+ `CANONRY_TRAFFIC_QUEUE` and no direct-push ingest URL or Worker secrets. After
489
+ the Worker is deployed and the route is ready, activate a staged source:
490
+
491
+ ```bash
492
+ cnry traffic activate <project> --source <source-id>
493
+ cnry traffic sync <project> --source <source-id>
494
+ ```
495
+
496
+ A new project with no other active traffic source activates immediately. A
497
+ mode change stays paused until the explicit activation command. Activation
498
+ pauses the prior source and points the project's one `traffic-sync` schedule at
499
+ Queue pull; activating direct push removes that Queue schedule.
500
+
501
+ For Queue-to-direct rollback, drain Queue pull until its backlog is empty,
502
+ switch the Cloudflare route, then activate direct push immediately. The route
503
+ change and Canonry activation are separate operator actions, so keep the
504
+ cutover window short.
505
+
506
+ ### Queue-pull acceptance test
507
+
508
+ Use this test after the Queue-pull source is active and the Worker route is
509
+ attached. The UTM request provides a deterministic classification signal.
510
+ If connect reports `activationRequired`, activate the source before this test:
511
+
512
+ ```bash
513
+ cnry traffic activate <project> --source <source-id>
514
+ ```
515
+
516
+ ```bash
517
+ CANONRY_QUEUE_SMOKE_PATH="/canonry-cloudflare-queue-smoke/$(date -u +%Y%m%dT%H%M%SZ)"
518
+
519
+ curl -sS -o /dev/null -w '%{http_code}\n' \
520
+ "https://www.example.com${CANONRY_QUEUE_SMOKE_PATH}?utm_source=chatgpt"
521
+
522
+ sleep 2
523
+ cnry traffic sync <project> --source <source-id>
524
+ cnry traffic events <project> --source <source-id> \
525
+ --kind all --since-minutes 120 --limit 5000 --format json
526
+ cnry traffic status <project> --format json
527
+ cnry doctor --project <project> --check 'traffic.source.*' --format json
528
+ ```
529
+
530
+ The manual sync must complete. The events output must contain the unique smoke
531
+ path as an AI referral. The status output shows `queueBacklogCount` and
532
+ `queueBacklogObservedAt`. The doctor must not report a credential failure. If
533
+ the path is absent, wait two seconds and repeat the sync up to ten times.
534
+
416
535
  ### Direct-push behavior and smoke test
417
536
 
418
537
  The Worker forwards a request candidate when at least one condition is true:
@@ -434,7 +553,7 @@ Direct push has bounded retries for network failures and HTTP 408, 425,
434
553
  429, and 5xx responses (250 ms, then 1 s). Delivery is asynchronous and
435
554
  never changes the site's response. If Canonry remains unavailable after
436
555
  the retry budget, the selected event is lost. Direct push has no durable
437
- edge buffer. This limit is the primary reason to add Queue pull next.
556
+ edge buffer. Use Queue pull when delivery needs a durable edge buffer.
438
557
 
439
558
  The receiver accepts at most 256 KiB per request. Its default budgets are
440
559
  6,000 requests per minute for each source and each caller IP. A bearer token
@@ -456,7 +575,7 @@ Raw samples retain the user-agent and normalized path. Do not put personal
456
575
  data in URL paths. Canonry replaces numeric, UUID, and long hexadecimal path
457
576
  segments with `:id`, but it retains other path segments.
458
577
 
459
- ### User acceptance test
578
+ ### Direct-push acceptance test
460
579
 
461
580
  1. Choose a stable, harmless origin path and create a unique smoke-run name.
462
581
  2. Before route attachment, capture the origin response and source rollups.
@@ -570,11 +689,26 @@ For a dedicated Canonry-generated Worker:
570
689
  1. Detach the exact site route from the Worker.
571
690
  2. Request an ordinary site page. Make sure that the origin status,
572
691
  headers, and body remain healthy.
573
- 3. Run `wrangler delete --config <artifact-directory>/wrangler.toml`.
574
- 4. Stop the tunnel if no other integration uses it.
575
- 5. Run the doctor. Expect source-health warnings until Canonry supports
692
+ 3. If this source uses Queue pull, run manual syncs until its backlog is zero.
693
+ 4. If a replacement source exists, activate it after the Queue is empty.
694
+ 5. If no replacement source exists, run
695
+ `cnry schedule remove <project> --kind traffic-sync`.
696
+ 6. If this source uses Queue pull, run
697
+ `wrangler queues consumer http remove <queue-name>`.
698
+ 7. Run `wrangler delete --config <artifact-directory>/wrangler.toml`.
699
+ 8. If this source uses direct push, stop its unused tunnel.
700
+ 9. Run the doctor. Expect source-health warnings until Canonry supports
576
701
  Cloudflare disconnect.
577
702
 
703
+ CAUTION: If no other producer uses the Queue, delete it only after the drain:
704
+
705
+ ```bash
706
+ wrangler queues delete <queue-name>
707
+ ```
708
+
709
+ If the API token is dedicated to Canonry, revoke it after you remove the HTTP
710
+ consumer.
711
+
578
712
  If Canonry code shares an existing Worker, remove only the Canonry code and
579
713
  bindings. Redeploy the prior Worker version. Do not delete the shared Worker.
580
714
 
@@ -589,30 +723,43 @@ source archival and local credential cleanup.
589
723
  | Wrangler preflight fails | Install the latest Wrangler. Run `wrangler whoami`. Then rerun connect. |
590
724
  | Worker deploys to the wrong account | Make the correct Wrangler profile active. Pass the matching `--account-id`. |
591
725
  | Route cannot attach | Make sure that the zone is active, DNS is proxied, and the exact route is unclaimed. Inspect every overlapping route. |
592
- | Receiver probe returns `403` | Remove the Access or WAF challenge from the exact ingest path. |
593
- | Worker log shows ingest `401` | Rerun connect from the credential-owning host. Make sure that system clocks differ by less than five minutes. |
594
- | No events arrive | Make sure that the route matches the exact host and `publicUrl` is reachable. Inspect AI Crawl Control, Access, WAF, and route precedence. Then send the UTM smoke request. |
726
+ | Direct-push receiver returns `403` | Remove the Access or WAF challenge from the exact ingest path. |
727
+ | Direct-push Worker log shows ingest `401` | Rerun connect from the credential-owning host. Make sure that system clocks differ by less than five minutes. |
728
+ | No direct-push events arrive | Make sure that the route matches the exact host. Make sure that `publicUrl` is reachable. Inspect AI Crawl Control, Access, WAF, and route precedence. Then send the UTM smoke request. |
729
+ | No Queue-pull events arrive | Run `wrangler queues info <queue-name>` and `wrangler queues consumer http list <queue-name>`. Make sure that the producer binding uses that Queue. Make sure that the account ID, Queue ID, and token scope match. Then run the Queue acceptance test. |
730
+ | Queue pull returns `403` | Create a token with **Account Queues Edit** for the Queue account. Reconnect with the token file. |
731
+ | Queue backlog remains above 1,000 | Run a manual sync. If the backlog grows again, shorten the 30-minute schedule interval. |
595
732
  | Some paths are absent | Inspect more-specific routes and terminating security rules. Integrate Canonry into those routes or document the excluded coverage. |
596
- | Ingest returns `429` | Wait for the rate window. The Worker retries twice, then drops the event. |
733
+ | Direct-push ingest returns `429` | Wait for the rate window. The Worker retries twice, then drops the event. |
597
734
  | Site returns Cloudflare error `1027` | Set the route to **Fail open**, or detach it. The account exhausted its daily Worker requests. |
598
735
  | Doctor reports stale or empty direct-push data | Do not run `traffic sync`. Inspect the route, receiver, and Worker logs. Then repeat the smoke request. |
599
736
 
600
- ### Queue pull seam (not shipped yet)
601
-
602
- Queue pull is the first follow-up for hosted or platform deployments with
603
- loopback-only tenant engines. Customer Workers cannot reach those engines.
604
- The future pull adapter will let Canonry read events without a public
605
- tenant-ingest endpoint.
606
-
607
- Before broad direct-push rollout, consider an edge sampling or batching control
608
- for high-volume generic bot traffic.
609
-
610
- `deliveryMode: direct-push` is explicit in the source config. The reserved
611
- `queue-pull` mode will keep the Worker filter, `CloudflareEdgeEventBatch`,
612
- canonical JSON encoding, normalizer, classifier, and rollups unchanged.
613
- Only delivery changes to a Queue binding, and Canonry gains a polling
614
- adapter. A source must use one mode at a time so the same edge event cannot
615
- arrive through both transports and double-count.
737
+ ### Queue pull delivery guarantees
738
+
739
+ Queue delivery is at least once. Canonry claims a durable source-scoped sync
740
+ lease, short-polls bounded batches, validates the transport-neutral event
741
+ schema and canonical host, and commits event receipts plus rollups in one DB
742
+ transaction. It acknowledges the Cloudflare lease only after that commit.
743
+
744
+ If acknowledgement fails after commit, Cloudflare can redeliver the batch.
745
+ The durable receipt makes the redelivery a no-op before Canonry acknowledges it
746
+ again. Unsupported V8 messages and malformed message bodies are acknowledged
747
+ as poison after their safe lease metadata is read; raw bodies and the API token
748
+ never enter errors or logs. A process crash leaves unacknowledged messages for
749
+ Cloudflare to redeliver after the visibility timeout.
750
+
751
+ The source lease rejects concurrent scheduler and operator drains. A stale
752
+ lease can be recovered after its expiry, and only its owner can release it.
753
+ Receipts remain for Cloudflare's current 14-day platform maximum plus a replay
754
+ margin. This stays safe even when the locally recorded retention is stale or
755
+ lower than the Queue's actual setting.
756
+
757
+ Each default Queue sync drains at most 10 batches of 100 messages: **1,000
758
+ messages per tick**. Doctor reports a residual of 1–1,000 messages as within
759
+ that budget. If no new messages arrive, the next scheduled sync can clear it.
760
+ Doctor warns when more than 1,000 messages remain because one default tick
761
+ cannot drain the residual. Run a manual sync to accelerate the drain. If the
762
+ backlog remains above 1,000, shorten the `traffic-sync` schedule interval.
616
763
 
617
764
  ## Syncing data
618
765
 
@@ -628,6 +775,12 @@ cnry traffic sync <project> --source <id>
628
775
  cnry traffic sync <project> --source <id> --since-minutes 4320 # 3 days
629
776
  ```
630
777
 
778
+ Cloudflare Queue pull is cursorless and ignores `--since-minutes`; each call
779
+ drains a bounded set of leased Queue messages. Repeat the command when you need
780
+ to accelerate a backlog, or let the auto-created 30-minute schedule continue
781
+ draining. Direct push still rejects `traffic sync` because it has no pull
782
+ transport.
783
+
631
784
  ### Unsticking a stuck source
632
785
 
633
786
  If a Vercel (or Cloud Run) source has been failing for so long that
@@ -654,9 +807,9 @@ above; clearing `lastError` for a transient WordPress failure also
654
807
  works. Archived sources are rejected — re-connect them with
655
808
  `cnry traffic connect ...` instead.
656
809
 
657
- Cross-sync dedupe via the `last_event_ids` ring buffer means re-running a
658
- sync over an overlapping window cannot double-count rolled-up hourly
659
- hits. Safe to schedule (see "Scheduling" below) or trigger from CI.
810
+ Time-window and cursor adapters use the `last_event_ids` overlap ring. Queue
811
+ pull uses durable event receipts sized to Queue retention. Both paths make a
812
+ repeated pull safe to schedule or trigger from CI.
660
813
 
661
814
  ## Inspecting source state
662
815
 
@@ -706,7 +859,7 @@ schema change, the stored rollups are untouched.
706
859
  | Project dashboard `/projects/:name/activity` | Live source table + 24h totals + GA4 referrals (combined view) |
707
860
  | Top-level `/traffic` route | Cross-project source admin (connect, sync, archive) |
708
861
  | `cnry report <project>` (HTML + SPA) | "AI Visibility — Server-Side" section, ranked above Indexing Health |
709
- | `cnry doctor --project <name>` | Source health checks, including Cloudflare `traffic.source.worker-version` deployment drift |
862
+ | `cnry doctor --project <name>` | Source health checks, including direct-push Worker drift and Queue-pull local credential/sync state |
710
863
  | MCP toolkit `traffic` | Read/status tools plus pull-source setup/sync tools. Cloudflare connect is local-CLI-only so Worker secrets cannot enter an MCP transcript. |
711
864
 
712
865
  ## Doctor signals
@@ -719,7 +872,9 @@ The doctor checks are adapter-agnostic. When they fail or warn:
719
872
  | `traffic.source.connected` | `traffic.source.all-errored` | Re-connect the source. The check's `details.lastError` shows the underlying reason. |
720
873
  | `traffic.source.recent-data` | `traffic.recent-data.stale` | For pull sources, run `cnry traffic sync …`. For Cloudflare direct push, inspect the route and receiver. |
721
874
  | `traffic.source.recent-data` | `traffic.recent-data.empty` | Inspect source configuration. For Cloudflare direct push, send the UTM smoke request and inspect Worker logs. |
722
- | `traffic.source.credentials` | `traffic.credentials.resolve-failed` | Reconnect from the host that owns the source credentials. |
875
+ | `traffic.source.queue-backlog` | `traffic.queue-backlog.within-drain-budget` | 1–1,000 Queue messages remain. If no new messages arrive, the next scheduled sync can drain them. Run a manual sync to accelerate. |
876
+ | `traffic.source.queue-backlog` | `traffic.queue-backlog.remaining` | More than 1,000 Queue messages remain. Run a manual sync. If the backlog persists, shorten the traffic-sync schedule. |
877
+ | `traffic.source.credentials` | `traffic.credentials.resolve-failed` | Reconnect from the host that owns the source credentials. Queue pull requires a non-empty Account Queues Edit token paired by source ID. |
723
878
  | `traffic.source.cache-blindspot` | `traffic.cache-blindspot.wordpress-plugin` | A WordPress source is connected, so the plugin cannot see cache-served page views. Exclude AI user-agents from the page cache and any CDN, or switch to a log/edge source. Warns only, not a failure. |
724
879
  | `traffic.source.worker-version` | `traffic.worker-version.waiting-for-first-event` | Send a smoke-test request through the Worker. Then run the doctor again. |
725
880
  | `traffic.source.worker-version` | `traffic.worker-version.stale` | Redeploy the generated Worker from the credential-owning host. Use `--deploy --confirm-route --confirm-fail-open`. Attach the route manually. |
@@ -728,14 +883,28 @@ Cloudflare doctor behavior is capability-driven: only
728
883
  `deliveryMode=direct-push` (and legacy Cloudflare rows with no mode) skips
729
884
  pull-watermark lag. The same mode enables `traffic.source.worker-version`.
730
885
  That check compares `configJson.workerVersion` with `lastWorkerVersion`.
731
- The reserved Queue pull mode uses pull checks and skips the version check.
886
+ Queue pull uses pull lag checks and skips the direct-push Worker-version check.
732
887
 
733
888
  ## Scheduling
734
889
 
735
- `cnry schedule` supports `--kind traffic-sync` for pull sources. Cloudflare
736
- direct push is event-driven and must not receive a traffic-sync schedule. Recurring syncs are
737
- safe because of the `last_event_ids` cross-sync dedupe ring buffer
738
- described above. Recommended cadence:
890
+ `cnry schedule` supports `--kind traffic-sync` for pull sources. Activation of
891
+ Queue, Cloud Run, WordPress, or Vercel creates or repoints this schedule at a
892
+ 30-minute cadence.
893
+ Cloudflare direct push is event-driven and must not receive a traffic-sync
894
+ schedule. Recurring syncs are safe because of the adapter's overlap ring or
895
+ durable Queue receipts.
896
+
897
+ Keep the auto-created 30-minute schedule for Queue pull. The Worker sends one
898
+ selected event in each Queue message. At its 1,000-message limit, this cadence
899
+ drains about 0.56 selected events per second. If the Queue backlog remains
900
+ above 1,000, run a manual sync. Then shorten the interval:
901
+
902
+ ```bash
903
+ cnry schedule set <project> --kind traffic-sync \
904
+ --source <source-id> --cron "*/10 * * * *"
905
+ ```
906
+
907
+ Use these cadences only for Cloud Run, WordPress, and Vercel:
739
908
 
740
909
  | Cadence | Use case |
741
910
  |---|---|
@@ -798,8 +967,8 @@ domains, or PII are surfaced.
798
967
  no client IP, so every Vercel crawler hit is unverified by
799
968
  construction (UA-only). A Vercel source reading 100% unverified is
800
969
  expected, not a misconfiguration.
801
- - **Four adapters ship: Cloud Run, WordPress, Vercel, and Cloudflare
802
- direct push. More adapters are planned.** The doctor checks and report renderer are
970
+ - **Four adapters ship: Cloud Run, WordPress, Vercel, and Cloudflare with
971
+ direct-push and queue-pull delivery. More adapters are planned.** The doctor checks and report renderer are
803
972
  adapter-agnostic — adding a new adapter is just a new entry in
804
973
  `traffic_sources.source_type` and a `TrafficSourceValidator`
805
974
  registration.
@@ -1 +1 @@
1
- import{r as a,u as h,j as e}from"./vendor-tanstack-Dock0eH1.js";import{cc as L,cd as P,bx as E,k as u,B as x,d as v,T as k}from"./index-DmsIZQAm.js";const o=100;function M(s){if(s==="all")return;const n=Number.parseInt(s,10);return new Date(Date.now()-n*24*60*60*1e3).toISOString()}function O(s){const n=s.userAgent?.toLowerCase()??"";return n.includes("canonry-mcp")?"MCP":n.includes("canonry-cli")?"CLI":n.includes("mozilla/")?"Dashboard":s.actor==="scheduler"?"Scheduler":s.actor==="agent"?"Agent":s.actor==="mcp"?"MCP":s.actor==="cli"?"CLI":s.actor}function B(s){return/delete|remove|revoke|disconnect|failed/.test(s)?"negative":/create|add|connect|completed/.test(s)?"positive":/update|replace|sync|apply/.test(s)?"caution":"neutral"}function D(s){return[s.action,s.actor,s.entityType,s.entityId,s.actorSession,s.userAgent,s.diff==null?"":JSON.stringify(s.diff)].filter(Boolean).join(" ").toLowerCase()}function H({projectName:s}){const[n,l]=a.useState(0),[c,y]=a.useState("90d"),[m,N]=a.useState(""),[p,w]=a.useState(""),S=a.useMemo(()=>M(c),[c]),g={limit:o,offset:n*o,since:S,actor:m||void 0},C=h({...L({client:u,path:{name:s??""},query:g}),enabled:!!s}),A=h({...P({client:u,query:g}),enabled:!s}),f=h({...E({client:u}),enabled:!s}),i=s?C:A,r=i.data??[],I=a.useMemo(()=>new Map((f.data??[]).map(t=>[t.id,t.displayName||t.name])),[f.data]),d=p.trim().toLowerCase(),j=a.useMemo(()=>d?r.filter(t=>D(t).includes(d)):r,[r,d]),b=t=>{l(0),t()};return i.isLoading?e.jsx("p",{className:"supporting-copy mt-6",role:"status",children:"Loading change history…"}):e.jsxs("section",{className:"mt-6","aria-labelledby":"change-history-heading",children:[e.jsxs("div",{className:"section-head section-head-inline",children:[e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"Audit trail"}),e.jsx("h2",{id:"change-history-heading",children:"Change history"}),e.jsx("p",{className:"supporting-copy mt-1 max-w-2xl",children:"Configuration and operator actions. Execution outcomes remain in Runs."})]}),e.jsx(x,{type:"button",variant:"outline",size:"sm",onClick:()=>{i.refetch()},disabled:i.isFetching,children:i.isFetching?"Refreshing…":"Refresh"})]}),e.jsxs("div",{className:"mt-4 grid gap-2 sm:grid-cols-3",children:[e.jsx("input",{type:"search","aria-label":"Search change history",value:p,onChange:t=>w(t.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"}),e.jsxs("select",{"aria-label":"Filter change history by actor",value:m,onChange:t=>b(()=>N(t.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:[e.jsx("option",{value:"",children:"All actors"}),e.jsx("option",{value:"api",children:"API and dashboard"}),e.jsx("option",{value:"cli",children:"CLI"}),e.jsx("option",{value:"scheduler",children:"Scheduler"}),e.jsx("option",{value:"agent",children:"Agent"}),e.jsx("option",{value:"mcp",children:"MCP"})]}),e.jsxs("select",{"aria-label":"Filter change history by date range",value:c,onChange:t=>b(()=>y(t.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:[e.jsx("option",{value:"30d",children:"Last 30 days"}),e.jsx("option",{value:"90d",children:"Last 90 days"}),e.jsx("option",{value:"365d",children:"Last year"}),e.jsx("option",{value:"all",children:"All retained history"})]})]}),i.isError?e.jsxs(v,{className:"surface-card empty-card mt-4",children:[e.jsx("h3",{children:"Change history unavailable"}),e.jsx("p",{children:i.error instanceof Error?i.error.message:"Could not load the audit trail."})]}):j.length===0?e.jsxs(v,{className:"surface-card empty-card mt-4",children:[e.jsx("h3",{children:"No changes match these filters"}),e.jsx("p",{children:"Widen the date range or clear the actor and search filters."})]}):e.jsx("div",{className:"evidence-table-wrap mt-4",children:e.jsxs("table",{className:"evidence-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),s?null:e.jsx("th",{children:"Scope"}),e.jsx("th",{children:"Action"}),e.jsx("th",{children:"Entity"}),e.jsx("th",{children:"Origin"}),e.jsx("th",{children:"Details"})]})}),e.jsx("tbody",{children:j.map(t=>e.jsxs("tr",{children:[e.jsx("td",{className:"whitespace-nowrap tabular-nums text-muted",children:new Date(t.createdAt).toLocaleString()}),s?null:e.jsx("td",{className:"text-secondary",children:t.projectId?I.get(t.projectId)??`Deleted project · ${t.projectId}`:"Instance settings"}),e.jsx("td",{children:e.jsx(k,{tone:B(t.action),children:t.action})}),e.jsxs("td",{children:[e.jsx("span",{className:"text-strong",children:t.entityType}),t.entityId?e.jsx("span",{className:"mt-0.5 block max-w-48 truncate font-mono text-[11px] text-faint",title:t.entityId,children:t.entityId}):null]}),e.jsxs("td",{children:[e.jsx("span",{className:"text-secondary",children:O(t)}),t.actorSession?e.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]}),e.jsx("td",{children:t.diff==null?e.jsx("span",{className:"text-faint",children:"No field diff"}):e.jsxs("details",{children:[e.jsx("summary",{className:"min-h-11 cursor-pointer py-3 text-sm text-secondary hover:text-strong",children:"View diff"}),e.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))})]})}),!i.isError&&(n>0||r.length===o)?e.jsxs("div",{className:"mt-4 flex items-center justify-between gap-3",children:[e.jsx(x,{type:"button",variant:"outline",size:"sm",disabled:n===0,onClick:()=>l(t=>Math.max(0,t-1)),children:"Previous"}),e.jsxs("span",{className:"text-xs tabular-nums text-muted",children:["Page ",n+1]}),e.jsx(x,{type:"button",variant:"outline",size:"sm",disabled:r.length<o,onClick:()=>l(t=>t+1),children:"Next"})]}):null]})}export{H as A};
1
+ import{r as a,u as h,j as e}from"./vendor-tanstack-Dock0eH1.js";import{cc as L,cd as P,bx as E,k as u,B as x,d as v,T as k}from"./index-CQGAqmDx.js";const o=100;function M(s){if(s==="all")return;const n=Number.parseInt(s,10);return new Date(Date.now()-n*24*60*60*1e3).toISOString()}function O(s){const n=s.userAgent?.toLowerCase()??"";return n.includes("canonry-mcp")?"MCP":n.includes("canonry-cli")?"CLI":n.includes("mozilla/")?"Dashboard":s.actor==="scheduler"?"Scheduler":s.actor==="agent"?"Agent":s.actor==="mcp"?"MCP":s.actor==="cli"?"CLI":s.actor}function B(s){return/delete|remove|revoke|disconnect|failed/.test(s)?"negative":/create|add|connect|completed/.test(s)?"positive":/update|replace|sync|apply/.test(s)?"caution":"neutral"}function D(s){return[s.action,s.actor,s.entityType,s.entityId,s.actorSession,s.userAgent,s.diff==null?"":JSON.stringify(s.diff)].filter(Boolean).join(" ").toLowerCase()}function H({projectName:s}){const[n,l]=a.useState(0),[c,y]=a.useState("90d"),[m,N]=a.useState(""),[p,w]=a.useState(""),S=a.useMemo(()=>M(c),[c]),g={limit:o,offset:n*o,since:S,actor:m||void 0},C=h({...L({client:u,path:{name:s??""},query:g}),enabled:!!s}),A=h({...P({client:u,query:g}),enabled:!s}),f=h({...E({client:u}),enabled:!s}),i=s?C:A,r=i.data??[],I=a.useMemo(()=>new Map((f.data??[]).map(t=>[t.id,t.displayName||t.name])),[f.data]),d=p.trim().toLowerCase(),j=a.useMemo(()=>d?r.filter(t=>D(t).includes(d)):r,[r,d]),b=t=>{l(0),t()};return i.isLoading?e.jsx("p",{className:"supporting-copy mt-6",role:"status",children:"Loading change history…"}):e.jsxs("section",{className:"mt-6","aria-labelledby":"change-history-heading",children:[e.jsxs("div",{className:"section-head section-head-inline",children:[e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"Audit trail"}),e.jsx("h2",{id:"change-history-heading",children:"Change history"}),e.jsx("p",{className:"supporting-copy mt-1 max-w-2xl",children:"Configuration and operator actions. Execution outcomes remain in Runs."})]}),e.jsx(x,{type:"button",variant:"outline",size:"sm",onClick:()=>{i.refetch()},disabled:i.isFetching,children:i.isFetching?"Refreshing…":"Refresh"})]}),e.jsxs("div",{className:"mt-4 grid gap-2 sm:grid-cols-3",children:[e.jsx("input",{type:"search","aria-label":"Search change history",value:p,onChange:t=>w(t.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"}),e.jsxs("select",{"aria-label":"Filter change history by actor",value:m,onChange:t=>b(()=>N(t.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:[e.jsx("option",{value:"",children:"All actors"}),e.jsx("option",{value:"api",children:"API and dashboard"}),e.jsx("option",{value:"cli",children:"CLI"}),e.jsx("option",{value:"scheduler",children:"Scheduler"}),e.jsx("option",{value:"agent",children:"Agent"}),e.jsx("option",{value:"mcp",children:"MCP"})]}),e.jsxs("select",{"aria-label":"Filter change history by date range",value:c,onChange:t=>b(()=>y(t.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:[e.jsx("option",{value:"30d",children:"Last 30 days"}),e.jsx("option",{value:"90d",children:"Last 90 days"}),e.jsx("option",{value:"365d",children:"Last year"}),e.jsx("option",{value:"all",children:"All retained history"})]})]}),i.isError?e.jsxs(v,{className:"surface-card empty-card mt-4",children:[e.jsx("h3",{children:"Change history unavailable"}),e.jsx("p",{children:i.error instanceof Error?i.error.message:"Could not load the audit trail."})]}):j.length===0?e.jsxs(v,{className:"surface-card empty-card mt-4",children:[e.jsx("h3",{children:"No changes match these filters"}),e.jsx("p",{children:"Widen the date range or clear the actor and search filters."})]}):e.jsx("div",{className:"evidence-table-wrap mt-4",children:e.jsxs("table",{className:"evidence-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),s?null:e.jsx("th",{children:"Scope"}),e.jsx("th",{children:"Action"}),e.jsx("th",{children:"Entity"}),e.jsx("th",{children:"Origin"}),e.jsx("th",{children:"Details"})]})}),e.jsx("tbody",{children:j.map(t=>e.jsxs("tr",{children:[e.jsx("td",{className:"whitespace-nowrap tabular-nums text-muted",children:new Date(t.createdAt).toLocaleString()}),s?null:e.jsx("td",{className:"text-secondary",children:t.projectId?I.get(t.projectId)??`Deleted project · ${t.projectId}`:"Instance settings"}),e.jsx("td",{children:e.jsx(k,{tone:B(t.action),children:t.action})}),e.jsxs("td",{children:[e.jsx("span",{className:"text-strong",children:t.entityType}),t.entityId?e.jsx("span",{className:"mt-0.5 block max-w-48 truncate font-mono text-[11px] text-faint",title:t.entityId,children:t.entityId}):null]}),e.jsxs("td",{children:[e.jsx("span",{className:"text-secondary",children:O(t)}),t.actorSession?e.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]}),e.jsx("td",{children:t.diff==null?e.jsx("span",{className:"text-faint",children:"No field diff"}):e.jsxs("details",{children:[e.jsx("summary",{className:"min-h-11 cursor-pointer py-3 text-sm text-secondary hover:text-strong",children:"View diff"}),e.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))})]})}),!i.isError&&(n>0||r.length===o)?e.jsxs("div",{className:"mt-4 flex items-center justify-between gap-3",children:[e.jsx(x,{type:"button",variant:"outline",size:"sm",disabled:n===0,onClick:()=>l(t=>Math.max(0,t-1)),children:"Previous"}),e.jsxs("span",{className:"text-xs tabular-nums text-muted",children:["Page ",n+1]}),e.jsx(x,{type:"button",variant:"outline",size:"sm",disabled:r.length<o,onClick:()=>l(t=>t+1),children:"Next"})]}):null]})}export{H as A};