@canonry/canonry 4.156.0 → 4.158.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 +2 -2
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +8 -3
  3. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +454 -21
  4. package/assets/assets/{AuditHistoryPanel-gmJMVlXX.js → AuditHistoryPanel-McQfjoU9.js} +1 -1
  5. package/assets/assets/{BacklinksPage-Bv2sTCyN.js → BacklinksPage-Da8y1Wv8.js} +1 -1
  6. package/assets/assets/{HistoryPage-13q7GkiI.js → HistoryPage-9MJfhRyT.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-BgBqXT1p.js → MeasurementPropertyPage-BiNAHEJW.js} +1 -1
  8. package/assets/assets/ProjectPage-mWSMjXrR.js +15 -0
  9. package/assets/assets/{RunRow-bPzapIGJ.js → RunRow-DCG2LBlS.js} +1 -1
  10. package/assets/assets/{RunsPage-Dqfz18lH.js → RunsPage-ByWA_FSk.js} +1 -1
  11. package/assets/assets/{SettingsPage-_wMecn-V.js → SettingsPage-DdSeKAKF.js} +1 -1
  12. package/assets/assets/{TrafficPage-RYBdsOLB.js → TrafficPage-5u8rPcBt.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-DpeMMyKU.js → TrafficSourceDetailPage-Dm-fK87u.js} +1 -1
  14. package/assets/assets/{extract-error-message-DUarAlyD.js → extract-error-message-CtxkAd6E.js} +1 -1
  15. package/assets/assets/{index-DP-fT2uI.js → index-DUfg50Pl.js} +28 -28
  16. package/assets/assets/index-cXb3mfeQ.css +1 -0
  17. package/assets/assets/{react-sigma_core.esm.min-DnypJl5w.js → react-sigma_core.esm.min-ComIyDKi.js} +1 -1
  18. package/assets/index.html +2 -2
  19. package/dist/{chunk-ZWESOFA5.js → chunk-4VZP64GZ.js} +273 -15
  20. package/dist/{chunk-AMPT2EP7.js → chunk-DEVAEC2S.js} +121 -1
  21. package/dist/{chunk-CPASKGUE.js → chunk-LK6YFHG4.js} +35 -1
  22. package/dist/{chunk-ABNN2ZVR.js → chunk-YP4K2KXH.js} +1933 -451
  23. package/dist/cli.js +667 -142
  24. package/dist/index.d.ts +32 -0
  25. package/dist/index.js +4 -4
  26. package/dist/{intelligence-service-WHC7B32V.js → intelligence-service-LQ6ZRWMF.js} +2 -2
  27. package/dist/mcp.js +2 -2
  28. package/package.json +12 -10
  29. package/assets/assets/ProjectPage-EsGwKhot.js +0 -15
  30. package/assets/assets/index-BxegEwic.css +0 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ Your entire AEO + web analytics stack, agent-first. **Open source. Self-hosted.
6
6
 
7
7
  - **Local-only:** Canonry runs on your machine and stores its data in SQLite.
8
8
  - Track citations across Gemini, ChatGPT, Claude, Perplexity, and local LLMs
9
- - Watch AI engines crawl and refer traffic via [server-log ingestion](skills/canonry/references/server-side-traffic.md) — Cloud Run, Vercel, and the WordPress Traffic Logger plugin today
9
+ - Capture server-side traffic from Cloudflare, Cloud Run, Vercel, and WordPress [setup guide](skills/canonry/references/server-side-traffic.md)
10
10
  - Diagnose against real traffic with built-in [GSC](docs/google-search-console-setup.md), [GA4](docs/google-analytics-setup.md), and [Bing Webmaster](docs/bing-webmaster-setup.md)
11
11
  - Track local AEO via [Google Business Profile](skills/canonry/references/google-business-profile.md) — search-term impressions, performance metrics, and hotel lodging + booking-CTA gaps
12
12
  - Manage [ChatGPT ads](docs/mcp.md#tool-surface) with OpenAI Ads Manager — connect an ad account, inspect conversion setup and performance, prepare paused campaigns, and launch only with an explicit human approval
@@ -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 (Cloud Run + Vercel + WordPress logs)](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, 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)) |
@@ -66,7 +66,7 @@ tool or `403` by switching credentials.
66
66
  - Running technical SEO audits (14‑factor scoring)
