@canonry/canonry 4.104.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 (52) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +117 -0
  3. package/assets/agent-workspace/AGENTS.md +88 -0
  4. package/assets/agent-workspace/USER.md +23 -0
  5. package/assets/agent-workspace/skills/aero/SKILL.md +72 -0
  6. package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +161 -0
  7. package/assets/agent-workspace/skills/aero/references/memory-patterns.md +66 -0
  8. package/assets/agent-workspace/skills/aero/references/orchestration.md +67 -0
  9. package/assets/agent-workspace/skills/aero/references/regression-playbook.md +58 -0
  10. package/assets/agent-workspace/skills/aero/references/reporting.md +98 -0
  11. package/assets/agent-workspace/skills/aero/references/wordpress-elementor-mcp.md +223 -0
  12. package/assets/agent-workspace/skills/aero/soul.md +30 -0
  13. package/assets/agent-workspace/skills/canonry/SKILL.md +152 -0
  14. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +200 -0
  15. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +714 -0
  16. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +300 -0
  17. package/assets/agent-workspace/skills/canonry/references/indexing.md +163 -0
  18. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +372 -0
  19. package/assets/agent-workspace/skills/canonry/references/wordpress-integration.md +61 -0
  20. package/assets/apple-touch-icon.png +0 -0
  21. package/assets/assets/BacklinksPage-CixBvw--.js +1 -0
  22. package/assets/assets/ChartPrimitives-CjX4mQCy.js +1 -0
  23. package/assets/assets/ProjectPage-C09hcd6v.js +7 -0
  24. package/assets/assets/RunRow-Bel7mWgu.js +1 -0
  25. package/assets/assets/RunsPage-wqKWA2Ei.js +1 -0
  26. package/assets/assets/SettingsPage-esOqUkW5.js +1 -0
  27. package/assets/assets/TrafficPage-V0iCV3l4.js +1 -0
  28. package/assets/assets/TrafficSourceDetailPage-DUeuOh2v.js +1 -0
  29. package/assets/assets/arrow-left-CVZPxpW6.js +1 -0
  30. package/assets/assets/extract-error-message-BWAlA7ea.js +1 -0
  31. package/assets/assets/index-LsyRFLMG.css +1 -0
  32. package/assets/assets/index-avJiga1P.js +210 -0
  33. package/assets/assets/trash-2-CA_L6uGC.js +1 -0
  34. package/assets/assets/vendor-markdown-6dwjPOKK.js +14 -0
  35. package/assets/assets/vendor-radix-CQ77EO2y.js +45 -0
  36. package/assets/assets/vendor-recharts-C9EZkgbP.js +36 -0
  37. package/assets/assets/vendor-tanstack-Be8JdV5M.js +1 -0
  38. package/assets/favicon-32.png +0 -0
  39. package/assets/favicon.svg +24 -0
  40. package/assets/index.html +25 -0
  41. package/bin/canonry-mcp.mjs +2 -0
  42. package/bin/canonry.mjs +2 -0
  43. package/dist/chunk-23DCY7DL.js +7030 -0
  44. package/dist/chunk-GIKNEXFE.js +12414 -0
  45. package/dist/chunk-GNMZTR6Y.js +35673 -0
  46. package/dist/chunk-Q7FFDQU4.js +5743 -0
  47. package/dist/cli.js +13344 -0
  48. package/dist/index.d.ts +260 -0
  49. package/dist/index.js +12 -0
  50. package/dist/intelligence-service-GM3RXE2F.js +7 -0
  51. package/dist/mcp.js +442 -0
  52. package/package.json +97 -0
