@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.
- package/README.md +2 -2
- package/assets/agent-workspace/skills/canonry/SKILL.md +8 -3
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +454 -21
- package/assets/assets/{AuditHistoryPanel-gmJMVlXX.js → AuditHistoryPanel-McQfjoU9.js} +1 -1
- package/assets/assets/{BacklinksPage-Bv2sTCyN.js → BacklinksPage-Da8y1Wv8.js} +1 -1
- package/assets/assets/{HistoryPage-13q7GkiI.js → HistoryPage-9MJfhRyT.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-BgBqXT1p.js → MeasurementPropertyPage-BiNAHEJW.js} +1 -1
- package/assets/assets/ProjectPage-mWSMjXrR.js +15 -0
- package/assets/assets/{RunRow-bPzapIGJ.js → RunRow-DCG2LBlS.js} +1 -1
- package/assets/assets/{RunsPage-Dqfz18lH.js → RunsPage-ByWA_FSk.js} +1 -1
- package/assets/assets/{SettingsPage-_wMecn-V.js → SettingsPage-DdSeKAKF.js} +1 -1
- package/assets/assets/{TrafficPage-RYBdsOLB.js → TrafficPage-5u8rPcBt.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DpeMMyKU.js → TrafficSourceDetailPage-Dm-fK87u.js} +1 -1
- package/assets/assets/{extract-error-message-DUarAlyD.js → extract-error-message-CtxkAd6E.js} +1 -1
- package/assets/assets/{index-DP-fT2uI.js → index-DUfg50Pl.js} +28 -28
- package/assets/assets/index-cXb3mfeQ.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-DnypJl5w.js → react-sigma_core.esm.min-ComIyDKi.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-ZWESOFA5.js → chunk-4VZP64GZ.js} +273 -15
- package/dist/{chunk-AMPT2EP7.js → chunk-DEVAEC2S.js} +121 -1
- package/dist/{chunk-CPASKGUE.js → chunk-LK6YFHG4.js} +35 -1
- package/dist/{chunk-ABNN2ZVR.js → chunk-YP4K2KXH.js} +1933 -451
- package/dist/cli.js +667 -142
- package/dist/index.d.ts +32 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-WHC7B32V.js → intelligence-service-LQ6ZRWMF.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +12 -10
- package/assets/assets/ProjectPage-EsGwKhot.js +0 -15
- 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
|
-
-
|
|
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
|
|
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,
|
|
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-
|
|
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
|
|
4
|
-
your
|
|
5
|
-
click-through arrivals
|
|
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
|
-
|
|
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
|
|
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` |
|
|
39
|
-
| `vercel` | Vercel project logs via the Vercel API | Sites deployed on Vercel
|
|
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.
|
|
286
|
-
| MCP toolkit `traffic` |
|
|
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` |
|
|
297
|
-
| `traffic.source.recent-data` | `traffic.recent-data.empty` |
|
|
298
|
-
| `traffic.source.credentials` | `traffic.credentials.resolve-failed` |
|
|
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
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
imitator.
|
|
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
|
-
- **
|
|
369
|
-
planned.** The doctor checks and
|
|
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{
|
|
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};
|