67
67
  - Implementing structured data (JSON‑LD)
68
68
  - Diagnosing indexing gaps via Google Search Console / Bing Webmaster Tools
69
- - Wiring server-side traffic (Cloud Run, WordPress, Vercel) and GA4 referrals into a single AEO signal
69
+ - Wiring server-side traffic (Cloudflare, Cloud Run, WordPress, Vercel) and GA4 referrals into a single AEO signal
70
70
  - Optimizing `llms.txt`, sitemaps, robots.txt for AI crawlers
71
71
  - Submitting URLs to Google Indexing API and Bing IndexNow
72
72
  - Analyzing competitor citation patterns
@@ -129,7 +129,12 @@ GA4 is a first-class signal alongside citation tracking. Connect once with `cnry
129
129
 
130
130
  ## Server-Side Traffic
131
131
 
132
- When the project ships behind a server you control, wire crawler + AI-referral evidence directly from the edge: `cnry traffic connect cloud-run | wordpress | vercel <project> ...` writes credentials to `~/.canonry/config.yaml`, `cnry traffic sync` pulls and classifies logs into hourly buckets, and `cnry traffic events / sources / status` expose the rollups. See `references/server-side-traffic.md` for adapter-specific setup.
132
+ When the project ships behind a server you control, connect Cloud Run, WordPress,
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.
133
138
 
134
139
  **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`.
135
140
 
@@ -168,7 +173,7 @@ Aero also wakes unprompted after every `run.completed` so insights and regressio
168
173
  | `references/aeo-analysis.md` | Interpreting sweep output, diagnosing regressions, planning content fixes |
169
174
  | `references/indexing.md` | Submitting URLs, checking GSC/Bing coverage, fixing indexing gaps |
170
175
  | `references/wordpress-integration.md` | Connecting to WordPress, editing pages, pushing staging → live |
171
- | `references/server-side-traffic.md` | Wiring server-log evidence (Cloud Run, WordPress, Vercel adapters) for AI Visibility — Server-Side. Connect, sync, manage sources, troubleshoot. |
176
+ | `references/server-side-traffic.md` | Wiring server-side evidence from Cloudflare, Cloud Run, WordPress, and Vercel. Connect, inspect, sync pull sources, and troubleshoot. |
172
177
  | `references/google-business-profile.md` | Connecting Google Business Profile for local AEO: access-form approval, GCP API enablement, the v4-reviews access gate, hotel lodging/place-action signals, data shapes, troubleshooting. |
173
178
 
174
179
  ---
@@ -1,8 +1,8 @@
1
1
  # Server-side traffic (AI Visibility — Server-Side)
2
2
 
3
- Server-side traffic ingestion captures **what AI engines actually do in
4
- your server logs** — bots crawling pages, AI products sending
5
- click-through arrivals in addition to the citation data that measures
3
+ Server-side traffic ingestion captures **what AI engines actually do at
4
+ your site** — bots crawling pages, AI products fetching pages for users,
5
+ and AI products sending click-through arrivals. Citation data measures
6
6
  **what models say** about you. The two surfaces are independent.
7
7
 
8
8
  ## When to use it
@@ -10,6 +10,7 @@ click-through arrivals — in addition to the citation data that measures
10
10
  Reach for server-side traffic when an analyst or operator asks:
11
11
 
12
12
  - *"Is GPTBot / ClaudeBot / PerplexityBot actually fetching my pages?"*
13
+ - *"Did an AI product fetch this page for a user?"*
13
14
  - *"Which paths are AI engines paying attention to?"*
14
15
  - *"Are users clicking through from chatgpt.com / claude.ai / etc.?"*
15
16
  - *"My citation rate is fine but there's no traffic — why?"*
@@ -21,24 +22,28 @@ blockers, or analytics gaps.
21
22
 
22
23
  ## Architecture
23
24
 
24
- Two tables, populated from server-log adapters:
25
+ Four tables store the shared output from every adapter:
25
26
 
26
27
  | Table | What's in it |
27
28
  |---|---|
28
29
  | `crawler_events_hourly` | One row per `(project, source, hour, bot, verification, path, status)` — bot crawls rolled up by hour |