@@ -0,0 +1,372 @@
1
+ # Server-side traffic (AI Visibility — Server-Side)
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
6
+ **what models say** about you. The two surfaces are independent.
7
+
8
+ ## When to use it
9
+
10
+ Reach for server-side traffic when an analyst or operator asks:
11
+
12
+ - *"Is GPTBot / ClaudeBot / PerplexityBot actually fetching my pages?"*
13
+ - *"Which paths are AI engines paying attention to?"*
14
+ - *"Are users clicking through from chatgpt.com / claude.ai / etc.?"*
15
+ - *"My citation rate is fine but there's no traffic — why?"*
16
+
17
+ GA4 referrals (chatgpt.com → your site) catch click-throughs after they
18
+ land. Server logs catch the upstream bot activity AND referrals at the
19
+ edge — including arrivals GA4 missed because of cookie consent, ad
20
+ blockers, or analytics gaps.
21
+
22
+ ## Architecture
23
+
24
+ Two tables, populated from server-log adapters:
25
+
26
+ | Table | What's in it |
27
+ |---|---|
28
+ | `crawler_events_hourly` | One row per `(project, source, hour, bot, verification, path, status)` — bot crawls rolled up by hour |
29
+ | `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 |
31
+
32
+ Each `traffic_sources` row is one server-log integration for a project.
33
+ Adapters today:
34
+
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.) |
40
+
41
+ Future adapters slot in by implementing the same contract.
42
+
43
+ ## Connecting a Cloud Run source
44
+
45
+ ```bash
46
+ # 1. Create a service account in the Cloud project that hosts the Cloud Run
47
+ # service. Grant it `roles/logging.viewer`. Download the JSON key.
48
+
49
+ # 2. Connect from cnry CLI:
50
+ cnry traffic connect cloud-run <project> \
51
+ --gcp-project <gcp-project-id> \
52
+ --service-account-key <path/to/key.json>
53
+
54
+ # 3. (Optional) narrow to a specific service or location:
55
+ cnry traffic connect cloud-run <project> \
56
+ --gcp-project <id> \
57
+ --service-account-key <path> \
58
+ --service my-service-name \
59
+ --location us-east1
60
+ ```
61
+
62
+ Credentials are stored in `~/.canonry/config.yaml` (not the DB). The
63
+ canonical key lives only on the host that runs `cnry serve`. The
64
+ sync flow does NOT echo the private key back in any response.
65
+
66
+ ## Connecting a WordPress source
67
+
68
+ The WordPress adapter pulls events from the **Canonry Traffic Logger**
69
+ WordPress plugin, which captures every non-admin GET page-load **that
70
+ reaches PHP** and exposes a paginated REST endpoint protected by an
71
+ Application Password.
72
+
73
+ > **Cache blind spot.** The plugin is a PHP hook, so it only sees
74
+ > requests that execute WordPress. A full-page cache (LiteSpeed, WP
75
+ > Rocket, W3 Total Cache, WP Super Cache) or CDN serves cached pages
76
+ > before PHP runs, so cache-served page views, including live AI
77
+ > user-fetches (Claude-User, ChatGPT-User), are NOT logged. Bot crawls
78
+ > of uncached endpoints (sitemap, feeds, assets, cache misses) still
79
+ > come through, which can make capture look healthy while real page
80
+ > views go uncounted. Exclude AI user-agents from the cache (and any
81
+ > CDN), or capture from access/edge logs instead. The
82
+ > `traffic.source.cache-blindspot` doctor check warns whenever a
83
+ > WordPress source is connected.
84
+
85
+ **Which user-agents to exclude from the cache** (one per line in
86
+ LiteSpeed's "Do Not Cache User Agents", WP Rocket's
87
+ `rocket_cache_reject_ua`, or W3TC / WP Super Cache "Rejected User
88
+ Agents"):
89
+
90
+ ```
91
+ Claude-User
92
+ ClaudeBot
93
+ ChatGPT-User
94
+ OAI-SearchBot
95
+ GPTBot
96
+ PerplexityBot
97
+ Perplexity-User
98
+ ```
99
+
100
+ These are the answer-engine fetchers in both live-user-fetch (`*-User`)
101
+ and crawler forms. Do NOT add `Googlebot` or `Bingbot`: caching helps
102
+ search crawlers (page speed is a ranking signal, and cached pages let
103
+ them crawl more per visit, which matters most on crawl-budget-starved
104
+ sites), and their crawl stats are already authoritative in GSC and Bing
105
+ Webmaster Tools. Rule of thumb: bypass cache only for agents you cannot
106
+ measure elsewhere and that gain nothing from being cached. Answer-engine
107
+ fetchers fit both; search crawlers fit neither.
108
+
109
+ ```bash
110
+ # 1. Install the plugin. Download the latest release zip from the
111
+ # canonry-traffic-logger plugin's GitHub release (the repo CI workflow
112
+ # publishes a zip on every plugin-file change), then in wp-admin:
113
+ # Plugins → Add New → Upload Plugin → upload + activate.
114
+
115
+ # 2. In wp-admin, create an Application Password for the operator user:
116
+ # Users → Profile → Application Passwords. Copy the generated password.
117
+
118
+ # 3. (Optional) Adjust settings at Settings → Canonry Traffic Logger:
119
+ # - Retention window: clamps to 7-365 days, default 90.
120
+ # - "Behind a proxy or CDN": enable this when the site sits behind
121
+ # Cloudflare or another reverse proxy, so the real visitor IP
122
+ # (needed to verify AI-bot hits) is read from forwarded headers
123
+ # rather than the proxy's edge address.
124
+ # The page also shows the current event count and oldest event.
125
+
126
+ # 4. Connect from cnry CLI:
127
+ cnry traffic connect wordpress <project> \
128
+ --url https://example.com \
129
+ --username admin \
130
+ --app-password "xxxx xxxx xxxx xxxx xxxx xxxx"
131
+ ```
132
+
133
+ What the events table looks like (mirrors the TS
134
+ `WordpressTrafficEventPayload`):
135
+
136
+ | Column | Meaning |
137
+ |---|---|
138
+ | `observed_at` | ISO 8601 UTC timestamp with millisecond precision |
139
+ | `method`, `host`, `path`, `query_string` | Split `REQUEST_URI` parts |
140
+ | `status` | HTTP response status code |
141
+ | `user_agent`, `referer` | Headers as captured at request time |
142
+ | `remote_ip` | Client IP address (IPv4 or IPv6), or empty when none was captured |
143
+
144
+ The plugin auto-prunes events older than the retention window (default
145
+ 90 days) once per day via WP-Cron. Operators who want a different
146
+ window change it in `Settings → Canonry Traffic Logger`.
147
+
148
+ ## Connecting a Vercel source
149
+
150
+ The Vercel adapter pulls per-request logs from the Vercel API for a
151
+ specific project + environment. Logs are filtered by canonical domain
152
+ before classification so a multi-tenant Vercel project only surfaces
153
+ hits for the tracked site.
154
+
155
+ ```bash
156
+ # 1. In the Vercel dashboard, create a token with read access to the
157
+ # target team (Settings → Tokens → Create). Note the team ID
158
+ # (Settings → General → Team ID) and the Vercel project ID
159
+ # (Project → Settings → General → Project ID).
160
+
161
+ # 2. Connect from cnry CLI:
162
+ cnry traffic connect vercel <project> \
163
+ --project-id prj_xxxxxxxx \
164
+ --team-id team_xxxxxxxx \
165
+ --token <vercel-token> # or: --token-file <path>
166
+
167
+ # 3. (Optional) scope to a specific environment (default: production):
168
+ cnry traffic connect vercel <project> \
169
+ --project-id prj_xxx --team-id team_xxx --token ... \
170
+ --environment preview
171
+ ```
172
+
173
+ Credentials live in `~/.canonry/config.yaml` under `vercelTraffic:`,
174
+ mirroring the cloud-run / wordpress blocks. The adapter classifies bot
175
+ crawls + AI-referral arrivals into the same `crawler_events_hourly` /
176
+ `ai_referral_events_hourly` tables — downstream commands
177
+ (`cnry traffic events / sources / status`) are source-agnostic.
178
+
179
+ ### Vercel first-sync window (gotcha)
180
+
181
+ A new Vercel source captures **only going-forward traffic** by default.
182
+ `cnry traffic connect vercel` seeds `lastSyncedAt = NOW` so the first
183
+ scheduled sync uses a tight window inside Vercel's ~14-day
184
+ `request-logs` retention. Without this, the first sync would fall back
185
+ to a 30-day window, exceed retention, and throw — leaving the source
186
+ permanently stuck.
187
+
188
+ Run `cnry traffic backfill <project> --source <id> --days N` (capped at
189
+ ~14 to stay inside retention) if you need any of the pre-connect
190
+ history. It's an explicit operator action; the connect flow never pulls
191
+ it implicitly.
192
+
193
+ ## Syncing data
194
+
195
+ ```bash
196
+ # Manual sync — pulls [lastSyncedAt, now]. For a freshly connected
197
+ # source the window is short (since connect-time NOW). For a
198
+ # regular-cadence schedule the window stays ~30 min wide.
199
+ cnry traffic sync <project> --source <id>
200
+
201
+ # Override the lookback window (minutes) — note: clamped forward to
202
+ # lastSyncedAt, so this can only NARROW the window, never widen it
203
+ # past data already pulled.
204
+ cnry traffic sync <project> --source <id> --since-minutes 4320 # 3 days
205
+ ```
206
+
207
+ ### Unsticking a stuck source
208
+
209
+ If a Vercel (or Cloud Run) source has been failing for so long that
210
+ `lastSyncedAt` aged past the upstream retention boundary, every sync
211
+ will throw a retention error and `lastSyncedAt` will never advance —
212
+ the source is permanently stuck. Recovery:
213
+
214
+ ```bash
215
+ # Advances lastSyncedAt to NOW, clears the error state. Skipped
216
+ # history is unrecoverable from the sync path; run backfill separately
217
+ # if any of it needs to be captured.
218
+ cnry traffic reset <project> --source <id> --advance-to-now
219
+ ```
220
+
221
+ `--advance-to-now` is required — there is no implicit reset.
222
+
223
+ `reset` accepts any **non-archived** source type. The `lastSyncedAt`
224
+ advance is meaningful for time-windowed sources (Vercel, Cloud Run)
225
+ where it determines the next sync window. Cursor-based sources
226
+ (WordPress) keep their `last_cursor` intact, so the `lastSyncedAt`
227
+ advance is informational — the next WordPress drain still resumes
228
+ from the cursor. The primary use case is the retention-trap recovery
229
+ above; clearing `lastError` for a transient WordPress failure also
230
+ works. Archived sources are rejected — re-connect them with
231
+ `cnry traffic connect ...` instead.
232
+
233
+ Cross-sync dedupe via the `last_event_ids` ring buffer means re-running a
234
+ sync over an overlapping window cannot double-count rolled-up hourly
235
+ hits. Safe to schedule (see "Scheduling" below) or trigger from CI.
236
+
237
+ ## Inspecting source state
238
+
239
+ ```bash
240
+ # All sources with last-24h totals + latest sync run (single-call):
241
+ cnry traffic status <project> --format json
242
+
243
+ # Just the source list:
244
+ cnry traffic sources <project> --format json
245
+
246
+ # Windowed events (defaults to last 24h):
247
+ cnry traffic events <project> --kind crawler --limit 200 --format json
248
+ cnry traffic events <project> --kind ai-referral --since 2026-04-01 --until 2026-04-30
249
+ ```
250
+
251
+ The `traffic status` composite returns the same per-source detail
252
+ (24h crawler hits, AI-referral arrivals, raw-event-sample count, latest
253
+ sync-run summary) whether you reach it via the CLI, the API, or the
254
+ MCP `canonry_traffic_status` tool.
255
+
256
+ **Crawler hits are segmented by path class (#719).** On real sites the raw
257
+ `crawlerHits` total is dominated by infrastructure polling — a bot re-fetching
258
+ `sitemap_index.xml`, `robots.txt`, and static assets — which overstates how
259
+ much of your *content* is being crawled. `traffic status` and `traffic events`
260
+ therefore return, alongside the unchanged `crawlerHits` total:
261
+
262
+ - `crawlerContentHits` — crawls of actual content/document pages (the signal you
263
+ usually want: "are bots reading my pages?").
264
+ - `crawlerInfraHits` — sitemap + robots + asset fetches.
265
+ - `crawlerSegments` — the full `{ content, sitemap, robots, asset, other }`
266
+ breakdown; the five buckets sum to `crawlerHits`, and
267
+ `content + infra + other == crawlerHits`. `other` captures non-page downloads
268
+ and feeds (PDF, CSV, RSS) plus WordPress polling endpoints that are not page
269
+ reads (`/feed`, `/<path>/feed`, `/wp-json/...`, `xmlrpc.php`, `wp-cron.php`), so
270
+ they stay out of `crawlerContentHits`.
271
+
272
+ Each crawler row from `traffic events` also carries a `pathClass`
273
+ (`content | sitemap | robots | asset | other`). The dashboard leads with the
274
+ content figure and shows infrastructure polling as a secondary number. The
275
+ classification is read-time only (the pure `classifyTrafficPath` helper) — no
276
+ schema change, the stored rollups are untouched.
277
+
278
+ ## Where the data shows up
279
+
280
+ | Surface | What's rendered |
281
+ |---|---|
282
+ | Project dashboard `/projects/:name/activity` | Live source table + 24h totals + GA4 referrals (combined view) |
283
+ | Top-level `/traffic` route | Cross-project source admin (connect, sync, archive) |
284
+ | `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` |
287
+
288
+ ## Doctor signals
289
+
290
+ The doctor checks are adapter-agnostic. When they fail or warn:
291
+
292
+ | Check | Code | What to do |
293
+ |---|---|---|
294
+ | `traffic.source.connected` | `traffic.source.none` | No source — `cnry traffic connect cloud-run …` |
295
+ | `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. |
299
+ | `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. |
300
+
301
+ ## Scheduling
302
+
303
+ `cnry schedule` supports `--kind traffic-sync`. Recurring syncs are
304
+ safe because of the `last_event_ids` cross-sync dedupe ring buffer
305
+ described above. Recommended cadence:
306
+
307
+ | Cadence | Use case |
308
+ |---|---|
309
+ | `0 */6 * * *` (every 6h) | Production agencies tracking active client sites |
310
+ | `0 0 * * *` (daily) | Lower-traffic sites or local dev |
311
+ | Manual only | First few weeks while validating data |
312
+
313
+ ## Telemetry
314
+
315
+ Every successful or failed sync emits a `traffic.synced` event to the
316
+ canonry telemetry pipeline:
317
+
318
+ ```jsonc
319
+ {
320
+ "event": "traffic.synced",
321
+ "errorCode": "PROVIDER_AUTH", // present only when status='failed'
322
+ "properties": {
323
+ "status": "completed" | "failed",
324
+ "sourceType": "cloud-run", // adapter type
325
+ "sourceId": "<uuid>", // opaque
326
+ "pulledEvents": 234,
327
+ "crawlerHits": 200,
328
+ "aiReferralHits": 12,
329
+ "durationMs": 4150
330
+ }
331
+ }
332
+ ```
333
+
334
+ Counts are aggregate. The sourceId is an opaque UUID. No raw paths,
335
+ domains, or PII are surfaced.
336
+
337
+ ## Limits & caveats
338
+
339
+ - **The WordPress plugin is blind to cache-served traffic.** The
340
+ `wordpress` adapter logs only requests that reach PHP. A full-page
341
+ cache or CDN serves cached pages from the edge, so cache-served page
342
+ views, including live AI user-fetches (Claude-User, ChatGPT-User),
343
+ never reach the plugin and go uncounted, even though bot crawls of
344
+ uncached endpoints (sitemap, assets) still appear. On a cached
345
+ WordPress site, treat the plugin's page-view counts as a floor, not a
346
+ total. Either exclude AI user-agents from the cache + CDN, or capture
347
+ cache-independent via a `cloud-run` / `vercel` / edge-log source. The
348
+ `traffic.source.cache-blindspot` doctor check surfaces this. Adapter
349
+ coverage differs: `vercel` ingests edge request-logs so cache hits are
350
+ captured (it records the `cache` HIT/MISS label), and `cloud-run` logs
351
+ every request that reaches the service, missing only what a CDN placed
352
+ in front of Cloud Run serves from its own edge cache. Only the
353
+ hook-based `wordpress` adapter has the always-present blind spot.
354
+ - **Path-level citation cross-reference is not implemented yet.** The
355
+ citation store is domain-grain (`query_snapshots.cited_domains`). A
356
+ future iteration that lands URL-grain citation evidence will extend
357
+ the `topCrawledPaths` entry with a `citationState` flag. Until then,
358
+ treat the report's crawled-paths table as "engine attention" — the
359
+ signal is the bot fetched it, not whether it was cited.
360
+ - **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.
364
+ **Vercel sources are a special case:** the Vercel pull API returns
365
+ no client IP, so every Vercel crawler hit is unverified by
366
+ construction (UA-only). A Vercel source reading 100% unverified is
367
+ expected, not a misconfiguration.
368
+ - **Three adapters shipped (Cloud Run + WordPress + Vercel); more
369
+ planned.** The doctor checks and the report renderer are
370
+ adapter-agnostic — adding a new adapter is just a new entry in
371
+ `traffic_sources.source_type` and a `TrafficSourceValidator`
372
+ registration.
@@ -0,0 +1,61 @@
1
+ # WordPress Integration
2
+
3
+ Canonry integrates with WordPress through the core REST API plus Application Passwords.
4
+
5
+ ## What Canonry Automates
6
+
7
+ - Read and write page content, titles, slugs, and status
8
+ - Audit pages for `noindex`, missing SEO title, missing meta description, missing schema, and thin content
9
+ - Compare live vs staging for a page with `cnry wordpress diff`
10
+ - Update SEO meta when the site exposes writable REST meta fields
11
+
12
+ ## What Stays Manual
13
+
14
+ Canonry generates payloads and instructions for these workflows, but it does not apply them remotely:
15
+
16
+ - `cnry wordpress set-schema`
17
+ - `cnry wordpress set-llms-txt`
18
+ - `cnry wordpress staging push`
19
+
20
+ Expect those commands to return:
21
+
22
+ - `manualRequired: true`
23
+ - generated content
24
+ - target/admin URL when available
25
+ - next-step instructions
26
+
27
+ ## Environment Model
28
+
29
+ Each project-scoped WordPress connection stores:
30
+
31
+ - live `url`
32
+ - optional `stagingUrl`
33
+ - `defaultEnv`
34
+ - `username`
35
+ - `appPassword`
36
+
37
+ Env-sensitive commands accept `--live` or `--staging`. If neither is provided, canonry uses `defaultEnv`.
38
+
39
+ ## Typical Workflow
40
+
41
+ ```bash
42
+ cnry wordpress connect mysite --url https://example.com --user admin --staging-url https://staging.example.com --default-env staging
43
+ cnry wordpress pages mysite --staging
44
+ cnry wordpress page mysite pricing --staging
45
+ cnry wordpress set-meta mysite pricing --title "SEO title" --description "Meta description" --staging
46
+ cnry wordpress audit mysite --staging
47
+ cnry wordpress diff mysite pricing
48
+ cnry wordpress staging push mysite
49
+ ```
50
+
51
+ ## Important Constraints
52
+
53
+ - WordPress auth is stored locally in `~/.canonry/config.yaml`
54
+ - Canonry does not use wp-admin automation or undocumented plugin APIs
55
+ - If SEO meta is not writable through REST, canonry returns an actionable error instead of guessing
56
+ - Duplicate slug matches are returned as explicit ambiguity errors with candidate page IDs/titles
57
+ - Authentication is verified on connect by calling `/wp/v2/users/me` — if that fails, canonry returns an actionable error message
58
+
59
+ ## Related: Elementor MCP
60
+
61
+ For programmatic management of Elementor page layouts, widgets, and styling via MCP tools, see the aero skill reference: [`skills/aero/references/wordpress-elementor-mcp.md`](../../aero/references/wordpress-elementor-mcp.md).
Binary file
@@ -0,0 +1 @@
1
+ import{r as n,j as e}from"./vendor-tanstack-Be8JdV5M.js";import{c as E,bR as O,ag as V,bS as U,bT as Y,bU as J,g as l,aJ as L,T as f,B as z,i as A,ap as K,bV as X,bW as Z,aj as ee,bX as se}from"./index-avJiga1P.js";import{C as ae,D as te,T as ne,a as ce}from"./trash-2-CA_L6uGC.js";import"./vendor-radix-CQ77EO2y.js";import"./vendor-recharts-C9EZkgbP.js";import"./vendor-markdown-6dwjPOKK.js";const ie=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]],re=E("circle-alert",ie);const le=[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]],de=E("external-link",le);function h({children:a,label:m="More info",placement:t="top",className:d}){const o=n.useId(),[y,r]=n.useState(!1);return e.jsxs("span",{className:`relative inline-flex ${d??""}`,children:[e.jsx("button",{type:"button","aria-label":m,"aria-describedby":y?o:void 0,className:"inline-flex h-4 w-4 items-center justify-center rounded-full text-zinc-500 hover:text-zinc-200 focus:text-zinc-200 focus:outline-none focus-visible:ring-1 focus-visible:ring-zinc-500",onMouseEnter:()=>r(!0),onMouseLeave:()=>r(!1),onFocus:()=>r(!0),onBlur:()=>r(!1),children:e.jsx(ce,{className:"h-3.5 w-3.5","aria-hidden":!0})}),y&&e.jsx("span",{id:o,role:"tooltip",className:`absolute z-50 w-64 rounded border border-zinc-700 bg-zinc-900 px-3 py-2 text-xs font-normal leading-relaxed text-zinc-200 shadow-lg ${t==="top"?"bottom-full mb-2":"top-full mt-2"} left-1/2 -translate-x-1/2 whitespace-normal`,children:a})]})}const oe="https://commoncrawl.org/web-graphs";function w(a){return a==null?"—":a>=1e12?`${(a/1e12).toFixed(1)} TB`:a>=1e9?`${(a/1e9).toFixed(1)} GB`:a>=1e6?`${(a/1e6).toFixed(1)} MB`:a>=1e3?`${(a/1e3).toFixed(1)} KB`:`${a} B`}function b(a){if(!a)return"—";const m=Date.now()-new Date(a).getTime(),t=Math.floor(m/6e4);if(t<1)return"just now";if(t<60)return`${t}m ago`;const d=Math.floor(t/60);return d<24?`${d}h ago`:`${Math.floor(d/24)}d ago`}function v(a){switch(a){case"ready":return"positive";case"failed":return"negative";case"downloading":case"querying":case"queued":return"caution"}}function fe(){const[a,m]=n.useState(null),[t,d]=n.useState(null),[o,y]=n.useState([]),[r,M]=n.useState([]),[u,P]=n.useState(null),[D,S]=n.useState(!0),[C,B]=n.useState(!1),[p,R]=n.useState(!1),[N,g]=n.useState(""),[F,k]=n.useState(!1),[q,i]=n.useState(null),[I,x]=n.useState(null),j=n.useCallback(async()=>{S(!0),i(null);try{const[s,c,W,_,H]=await Promise.all([O(),V().catch(()=>null),U().catch(()=>[]),Y().catch(()=>[]),J().catch(()=>null)]);m(s),d(c),y(W),M(_),P(H)}catch(s){i(s instanceof Error?s.message:"Failed to load backlinks status")}finally{S(!1)}},[]);n.useEffect(()=>{j()},[j]);async function T(){B(!0),i(null),x(null);try{const s=await X();x(s.alreadyPresent?`DuckDB already installed (${s.version}).`:`Installed DuckDB ${s.version}.`),await j()}catch(s){i(s instanceof Error?s.message:"Failed to install DuckDB")}finally{B(!1)}}async function $(){const s=N.trim()||void 0;R(!0),i(null),x(null);try{const c=await Z(s);x(s?`Queued sync for ${c.release}. Download + query runs in the background.`:`Queued sync for auto-discovered release ${c.release}. Download + query runs in the background.`),g(""),k(!1),await j()}catch(c){c instanceof ee&&c.code==="MISSING_DEPENDENCY"?i("DuckDB is not installed. Install it first."):i(c instanceof Error?c.message:"Failed to trigger sync")}finally{R(!1)}}async function Q(s){i(null),x(null);try{await se(s),x(`Pruned cached release ${s}.`),await j()}catch(c){i(c instanceof Error?c.message:"Failed to prune release")}}const G=t?.status==="ready"&&r.every(s=>s.release!==t.release);return e.jsxs("div",{className:"page-container",children:[e.jsx("div",{className:"page-header",children:e.jsxs("div",{className:"page-header-left",children:[e.jsx("h1",{className:"page-title",children:"Backlinks"}),e.jsx("p",{className:"page-subtitle",children:"Find domains that link to your projects, computed from the open Common Crawl web graph. Runs entirely on your machine — nothing is sent to third parties."})]})}),e.jsx(l,{className:"surface-card p-4 mb-6 border-amber-800/60",children:e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(L,{className:"h-5 w-5 text-amber-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-sm text-zinc-300 leading-relaxed",children:[e.jsx("p",{className:"font-medium text-amber-200",children:"Heads up — a release sync is a large download."}),e.jsxs("ul",{className:"mt-1.5 space-y-1 text-zinc-400",children:[e.jsxs("li",{children:[e.jsx("span",{className:"text-zinc-200",children:"~16 GB"})," of gzipped vertex + edge files per release, stored at"," ",e.jsx("code",{className:"text-zinc-300",children:"~/.canonry/cache/commoncrawl/"}),"."]}),e.jsxs("li",{children:[e.jsx("span",{className:"text-zinc-200",children:"10–20 min on a fast connection"})," for the download, then ~5 min for the DuckDB query."]}),e.jsx("li",{children:"One sync covers every project in this workspace. Releases are immutable, so the download only happens once per release."})]})]})]})}),e.jsxs("section",{className:"page-section-divider",children:[e.jsx("div",{className:"section-head section-head-inline",children:e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"About"}),e.jsx("h2",{children:"How it works"})]})}),e.jsxs(l,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-sm text-zinc-400 leading-relaxed max-w-3xl mb-4",children:["Common Crawl publishes a quarterly snapshot of the public web’s hyperlink graph. Canonry downloads one"," ",e.jsx("span",{className:"text-zinc-200",children:"release"})," at a time and extracts backlinks for every project in this workspace in a single pass."]}),e.jsxs("ol",{className:"space-y-3 text-sm text-zinc-400 max-w-3xl",children:[e.jsxs("li",{className:"flex gap-3",children:[e.jsx("span",{className:"shrink-0 inline-flex h-6 w-6 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900 text-xs font-semibold text-zinc-300 tabular-nums",children:"1"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-zinc-200 font-medium",children:"Download (one-time, ~16 GB)"})," — vertex + edge files cached to"," ",e.jsx("code",{className:"text-zinc-300",children:"~/.canonry/cache/commoncrawl/"}),". Runs once per release; subsequent operations reuse the cache."]})]}),e.jsxs("li",{className:"flex gap-3",children:[e.jsx("span",{className:"shrink-0 inline-flex h-6 w-6 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900 text-xs font-semibold text-zinc-300 tabular-nums",children:"2"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-zinc-200 font-medium",children:"Query (~5 min)"})," — one DuckDB pass scans the cached files and extracts referring domains for every project’s canonical domain. DuckDB is only used to ",e.jsx("span",{className:"text-zinc-200",children:"read"})," these dumps; it doesn’t store any canonry state."]})]}),e.jsxs("li",{className:"flex gap-3",children:[e.jsx("span",{className:"shrink-0 inline-flex h-6 w-6 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900 text-xs font-semibold text-zinc-300 tabular-nums",children:"3"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-zinc-200 font-medium",children:"Persist"})," — results land in the same SQLite database the rest of canonry uses. After the first sync, per-project reads (and re-run extracts against the cached release) are instant."]})]})]})]})]}),q&&e.jsx(l,{className:"surface-card p-4 mb-4 border-rose-800/60",children:e.jsx("p",{className:"text-sm text-rose-300",children:q})}),I&&e.jsx(l,{className:"surface-card p-4 mb-4 border-emerald-800/60",children:e.jsx("p",{className:"text-sm text-emerald-300",children:I})}),e.jsxs("section",{className:"page-section-divider",children:[e.jsxs("div",{className:"section-head section-head-inline",children:[e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"Dependency"}),e.jsxs("h2",{className:"flex items-center gap-2",children:["DuckDB install status",e.jsxs(h,{label:"Why DuckDB?",children:[e.jsx("span",{className:"block",children:"DuckDB is a query engine canonry uses to scan the ~16 GB Common Crawl dumps and pull out your referring domains."}),e.jsxs("span",{className:"mt-2 block text-zinc-400",children:["It does ",e.jsx("span",{className:"text-zinc-200",children:"not"})," store any canonry data — your backlink results live in SQLite alongside the rest of your projects. DuckDB is purely a tool for processing the raw CSV files."]}),e.jsxs("span",{className:"mt-2 block text-zinc-500",children:["Installed on demand (not bundled) into ",e.jsx("code",{className:"text-zinc-300",children:"~/.canonry/plugins/"})," so users who never run backlinks don’t pay the ~40 MB install cost."]})]})]})]}),a?.duckdbInstalled?e.jsx(f,{tone:"positive",children:"Installed"}):e.jsx(f,{tone:"caution",children:"Not installed"})]}),e.jsx(l,{className:"surface-card p-5",children:D?e.jsx("p",{className:"text-sm text-zinc-500",children:"Checking…"}):a?.duckdbInstalled?e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(ae,{className:"h-5 w-5 text-emerald-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-sm text-zinc-200",children:["Version ",a.duckdbVersion??"unknown"," installed at"," ",e.jsx("code",{className:"text-zinc-300",children:a.pluginDir})]}),e.jsxs("p",{className:"text-xs text-zinc-500 mt-1",children:["Required spec: ",a.duckdbSpec]})]})]}):e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(re,{className:"h-5 w-5 text-amber-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"flex-1",children:[e.jsx("p",{className:"text-sm text-zinc-200",children:"DuckDB is not installed. It’s the query engine canonry uses to scan Common Crawl dumps — required before you can run a release sync or per-project extract."}),e.jsx("p",{className:"text-xs text-zinc-500 mt-1",children:"Installing doesn’t touch your project data. DuckDB only reads the downloaded CSV files; backlink results are written to the same SQLite database canonry already uses."}),a&&e.jsxs("p",{className:"text-xs text-zinc-500 mt-1",children:["Will be installed into ",e.jsx("code",{className:"text-zinc-300",children:a.pluginDir})," (~40 MB)."]}),e.jsx("div",{className:"mt-3",children:e.jsxs(z,{type:"button",size:"sm",disabled:C,onClick:A(T),children:[e.jsx(te,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),C?"Installing…":"Install DuckDB"]})})]})]})})]}),e.jsxs("section",{className:"page-section-divider",children:[e.jsxs("div",{className:"section-head section-head-inline",children:[e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"Latest sync"}),e.jsxs("h2",{className:"flex items-center gap-2",children:["Release sync",e.jsx(h,{label:"What is a release sync?",children:"A release sync downloads one Common Crawl dump (~16 GB) and extracts backlinks for every project in this workspace in one pass. This is the heavy job — subsequent per-project re-runs skip the download and just re-query the cached files."})]})]}),t&&e.jsx(f,{tone:v(t.status),children:t.status})]}),e.jsxs(l,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-xs text-zinc-500 max-w-3xl mb-4",children:["A release is one Common Crawl dump (e.g. ",e.jsx("code",{className:"text-zinc-400",children:"cc-main-2026-jan-feb-mar"}),"). Syncing it downloads the graph and populates backlinks for every project in this workspace."]}),t?e.jsxs("div",{className:"space-y-2 text-sm",children:[e.jsxs("p",{className:"text-zinc-200",children:["Release ",e.jsx("code",{className:"text-zinc-300",children:t.release})]}),t.phaseDetail&&e.jsx("p",{className:"text-zinc-500",children:t.phaseDetail}),e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-zinc-500 pt-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-zinc-600 uppercase tracking-wide",children:"Projects"}),e.jsx("p",{className:"text-zinc-300 mt-0.5",children:t.projectsProcessed??"—"})]}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-zinc-600 uppercase tracking-wide flex items-center gap-1",children:["Rows",e.jsx(h,{label:"What are rows?",children:"Total number of (project, referring domain) pairs persisted in SQLite from this sync, across every project in the workspace."})]}),e.jsx("p",{className:"text-zinc-300 mt-0.5",children:t.domainsDiscovered??"—"})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-zinc-600 uppercase tracking-wide",children:"Started"}),e.jsx("p",{className:"text-zinc-300 mt-0.5",children:b(t.downloadStartedAt??t.createdAt)})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-zinc-600 uppercase tracking-wide",children:"Finished"}),e.jsx("p",{className:"text-zinc-300 mt-0.5",children:b(t.queryFinishedAt)})]})]}),t.error&&e.jsx("p",{className:"text-sm text-rose-400 pt-2",children:t.error})]}):e.jsx("p",{className:"text-sm text-zinc-500",children:"No release sync has run in this workspace yet."}),G&&e.jsx("div",{className:"mt-4 rounded border border-amber-800/60 bg-amber-950/20 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsx(L,{className:"h-4 w-4 text-amber-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-xs text-zinc-300 leading-relaxed",children:[e.jsx("p",{className:"font-medium text-amber-200",children:"Cached files for this release are missing."}),e.jsxs("p",{className:"mt-1 text-zinc-400",children:["The sync record in the database says this release finished successfully, but the ~16 GB dump at"," ",e.jsxs("code",{className:"text-zinc-300",children:["~/.canonry/cache/commoncrawl/",t?.release,"/"]})," isn’t on disk. Your backlink data is still intact (it lives in SQLite), but per-project re-run extracts will fail until you either re-sync this release or start a new one."]})]})]})}),e.jsxs("div",{className:"mt-4 rounded border border-zinc-800 bg-zinc-900/40 p-3",children:[e.jsxs("div",{className:"flex items-start justify-between gap-3 mb-3",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-[10px] uppercase tracking-wide text-zinc-500",children:"Auto-detected release"}),u?e.jsxs("p",{className:"text-sm text-zinc-200 mt-0.5",children:[e.jsx("code",{className:"text-zinc-100",children:u.release}),e.jsxs("span",{className:"ml-2 text-xs text-zinc-500",children:["— vertex ",w(u.vertexBytes),", edges ",w(u.edgesBytes)]})]}):e.jsx("p",{className:"text-sm text-zinc-500 mt-0.5",children:D?"Probing Common Crawl…":"Could not auto-detect — pass an explicit release below."}),e.jsxs("a",{href:oe,target:"_blank",rel:"noopener noreferrer",className:"mt-1 inline-flex items-center gap-1 text-xs text-zinc-400 hover:text-zinc-200 focus:text-zinc-200 focus:outline-none focus-visible:ring-1 focus-visible:ring-zinc-500 rounded",children:["Browse all Common Crawl web-graph releases",e.jsx(de,{className:"h-3 w-3","aria-hidden":!0})]})]}),e.jsxs("div",{className:"flex items-center gap-2 shrink-0",children:[e.jsxs(z,{type:"button",size:"sm",disabled:p||!a?.duckdbInstalled||!u&&!N.trim(),onClick:A($),children:[e.jsx(K,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),p?"Queuing…":"Run sync"]}),e.jsxs(h,{label:"What does Run sync do?",children:[e.jsxs("span",{className:"block",children:["Downloads the auto-detected (or chosen) Common Crawl release (~16 GB) to"," ",e.jsx("code",{className:"text-zinc-300",children:"~/.canonry/cache/commoncrawl/"}),", then runs a single DuckDB query that extracts referring domains for every project in this workspace."]}),e.jsxs("span",{className:"mt-2 block text-zinc-400",children:["First time for a release: ",e.jsx("span",{className:"text-zinc-200",children:"~10–20 min download + ~5 min query"}),". Re-running the same release later: ",e.jsx("span",{className:"text-zinc-200",children:"skips download, just re-queries"})," (~5 min)."]})]})]})]}),F?e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("input",{type:"text",className:"flex-1 min-w-[240px] rounded border border-zinc-700 bg-transparent px-2.5 py-1.5 text-sm text-zinc-200 placeholder-zinc-600 focus:border-zinc-500 focus:outline-none",placeholder:"cc-main-2026-jan-feb-mar",value:N,onChange:s=>g(s.target.value),disabled:p,autoFocus:!0}),e.jsx("button",{type:"button",className:"text-xs text-zinc-500 hover:text-zinc-300 focus:text-zinc-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-zinc-500 rounded",onClick:()=>{g(""),k(!1)},disabled:p,children:"Cancel"})]}):e.jsx("button",{type:"button",className:"text-xs text-zinc-500 hover:text-zinc-300 focus:text-zinc-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-zinc-500 rounded",onClick:()=>k(!0),disabled:p,children:"Use a different release →"})]}),!a?.duckdbInstalled&&e.jsx("p",{className:"text-xs text-zinc-600 mt-2",children:"Install DuckDB first to enable sync."})]})]}),e.jsxs("section",{className:"page-section-divider",children:[e.jsx("div",{className:"section-head section-head-inline",children:e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"Cached releases"}),e.jsxs("h2",{className:"flex items-center gap-2",children:["Local disk cache",e.jsxs(h,{label:"What is this?",children:[e.jsxs("span",{className:"block",children:["Raw Common Crawl dumps stored at"," ",e.jsx("code",{className:"text-zinc-300",children:"~/.canonry/cache/commoncrawl/<release>/"}),". Each release takes ~16 GB."]}),e.jsxs("span",{className:"mt-2 block text-zinc-400",children:["These files are needed to re-run per-project extracts against a release without re-downloading. Pruning here ",e.jsx("span",{className:"text-zinc-200",children:"does not delete your backlink data"})," — that lives in SQLite."]})]})]})]})}),e.jsx("p",{className:"text-xs text-zinc-500 mb-3 max-w-3xl",children:"Each cached release is a ~16 GB pair of gzipped files. They’re needed to re-query the graph (e.g. for a newly-added project) without re-downloading. Safe to prune — backlink results persist in SQLite."}),e.jsx(l,{className:"surface-card overflow-hidden",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-zinc-800 text-left text-xs uppercase tracking-wide text-zinc-600",children:[e.jsx("th",{className:"px-4 py-2 font-medium",children:"Release"}),e.jsx("th",{className:"px-4 py-2 font-medium",children:"Sync status"}),e.jsx("th",{className:"px-4 py-2 text-right font-medium",children:"Size"}),e.jsx("th",{className:"px-4 py-2 font-medium",children:"Last used"}),e.jsx("th",{className:"px-4 py-2 font-medium sr-only",children:"Actions"})]})}),e.jsxs("tbody",{children:[r.map(s=>e.jsxs("tr",{className:"border-b border-zinc-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-zinc-200",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:s.syncStatus?e.jsx(f,{tone:v(s.syncStatus),children:s.syncStatus}):e.jsx("span",{className:"text-zinc-600",children:"—"})}),e.jsx("td",{className:"px-4 py-2 text-right text-zinc-400 tabular-nums",children:w(s.bytes)}),e.jsx("td",{className:"px-4 py-2 text-zinc-400",children:b(s.lastUsedAt)}),e.jsx("td",{className:"px-4 py-2 text-right",children:e.jsxs("div",{className:"inline-flex items-center gap-1",children:[e.jsxs(z,{type:"button",variant:"outline",size:"sm",onClick:()=>{Q(s.release)},children:[e.jsx(ne,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),"Prune"]}),e.jsx(h,{label:"What does Prune do?",placement:"top",children:"Deletes the ~16 GB cache for this release from disk. Backlink results already in SQLite remain untouched. To re-run extracts against this release, you’d have to sync it again (another ~16 GB download)."})]})})]},s.release)),r.length===0&&e.jsx("tr",{children:e.jsx("td",{className:"px-4 py-4 text-sm text-zinc-500",colSpan:5,children:"No cached releases on this machine. If you ran a sync from a different machine (or deleted the cache), the backlink data is still in the database — but you’ll need to re-sync a release to run new extracts."})})]})]})})]}),o.length>1&&e.jsxs("section",{className:"page-section-divider",children:[e.jsx("div",{className:"section-head section-head-inline",children:e.jsxs("div",{children:[e.jsx("p",{className:"eyebrow eyebrow-soft",children:"History"}),e.jsx("h2",{children:"Past release syncs"})]})}),e.jsx(l,{className:"surface-card overflow-hidden",children:e.jsxs("table",{className:"w-full text-sm",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"border-b border-zinc-800 text-left text-xs uppercase tracking-wide text-zinc-600",children:[e.jsx("th",{className:"px-4 py-2 font-medium",children:"Release"}),e.jsx("th",{className:"px-4 py-2 font-medium",children:"Status"}),e.jsx("th",{className:"px-4 py-2 text-right font-medium",children:"Projects"}),e.jsx("th",{className:"px-4 py-2 text-right font-medium",children:"Rows"}),e.jsx("th",{className:"px-4 py-2 font-medium",children:"Finished"})]})}),e.jsx("tbody",{children:o.map(s=>e.jsxs("tr",{className:"border-b border-zinc-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-zinc-200",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:e.jsx(f,{tone:v(s.status),children:s.status})}),e.jsx("td",{className:"px-4 py-2 text-right text-zinc-400 tabular-nums",children:s.projectsProcessed??"—"}),e.jsx("td",{className:"px-4 py-2 text-right text-zinc-400 tabular-nums",children:s.domainsDiscovered??"—"}),e.jsx("td",{className:"px-4 py-2 text-zinc-400",children:b(s.queryFinishedAt??s.updatedAt)})]},s.id))})]})})]})]})}export{fe as BacklinksPage};
@@ -0,0 +1 @@
1
+ import{c as n}from"./index-avJiga1P.js";const r=[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]],s=n("chevron-left",r),f={contentStyle:{backgroundColor:"#18181b",border:"1px solid #3f3f46",borderRadius:8,fontSize:12},labelStyle:{color:"#e4e4e7"},itemStyle:{color:"#a1a1aa"}},d={fill:"#71717a",fontSize:11},l="#27272a",S="#27272a",a=["#34d399","#60a5fa","#f472b6","#facc15","#a78bfa","#fb923c","#22d3ee","#f87171"],c={gemini:"#60a5fa",openai:"#34d399",claude:"#fb923c",perplexity:"#22d3ee",local:"#a78bfa"};function R(t,e=0){return c[t]??a[e%a.length]}const T={text:"#a1a1aa",textDim:"#71717a",textFaint:"#52525b",surface:"#27272a"},u={positive:"#34d399",positiveDeep:"#10b981"};function o(t){const e=String(t);return e.includes("T")?new Date(e):new Date(e+"T00:00:00")}function C(t){return o(String(t)).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}function _(t){const e=o(t);return`${e.getMonth()+1}/${e.getDate()}`}export{S as C,d as a,C as b,f as c,T as d,a as e,_ as f,u as g,s as h,l as i,R as p};