30
+ | `ai_user_fetch_events_hourly` | One row per `(project, source, hour, bot, verification, path, status)` — user-initiated AI fetches rolled up by hour |
29
31
  | `ai_referral_events_hourly` | One row per `(project, source, hour, product, source_domain, evidence_type, landing_path, status)` — click-through arrivals rolled up by hour |
30
- | `raw_event_samples` | Bounded forensic samples (≤100 per sync) for spot-checking |
32
+ | `raw_event_samples` | Bounded forensic samples for spot checks. Cloudflare uses a per-source, per-hour cap |
31
33
 
32
34
  Each `traffic_sources` row is one server-log integration for a project.
33
35
  Adapters today:
34
36
 
35
- | Adapter | Source | Best for |
36
- |---|---|---|
37
- | `cloud-run` | GCP Cloud Run request logs via Logging API | Any service running on Cloud Run |
38
- | `wordpress` | The Canonry Traffic Logger WP plugin's REST endpoint | WordPress sites where you control wp-admin |
39
- | `vercel` | Vercel project logs via the Vercel API | Sites deployed on Vercel (Next.js, SvelteKit, etc.) |
37
+ | Adapter | Transport | Source | Best for |
38
+ |---|---|---|---|
39
+ | [`cloud-run`](#connecting-a-cloud-run-source) | Pull | GCP Cloud Run request logs via Logging API | Any service running on Cloud Run |
40
+ | [`wordpress`](#connecting-a-wordpress-source) | Pull | Canonry Traffic Logger REST endpoint | WordPress sites where you control wp-admin |
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 |
40
43
 
41
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`.
42
47
 
43
48
  ## Connecting a Cloud Run source
44
49
 
@@ -190,6 +195,425 @@ Run `cnry traffic backfill <project> --source <id> --days N` (capped at
190
195
  history. It's an explicit operator action; the connect flow never pulls
191
196
  it implicitly.
192
197
 
198
+ ## Connecting a Cloudflare source (direct push)
199
+
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.
205
+
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.
208
+
209
+ ```mermaid
210
+ flowchart LR
211
+ request["Site request"] --> worker["Canonry Cloudflare Worker"]
212
+ worker --> origin["Existing origin or Pages site"]
213
+ origin --> response["Unchanged site response"]
214
+ worker -. "signed selected event via ctx.waitUntil" .-> ingest["Canonry public HTTPS ingest"]
215
+ ingest --> classify["Shared traffic classifier"]
216
+ classify --> rollups["Hourly crawler, user-fetch, and referral rollups"]
217
+ ```
218
+
219
+ ### Setup flow
220
+
221
+ 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.
223
+ 3. Authenticate Wrangler with the Cloudflare account that owns the zone.
224
+ 4. Inspect existing Worker routes for the exact canonical hostname.
225
+ 5. Run the local Cloudflare connect command with `--deploy`.
226
+ 6. Attach the printed route in Cloudflare and set it to **Fail open**.
227
+ 7. Send the smoke requests and run the Canonry doctor.
228
+
229
+ ### Current support boundary
230
+
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.
234
+
235
+ 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.
238
+
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.
243
+
244
+ ### Expose the Canonry receiver
245
+
246
+ Complete the local dashboard setup and set its password before you expose
247
+ Canonry. A tunnel makes the loopback server reachable from the public Internet.
248
+
249
+ Use a named Cloudflare Tunnel or another stable reverse proxy:
250
+
251
+ ```text
252
+ https://canonry-ops.example.net
253
+ -> stable tunnel or reverse proxy
254
+ -> http://127.0.0.1:4100
255
+ ```
256
+
257
+ Then complete these steps:
258
+
259
+ 1. Back up `~/.canonry/config.yaml`.
260
+ 2. Set `publicUrl` to the external Canonry URL.
261
+ 3. If Canonry uses a base path, include that path in `publicUrl`.
262
+ 4. Restart `canonry serve`.
263
+ 5. Send an unsigned request to the ingest path from the public Internet.
264
+ 6. Make sure that the response status is `401`.
265
+
266
+ ```yaml
267
+ publicUrl: https://canonry-ops.example.net
268
+ ```
269
+
270
+ ```bash
271
+ CANONRY_PUBLIC_URL=https://canonry-ops.example.net
272
+
273
+ curl -sS -o /dev/null -w '%{http_code}\n' \
274
+ -X POST -H 'content-type: application/json' --data '{}' \
275
+ "${CANONRY_PUBLIC_URL%/}/api/v1/projects/<project>/traffic/cloudflare/ingest"
276
+ ```
277
+
278
+ The `401` response proves that the request reached Canonry's transport
279
+ authentication. A `403` can mean that Cloudflare Access or a WAF rule blocked
280
+ the request first. Exempt only the exact ingest path from interactive
281
+ challenges. Keep the rest of Canonry behind its normal access controls.
282
+
283
+ Do not use a random TryCloudflare URL for production. The Worker stores the
284
+ ingest URL at deployment time. If a temporary URL changes, rerun connect and
285
+ redeploy the Worker.
286
+
287
+ The public URL is the Canonry receiver, not the tracked site. Use a different
288
+ hostname from the generated Worker route. Do not add credentials, a query, or
289
+ a fragment to this URL.
290
+
291
+ ### Prerequisites and route preflight
292
+
293
+ 1. Install a current Wrangler release.
294
+ 2. Run `wrangler login`.
295
+ If Wrangler lists multiple accounts, pass the exact `--account-id`.
296
+ 3. Run `wrangler whoami` to get the account ID.
297
+ 4. Get the zone ID from the Cloudflare zone Overview page.
298
+ 5. Make sure that the zone belongs to the selected account.
299
+ 6. Make sure that Cloudflare proxies the canonical hostname in DNS.
300
+ 7. Inspect every Worker route that overlaps the canonical hostname. Cloudflare
301
+ uses the most-specific matching route. Record any excluded path coverage.
302
+ 8. Make sure that `<canonical-host>/*` is unclaimed. If a more-specific route
303
+ must remain, integrate Canonry there or accept that those paths are absent.
304
+ 9. If another Worker owns the catch-all route, stop. Integrate Canonry into that
305
+ Worker instead of attaching a second Worker.
306
+ 10. Review the account request volume. A route invokes the Worker for every
307
+ matching request. The filter does not reduce Worker invocations.
308
+
309
+ ```bash
310
+ npm install -g wrangler@latest
311
+ wrangler login
312
+ wrangler whoami
313
+ ```
314
+
315
+ Canonry does not pass a `--profile` option to Wrangler. Wrangler uses the auth
316
+ profile that is active for the current directory, or its default profile. If
317
+ you use named profiles, activate one before you run Canonry:
318
+
319
+ ```bash
320
+ wrangler auth list
321
+ wrangler auth activate <profile> .
322
+ wrangler whoami --json
323
+ ```
324
+
325
+ The project must already exist. Use a bare public hostname such as
326
+ `www.example.com` for its canonical domain. `--zone-id` is mandatory with
327
+ `--deploy`. You can omit `--account-id` when the active profile has one account.
328
+
329
+ The generated route covers only the project's exact canonical hostname. It
330
+ does not also cover `www` or the apex. Test and redirect the other hostname
331
+ separately.
332
+
333
+ A Worker Route can run in front of a Cloudflare Pages custom domain. If the
334
+ origin is another Worker, use a Custom Domain or integrate Canonry into that
335
+ Worker. A same-zone Worker Route cannot be the target of `fetch()`.
336
+
337
+ Workers Free includes 100,000 requests per account each day. The quota
338
+ resets at midnight UTC. A fail-closed route returns Cloudflare error 1027
339
+ after the account exhausts this quota. A fail-open route bypasses the
340
+ Worker and keeps the origin available. See [Cloudflare Workers limits](https://developers.cloudflare.com/workers/platform/limits/).
341
+
342
+ Free-plan classification uses user-agent, referer, and UTM evidence. Granular
343
+ `request.cf.botManagement` scores require the Enterprise Bot Management add-on.
344
+
345
+ ### Generate or deploy
346
+
347
+ ```bash
348
+ # Generate secret-free worker.js and wrangler.toml in
349
+ # ./canonry-cloudflare-<project-slug>:
350
+ cnry traffic connect cloudflare <project> \
351
+ --zone-id <cloudflare-zone-id> \
352
+ --account-id <cloudflare-account-id>
353
+
354
+ # Deploy only the Worker after the route preflight.
355
+ # Both safety acknowledgements are mandatory:
356
+ cnry traffic connect cloudflare <project> \
357
+ --zone-id <cloudflare-zone-id> \
358
+ --account-id <cloudflare-account-id> \
359
+ --deploy --confirm-route --confirm-fail-open
360
+
361
+ # Optional artifact directory / source label:
362
+ cnry traffic connect cloudflare <project> \
363
+ --display-name "Cloudflare · production" \
364
+ --output-dir ./infra/canonry-cloudflare
365
+ ```
366
+
367
+ `--confirm-route` acknowledges that you inspected the exact route.
368
+ `--confirm-fail-open` acknowledges that you will configure the required route
369
+ toggle. Canonry does not persist these flags. Neither flag changes Cloudflare.
370
+
371
+ The command prints the source ID, artifact directory, and manual route steps.
372
+ Keep the source ID for the acceptance test and later diagnosis.
373
+
374
+ With `--deploy`, Canonry checks Wrangler before it connects the source. The
375
+ preflight uses `--dry-run --secrets-file ... --strict` to parse and bundle
376
+ representative generated artifacts. An incompatible Wrangler stops this step
377
+ before Canonry changes state. The generated `[secrets].required` table is an
378
+ [official Wrangler safety check](https://developers.cloudflare.com/changelog/post/2026-03-24-secrets-config-property/);
379
+ it does not contain secret values.
380
+
381
+ The generated TOML never declares `[[routes]]`. Thus, Wrangler deploys the
382
+ Worker without placing it on production traffic. After deployment, open
383
+ Cloudflare Dashboard and complete these steps:
384
+
385
+ 1. Open **Workers & Pages → Overview** and select the generated Worker.
386
+ 2. Open **Settings → Domains & Routes → Add → Route**.
387
+ 3. Select the zone and enter the exact `<canonical-host>/*` route.
388
+ 4. Set **Request limit failure mode** to **Fail open**.
389
+ 5. Add the route.
390
+
391
+ Wrangler cannot configure the failure-mode toggle.
392
+
393
+ The generated source and TOML contain no credential values. Non-secret
394
+ configuration values are Worker vars. The `--account-id` option makes
395
+ connect write `account_id` at the top level.
396
+
397
+ The bearer and HMAC values stay in the local Canonry credential store.
398
+ They become `CANONRY_BEARER_TOKEN` and `CANONRY_HMAC_SECRET` Worker
399
+ secrets. With `--deploy`, the CLI uses a mode-0600 temporary secrets file.
400
+ Then it invokes Wrangler with `--secrets-file ... --strict` and removes
401
+ the file.
402
+
403
+ Rerun the same connect command to update an old generated Worker. Canonry
404
+ atomically replaces recognizable Canonry-generated files. It refuses
405
+ operator-owned files and symlinks. Therefore, doctor remediation does not
406
+ require manual file deletion.
407
+
408
+ Wrangler preflight failures occur before Canonry creates a source. Artifact or
409
+ deployment failures can occur after source creation. Rerun the same command
410
+ with the same output directory. Canonry reuses the source and its credentials.
411
+
412
+ Cloudflare connect is absent from MCP because deployment reads local
413
+ secrets. An agent can guide the CLI command. It must not request, print,
414
+ or send either secret through chat.
415
+
416
+ ### Direct-push behavior and smoke test
417
+
418
+ The Worker forwards a request candidate when at least one condition is true:
419
+
420
+ - Its user-agent matches a known AI crawler or user-fetch agent.
421
+ - Its user-agent contains the broad `bot`, `crawler`, or `spider` keyword.
422
+ - Its referer or `utm_source` matches a known AI product.
423
+ - Bot Management marks it as verified or gives it a score below 30.
424
+
425
+ The shared Canonry classifier makes the strict classification after delivery.
426
+ Thus, the broad edge filter can send a candidate that produces no rollup.
427
+
428
+ Direct push observes only requests that reach this Worker. A terminating
429
+ Access, WAF, bot-protection, or AI Crawl Control action can stop a request first.
430
+ For a crawler smoke test, use a narrow exception for only the smoke path when
431
+ the site's security policy permits it. Do not disable site-wide protection.
432
+
433
+ Direct push has bounded retries for network failures and HTTP 408, 425,
434
+ 429, and 5xx responses (250 ms, then 1 s). Delivery is asynchronous and
435
+ never changes the site's response. If Canonry remains unavailable after
436
+ 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.
438
+
439
+ The receiver accepts at most 256 KiB per request. Its default budgets are
440
+ 6,000 requests per minute for each source and each caller IP. A bearer token
441
+ and HMAC protect the route. The HMAC timestamp window is five minutes. A
442
+ transactional receipt protects each event ID for ten minutes. Receipt claims,
443
+ hourly rollups, samples, and source progress commit in one transaction.
444
+
445
+ Direct push sends one selected event per batch. It does not batch or sample at
446
+ the edge. Canonry stores at most 100 raw samples per source per UTC hour, while
447
+ it continues to add all classified events to hourly rollups.
448
+
449
+ The Worker transports the raw client IP, full query string, and full referer
450
+ URL for classification. Canonry does not persist these raw values. Raw samples
451
+ store `ipHash: null`, a normalized path without its query, and only the referer
452
+ host. A later privacy change can send only the query parameters that the
453
+ classifier requires.
454
+
455
+ Raw samples retain the user-agent and normalized path. Do not put personal
456
+ data in URL paths. Canonry replaces numeric, UUID, and long hexadecimal path
457
+ segments with `:id`, but it retains other path segments.
458
+
459
+ ### User acceptance test
460
+
461
+ 1. Choose a stable, harmless origin path and create a unique smoke-run name.
462
+ 2. Before route attachment, capture the origin response and source rollups.
463
+ 3. Attach the exact route with **Fail open**.
464
+ 4. Capture the origin response again. Compare its body and critical headers.
465
+ 5. Send four classified requests and one negative control.
466
+ 6. Poll the source rollups, inspect the unique paths, and run the doctor.
467
+
468
+ Replace the example hostname, `<project>`, and `<source-id>` before you run
469
+ these commands:
470
+
471
+ ```bash
472
+ CANONRY_SMOKE_SITE=https://www.example.com
473
+ CANONRY_SMOKE_RUN=run-$(date -u +%Y%m%dT%H%M%SZ)
474
+ CANONRY_SMOKE_PREFIX=/canonry-cloudflare-smoke/$CANONRY_SMOKE_RUN
475
+ CANONRY_SMOKE_DIR=$(mktemp -d)
476
+
477
+ # Run these commands before route attachment.
478
+ curl -sS -D "$CANONRY_SMOKE_DIR/origin-before.headers" \
479
+ -o "$CANONRY_SMOKE_DIR/origin-before.body" \
480
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/origin"
481
+ cnry traffic events <project> --source <source-id> \
482
+ --kind all --since-minutes 120 --limit 5000 --format json \
483
+ > "$CANONRY_SMOKE_DIR/events-before.json"
484
+
485
+ # Stop here. Attach the exact route with Fail open, then continue.
486
+ curl -sS -D "$CANONRY_SMOKE_DIR/origin-after.headers" \
487
+ -o "$CANONRY_SMOKE_DIR/origin-after.body" \
488
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/origin"
489
+ grep -Ei '^(HTTP/|content-type:|cache-control:|location:)' \
490
+ "$CANONRY_SMOKE_DIR/origin-before.headers" \
491
+ "$CANONRY_SMOKE_DIR/origin-after.headers"
492
+ cmp "$CANONRY_SMOKE_DIR/origin-before.body" \
493
+ "$CANONRY_SMOKE_DIR/origin-after.body"
494
+
495
+ # AI crawler
496
+ curl -sS -o /dev/null -w '%{http_code}\n' -A 'GPTBot/1.2' \
497
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/crawler"
498
+
499
+ # AI user fetch
500
+ curl -sS -o /dev/null -w '%{http_code}\n' -A 'ChatGPT-User/1.0' \
501
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/user-fetch"
502
+
503
+ # AI referral by referer
504
+ curl -sS -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 CanonrySmoke' \
505
+ -e 'https://chatgpt.com/' \
506
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/referral"
507
+
508
+ # AI referral by UTM
509
+ curl -sS -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 CanonrySmoke' \
510
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/utm?utm_source=chatgpt"
511
+
512
+ # Negative control: this request must not add a classified traffic rollup
513
+ curl -sS -o /dev/null -w '%{http_code}\n' -A 'Mozilla/5.0 CanonrySmoke' \
514
+ "$CANONRY_SMOKE_SITE$CANONRY_SMOKE_PREFIX/control"
515
+
516
+ # Poll until the deterministic UTM event arrives, for at most 20 seconds.
517
+ for CANONRY_SMOKE_ATTEMPT in 1 2 3 4 5 6 7 8 9 10; do
518
+ cnry traffic events <project> --source <source-id> \
519
+ --kind all --since-minutes 120 --limit 5000 --format json \
520
+ > "$CANONRY_SMOKE_DIR/events-after.json"
521
+ grep -Fq "$CANONRY_SMOKE_PREFIX/utm" \
522
+ "$CANONRY_SMOKE_DIR/events-after.json" && break
523
+ sleep 2
524
+ done
525
+
526
+ cat "$CANONRY_SMOKE_DIR/events-before.json"
527
+ cat "$CANONRY_SMOKE_DIR/events-after.json"
528
+
529
+ if grep -Fq "$CANONRY_SMOKE_PREFIX/control" \
530
+ "$CANONRY_SMOKE_DIR/events-after.json"; then
531
+ echo 'Unexpected classified control rollup'
532
+ else
533
+ echo 'Negative control passed'
534
+ fi
535
+
536
+ cnry doctor --project <project> --check 'traffic.source.*' --format json
537
+ ```
538
+
539
+ The smoke paths can return `404`. The body and critical headers must match the
540
+ no-Worker baseline. When all four selected requests reach the Worker, the
541
+ unique paths must show one crawler, one AI user fetch, and two AI referrals.
542
+ The control path must not appear in a classified rollup. On Bot Management
543
+ plans, a low bot score can still cause its transport.
544
+
545
+ The crawler request uses a spoofed user agent. It tests forwarding and
546
+ classification only. Canonry reports it as `claimed_unverified` unless the
547
+ client IP belongs to the bot's published IP ranges. If only the crawler path is
548
+ absent, inspect AI Crawl Control, Access, WAF, and other terminating rules. The
549
+ UTM request is the deterministic direct-push transport check.
550
+
551
+ The events command returns hourly rollups, not raw request records. The doctor
552
+ must report current Worker version and recent traffic.
553
+
554
+ ### Staging release test
555
+
556
+ 1. Cause a receiver failure. Make sure that the origin stays healthy.
557
+ 2. Return HTTP `429` from staging and send a bounded bot burst.
558
+ 3. Make sure that delivery stops after one request and two retries.
559
+ 4. Compare p50 and p95 TTFB before and after route attachment.
560
+ 5. Inspect Worker logs. Make sure that logs contain no secret values.
561
+ 6. Rehearse rollback and make sure that the origin stays healthy.
562
+
563
+ A direct-push source does not use `cnry traffic sync` or a traffic-sync
564
+ schedule.
565
+
566
+ ### Rollback and teardown
567
+
568
+ For a dedicated Canonry-generated Worker:
569
+
570
+ 1. Detach the exact site route from the Worker.
571
+ 2. Request an ordinary site page. Make sure that the origin status,
572
+ 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
576
+ Cloudflare disconnect.
577
+
578
+ If Canonry code shares an existing Worker, remove only the Canonry code and
579
+ bindings. Redeploy the prior Worker version. Do not delete the shared Worker.
580
+
581
+ The current CLI and API have no Cloudflare disconnect operation. The source
582
+ row and local credential store remain after teardown. A follow-up must add
583
+ source archival and local credential cleanup.
584
+
585
+ ### Cloudflare troubleshooting
586
+
587
+ | Symptom | Action |
588
+ |---|---|
589
+ | Wrangler preflight fails | Install the latest Wrangler. Run `wrangler whoami`. Then rerun connect. |
590
+ | Worker deploys to the wrong account | Make the correct Wrangler profile active. Pass the matching `--account-id`. |
591
+ | 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. |
595
+ | 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. |
597
+ | Site returns Cloudflare error `1027` | Set the route to **Fail open**, or detach it. The account exhausted its daily Worker requests. |
598
+ | 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
+
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.
616
+
193
617
  ## Syncing data
194
618
 
195
619
  ```bash
@@ -282,8 +706,8 @@ schema change, the stored rollups are untouched.
282
706
  | Project dashboard `/projects/:name/activity` | Live source table + 24h totals + GA4 referrals (combined view) |
283
707
  | Top-level `/traffic` route | Cross-project source admin (connect, sync, archive) |
284
708
  | `cnry report <project>` (HTML + SPA) | "AI Visibility — Server-Side" section, ranked above Indexing Health |
285
- | `cnry doctor --project <name>` | `traffic.source.connected`, `recent-data`, `credentials`, `scopes`, `cache-blindspot` checks |
286
- | MCP toolkit `traffic` | Tools: `canonry_traffic_status`, `_sources_list`, `_source_get`, `_events`, `_connect_cloud_run`, `_sync` |
709
+ | `cnry doctor --project <name>` | Source health checks, including Cloudflare `traffic.source.worker-version` deployment drift |
710
+ | 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. |
287
711
 
288
712
  ## Doctor signals
289
713
 
@@ -293,14 +717,23 @@ The doctor checks are adapter-agnostic. When they fail or warn:
293
717
  |---|---|---|
294
718
  | `traffic.source.connected` | `traffic.source.none` | No source — `cnry traffic connect cloud-run …` |
295
719
  | `traffic.source.connected` | `traffic.source.all-errored` | Re-connect the source. The check's `details.lastError` shows the underlying reason. |
296
- | `traffic.source.recent-data` | `traffic.recent-data.stale` | Last sync was >7d ago. Run `cnry traffic sync …` or schedule a recurring sync. |
297
- | `traffic.source.recent-data` | `traffic.recent-data.empty` | Source connected but no data in 30d. Verify config and credentials with `cnry traffic sources <project>`. |
298
- | `traffic.source.credentials` | `traffic.credentials.resolve-failed` | Service-account key in `~/.canonry/config.yaml` is invalid or expired. Re-connect. |
720
+ | `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
+ | `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. |
299
723
  | `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
+ | `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
+ | `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. |
726
+
727
+ Cloudflare doctor behavior is capability-driven: only
728
+ `deliveryMode=direct-push` (and legacy Cloudflare rows with no mode) skips
729
+ pull-watermark lag. The same mode enables `traffic.source.worker-version`.
730
+ That check compares `configJson.workerVersion` with `lastWorkerVersion`.
731
+ The reserved Queue pull mode uses pull checks and skips the version check.
300
732
 
301
733
  ## Scheduling
302
734
 
303
- `cnry schedule` supports `--kind traffic-sync`. Recurring syncs are
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
304
737
  safe because of the `last_event_ids` cross-sync dedupe ring buffer
305
738
  described above. Recommended cadence:
306
739
 
@@ -358,15 +791,15 @@ domains, or PII are surfaced.
358
791
  treat the report's crawled-paths table as "engine attention" — the
359
792
  signal is the bot fetched it, not whether it was cited.
360
793
  - **Verified vs unverified.** The headline numbers count only
361
- rDNS-verified hits. Unverified bots claim a known UA but couldn't be
362
- cross-confirmed via reverse-DNS they may be the real bot or an
363
- imitator. Don't promote unverified counts in client-facing copy.
794
+ published-IP-range-verified hits. Unverified bots claim a known UA but
795
+ do not match the operator's published IP ranges. The bot can be real or an
796
+ imitator. Do not promote unverified counts in client-facing copy.
364
797
  **Vercel sources are a special case:** the Vercel pull API returns
365
798
  no client IP, so every Vercel crawler hit is unverified by
366
799
  construction (UA-only). A Vercel source reading 100% unverified is
367
800
  expected, not a misconfiguration.
368
- - **Three adapters shipped (Cloud Run + WordPress + Vercel); more
369
- planned.** The doctor checks and the report renderer are
801
+ - **Four adapters ship: Cloud Run, WordPress, Vercel, and Cloudflare
802
+ direct push. More adapters are planned.** The doctor checks and report renderer are
370
803
  adapter-agnostic — adding a new adapter is just a new entry in
371
804
  `traffic_sources.source_type` and a `TrafficSourceValidator`
372
805
  registration.
@@ -1 +1 @@
1
- import{r as a,u as h,j as e}from"./vendor-tanstack-pFxw0eGP.js";import{cs as L,ct as P,bN as E,k as u,B as x,d as v,T as k}from"./index-DP-fT2uI.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-pFxw0eGP.js";import{cv as L,cw as P,bQ as E,k as u,B as x,d as v,T as k}from"./index-DUfg50Pl.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 F({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{F as A};