@canonry/canonry 4.128.0 → 4.129.1

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 (35) hide show
  1. package/README.md +15 -10
  2. package/assets/agent-workspace/skills/aero/SKILL.md +15 -7
  3. package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -1
  4. package/assets/agent-workspace/skills/aero/references/orchestration.md +5 -3
  5. package/assets/agent-workspace/skills/aero/soul.md +1 -1
  6. package/assets/agent-workspace/skills/canonry/SKILL.md +50 -13
  7. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -1
  8. package/assets/agent-workspace/skills/canonry/references/indexing.md +4 -2
  9. package/assets/assets/{AuditHistoryPanel-DwJpVWme.js → AuditHistoryPanel-wJqs99Ak.js} +1 -1
  10. package/assets/assets/{BacklinksPage-K8FQUBxI.js → BacklinksPage-BBDExYo_.js} +1 -1
  11. package/assets/assets/{ChartPrimitives-6oFKrJFk.js → ChartPrimitives-DSr5ldMf.js} +1 -1
  12. package/assets/assets/{HistoryPage-DwOAoN1Z.js → HistoryPage-Ca-GGnTI.js} +1 -1
  13. package/assets/assets/{ProjectPage-CdgFUk7h.js → ProjectPage-BuaSDSwf.js} +1 -1
  14. package/assets/assets/{RunRow-lt9IS99U.js → RunRow-O9TbOdQY.js} +1 -1
  15. package/assets/assets/{RunsPage-DitYcIdx.js → RunsPage-C1Sqd7AN.js} +1 -1
  16. package/assets/assets/{SettingsPage-CAOrh65Q.js → SettingsPage-DPFh62BV.js} +1 -1
  17. package/assets/assets/{TrafficPage-BqzNvx75.js → TrafficPage-5vKoQyoP.js} +1 -1
  18. package/assets/assets/TrafficSourceDetailPage-D17oADs8.js +1 -0
  19. package/assets/assets/{arrow-left-AmG4gtUR.js → arrow-left-De5P5AIE.js} +1 -1
  20. package/assets/assets/extract-error-message-BqMd-fQ6.js +1 -0
  21. package/assets/assets/{index-BiCDkOH4.js → index-C1ABEgkr.js} +53 -53
  22. package/assets/assets/{trash-2-gFHWd1A1.js → trash-2-6ZpABj9G.js} +1 -1
  23. package/assets/index.html +1 -1
  24. package/dist/{chunk-ZKUSY4PI.js → chunk-GYAKRVE2.js} +25 -5
  25. package/dist/{chunk-BLSHHRRU.js → chunk-K4FDDCYG.js} +29 -0
  26. package/dist/{chunk-NCFH26X5.js → chunk-LPEE5AYY.js} +198 -25
  27. package/dist/{chunk-IHRW6WWV.js → chunk-WWZE7VDQ.js} +10 -6
  28. package/dist/cli.js +226 -38
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +4 -4
  31. package/dist/{intelligence-service-SH5Z6JFT.js → intelligence-service-2QBAGT5X.js} +2 -2
  32. package/dist/mcp.js +2 -2
  33. package/package.json +7 -7
  34. package/assets/assets/TrafficSourceDetailPage-DXbygZUA.js +0 -1
  35. package/assets/assets/extract-error-message-DlVtjoPz.js +0 -1
package/README.md CHANGED
@@ -8,12 +8,13 @@
8
8
  - Watch AI engines crawl and refer traffic via [server-log ingestion](skills/canonry/references/server-side-traffic.md) — Cloud Run, Vercel, and the WordPress Traffic Logger plugin today
9
9
  - 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)
10
10
  - 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
11
+ - 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
11
12
  - Discover who links to you with [Common Crawl backlinks](skills/canonry/references/canonry-cli.md#backlinks-common-crawl) — follows Common Crawl's rolling monthly hyperlink graph, auto-syncing each new window on a schedule, queried locally with DuckDB
12
13
  - Execute fixes via [WordPress](docs/wordpress-setup.md), JSON-LD schema, and indexing submissions
13
14
  - Manage many clients declaratively — config-as-code YAML + `cnry apply`
14
15
  - Schedule recurring visibility checks, traffic syncs, and Business Profile syncs, with webhook alerts on regressions
15
16
  - Generate client-ready HTML reports — `cnry report <project>`
16
- - Drive from your own agent via the [67-tool MCP adapter](docs/mcp.md) or webhooks
17
+ - Drive from your own agent via the [MCP adapter](docs/mcp.md), a [native Codex or Claude Code plugin](docs/plugins.md), or webhooks
17
18
  - Or use **Aero** — Canonry's built-in agent that wakes up after every run
18
19
 
19
20
  Every dashboard view has a matching CLI command and API endpoint. The CLI is the surface; the UI consumes the same API your agent does.
@@ -54,20 +55,22 @@ cnry project update my-site --provider gemini --provider-model gemini=gemini-2.5
54
55
  cnry project update my-site --clear-provider-model gemini
55
56
  ```
56
57
 
57
- ## Or set it up with your AI coding agent
58
+ ## Or use any shell-capable coding agent
58
59
 
59
- Drop this into Claude Code, Codex, or any shell-capable agent. It installs canonry, runs your first sweep, audits your site for AEO readiness, and stops for your sign-off before taking any action on your behalf:
60
+ Without the native plugin, drop this into any shell-capable agent. It keeps
61
+ credential setup private and asks before each persisted or quota-consuming
62
+ operation:
60
63
 
61
64
  ```text
62
65
  Set up canonry for me. Canonry is an open-source platform that tracks how AI answer engines (Gemini, ChatGPT, Claude, Perplexity) cite my site.
63
66
 
64
67
  1. Ask me for: my domain, 3–5 queries I want to track, and which provider I want to start with (gemini / openai / claude / perplexity). Wait for my answers before proceeding.
65
- 2. Run `npm install -g @canonry/canonry`.
66
- 3. Run `cnry init` in this directory. This scaffolds config and installs the canonry skills into `.claude/skills/canonry/`, `.claude/skills/aero/`, `.codex/skills/canonry/`, and `.codex/skills/aero/`. If the skills aren't there afterwards, run `cnry skills install`.
67
- 4. Read the operator playbook at `.claude/skills/canonry/SKILL.md` and follow it end-to-end: create the project with my domain and queries, wire up the provider key I chose, and trigger the first sweep.
68
- 5. Open my browser to the dashboard so I can see the run results.
69
- 6. Switch to the analyst playbook at `.claude/skills/aero/SKILL.md` and run a baseline AEO audit on my behalf. Read citation evidence with `cnry evidence <project> --format json`, then run `cnry technical-aeo run <project> --wait` followed by `cnry technical-aeo score <project> --format json` for a site-readiness score. This crawls every page in my sitemap (not just the homepage), scores the whole site 0–100, and saves the result so it appears in the dashboard and can be re-audited on a schedule.
70
- 7. Summarize what you found: my mention and citation rates per provider, the top 3 queries I'm not yet cited on, and the highest-impact site issues from the audit. Ask me for permission before taking any further action, such as drafting content, submitting URLs for indexing, editing files, or anything else that changes my site.
68
+ 2. Ask for approval, then run `npm install -g @canonry/canonry`.
69
+ 3. Do not run `cnry init` yourself or ask me for credentials. Tell me to run `cnry init` in my own private terminal because it prompts for provider secrets and prints the new full-access API key once. Wait for me to confirm completion; never ask me to paste its output. This scaffolds config and installs the Canonry skills.
70
+ 4. Read `.claude/skills/canonry/SKILL.md`, run the read-only doctor checks, and show me the exact project/domain/query changes you propose. Ask for explicit approval before creating the project or changing queries.
71
+ 5. After project setup, show the provider and query count for the first sweep and ask for explicit approval for that quota-consuming run. Only then trigger it.
72
+ 6. Read `.claude/skills/aero/SKILL.md` and summarize the existing mention and citation evidence. Propose the technical audit, including its page limit, and ask for separate approval before running `cnry technical-aeo run <project> --wait`; after approval, read the result with `cnry technical-aeo score <project> --format json`.
73
+ 7. Open the dashboard only if I ask, then summarize what you found: mention and citation rates per provider, the top 3 gaps, and the highest-impact site issues. Ask again before drafting content, submitting URLs, editing files, publishing, or performing any other mutation or quota-consuming operation.
71
74
  ```
72
75
 
73
76
  One-click copy at [canonry.ai](https://canonry.ai).
@@ -100,11 +103,13 @@ Configure during `cnry init`, in the dashboard `/settings`, or as env vars.
100
103
  |---|---|
101
104
  | **Architecture & data model** | [docs/architecture.md](docs/architecture.md) · [docs/data-model.md](docs/data-model.md) |
102
105
  | **Aero — built-in agent** | [skills/aero/SKILL.md](skills/aero/SKILL.md) |
106
+ | **Native plugins — Codex / Claude Code** | [docs/plugins.md](docs/plugins.md) |
103
107
  | **MCP — Claude Desktop / Cursor / Codex** | [docs/mcp.md](docs/mcp.md) |
104
108
  | **Integrations** | [GSC](docs/google-search-console-setup.md) · [GA4](docs/google-analytics-setup.md) · [Bing](docs/bing-webmaster-setup.md) · [Google Business Profile](skills/canonry/references/google-business-profile.md) · [WordPress](docs/wordpress-setup.md) · [Server-side traffic (Cloud Run + Vercel + WordPress logs)](skills/canonry/references/server-side-traffic.md) |
105
109
  | **Deployment** — Docker, Railway, Render, systemd, Tailscale | [docs/deployment.md](docs/deployment.md) |
106
110
  | **API** — 118+ endpoints | `GET /api/v1/openapi.json` (no auth) |
107
- | **Skills bundle** for Claude Code / Codex | `cnry skills install` ([details](skills/canonry/SKILL.md)) |
111
+ | **Standalone skills bundle** for Claude Code / Codex | `cnry skills install` ([details](skills/canonry/SKILL.md)) |
112
+ | **Roadmap & ADRs** | [docs/roadmap.md](docs/roadmap.md) · [docs/adr/](docs/adr/) |
108
113
  | **All docs** | [docs/README.md](docs/README.md) |
109
114
 
110
115
  ## Requirements
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: aero
3
- slug: aero
4
3
  description: AEO analyst orchestration — coordinates canonry sweeps and aeo-audit analysis with persistent memory and proactive regression response.
5
- homepage: https://canonry.ai
6
- repository: https://github.com/AINYC/aero
4
+ metadata:
5
+ homepage: https://canonry.ai
6
+ repository: https://github.com/AINYC/aero
7
7
  ---
8
8
 
9
9
  # Aero Orchestration Skill
@@ -16,7 +16,7 @@ Persist only *user-scoped* context (operator preferences, communication style) i
16
16
 
17
17
  **Two signals, not one.** Every (query × provider) snapshot tracks **mentioned** (brand in answer text) and **cited** (domain in source links) independently. Lead with **Mention Coverage** when narrating health — it is the primary gauge — and report **Citation Coverage** as the secondary signal. Never compute one from the other, and never collapse them into a single "visibility" headline. The downloadable report (`cnry report`) and the dashboard hero both honor this split.
18
18
 
19
- When a project has GA4 connected, traffic is a first-class signal alongside mentions and citations. Use `cnry ga traffic` / `cnry ga attribution --trend` for the current snapshot, `cnry ga ai-referral-history` and `cnry ga social-referral-history` for daily series. Reads query a local DB synced by `cnry ga sync` — confirm `cnry ga status` shows a recent `lastSyncedAt` before quoting numbers; if stale, re-sync first. When the project has a server-side traffic source attached (Cloud Run / WordPress / Vercel), `cnry traffic status` and `cnry traffic events` surface crawler + AI-referral evidence the GA4 layer can miss. Full command reference and return shapes live in the co-installed `canonry/references/canonry-cli.md`.
19
+ When a project has GA4 connected, traffic is a first-class signal alongside mentions and citations. Use `cnry ga traffic` / `cnry ga attribution --trend` for the current snapshot, `cnry ga ai-referral-history` and `cnry ga social-referral-history` for daily series. Reads query a local DB synced by `cnry ga sync` — confirm `cnry ga status` shows a recent `lastSyncedAt` before quoting numbers; if stale, ask for explicit approval before re-syncing. When the project has a server-side traffic source attached (Cloud Run / WordPress / Vercel), `cnry traffic status` and `cnry traffic events` surface crawler + AI-referral evidence the GA4 layer can miss. Full command reference and return shapes live in the co-installed `canonry/references/canonry-cli.md`.
20
20
 
21
21
  **Diagnosing a stuck Vercel/Cloud Run source:** if `cnry traffic status` shows `status=error` with a recent `lastError` of `refusing to advance` or `ExceedsBillingLimitError`, the source's `lastSyncedAt` has aged past the upstream retention boundary and every sync now throws. Recovery: `cnry traffic reset <project> --source <id> --advance-to-now`. This advances `lastSyncedAt` to NOW and resumes going-forward syncs — historical events in the gap are unrecoverable from the sync path; run `cnry traffic backfill --days N` separately if any of that history is needed (capped at retention).
22
22
 
@@ -35,19 +35,27 @@ Mention is the primary gauge (see "Two signals, not one" above); citation is the
35
35
  ### What NOT to Do
36
36
  - Don't promise fixes will appear in the next sweep (AEO changes take weeks/months)
37
37
  - Don't give generic SEO advice — always ground recommendations in mention and citation data, leading with the mention signal
38
- - Don't run sweeps without user confirmation (they consume API quota)
38
+ - Don't run sweeps, probes, syncs, audits, discovery sessions, or any other write or quota-consuming operation without explicit user approval
39
39
  - Don't edit client's code without showing diffs and getting approval
40
40
  - Don't conflate "not mentioned" with "page doesn't exist" — and don't conflate "not cited" with "not mentioned" either; check first. The two signals are independent (see "Two signals, not one") and are never computed from each other.
41
41
  - Don't coerce `answerMentioned` null → false. Null means "not checked," not "not mentioned" — treat it as missing data, never as a negative.
42
42
 
43
43
  ### When to use `--probe` runs
44
- When you need to **verify** something on your own initiative — "did the OpenAI provider migration land cleanly?", "is the regression still reproducible after the WP fix?", "does this query actually surface us when I think it should?" — use `cnry run <project> --probe --provider <p> --query "..."`. Probe runs:
44
+ When a verification would help, propose the exact probe and get explicit
45
+ approval before running it. A probe is safer for metrics than a real sweep, but
46
+ it is still a paid/quota-consuming write. After approval, use `cnry run
47
+ <project> --probe --provider <p> --query "..."`. Probe runs:
45
48
  - Still cost provider API quota (same wire call)
46
49
  - Write a snapshot you can inspect via `cnry runs get <id>`
47
50
  - Are EXCLUDED from dashboard, analytics, intelligence, insights, and notifications
48
51
  - Won't wake you up again via the post-run hook (no recursive analysis loops)
49
52
 
50
- Use probes whenever the run is for **your** investigation, not for the user's metrics. The two May-17 ainyc probes that broke the dashboard before this convention existed are the canonical example of why this matters — a 1-snapshot test masqueraded as "the latest sweep" and zeroed the headline.
53
+ Use an approved probe when the run is for investigation rather than the user's
54
+ metrics. Approval for one probe does not authorize repeats; ask again unless
55
+ the operator approved a specific bounded batch. The two May-17 ainyc probes
56
+ that broke the dashboard before this convention existed are the canonical
57
+ example of why this matters — a 1-snapshot test masqueraded as "the latest
58
+ sweep" and zeroed the headline.
51
59
 
52
60
  A real (non-probe) sweep is appropriate when the user explicitly asks to refresh data ("run it again", "get the latest", "trigger a sweep").
53
61
 
@@ -31,7 +31,9 @@ After probing, one Gemini call classifies every recurring cited domain into a `c
31
31
 
32
32
  ## Triggering a session
33
33
 
34
- The operator runs:
34
+ Discovery is a paid, quota-consuming write (about $1 at the default 100-probe
35
+ budget). Show the proposed ICP, provider, and probe budget, then get explicit
36
+ operator approval before starting a session. After approval, the operator runs:
35
37
 
36
38
  ```bash
37
39
  cnry discover run <project> --icp "..." --wait
@@ -14,7 +14,7 @@ Trigger: First sweep completes for a new project
14
14
  Steps:
15
15
  1. `cnry overview <project> --format json` → mention read first: `queryCounts.mentionRate`, `scores.mention`, `scores.mentionShare`. Then `cnry analytics <project> --feature gaps --format json` for `mentionedQueries[]` / `mentionGap[]` / `notMentioned[]`, and `cnry evidence <project> --format json` for the per-query `[C/c][M/m]` drilldown and the secondary cited data.
16
16
  2. Compute baseline in this order: **mention rate, mention share**, then cited rate; provider breakdown; top/bottom queries by mention.
17
- 3. `cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json` → site readiness score across every page in the sitemap (auto-discovered; add `--limit <n>` to `run` to cap, default 500). Persists to the dashboard and is trendable via `cnry technical-aeo trend <project>`.
17
+ 3. With explicit operator approval for the crawl and persisted run, `cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json` → site readiness score across every page in the sitemap (auto-discovered; add `--limit <n>` to `run` to cap, default 500). Persists to the dashboard and is trendable via `cnry technical-aeo trend <project>`.
18
18
  4. Identify top 3 gaps — lead with `mentionGap[]` / `notMentioned[]` (where competitors are named and you aren't), then the cited gaps with fixable site issues.
19
19
  5. Generate onboarding report with baseline + action plan
20
20
  6. Store baseline metrics in memory (include mention rate + mention share, not just cited rate)
@@ -34,13 +34,15 @@ Steps:
34
34
  8. If content fix: generate diff (schema, llms.txt, or content changes)
35
35
  9. Update memory with regression event + diagnosis (record which signal regressed: mention, citation, or both)
36
36
 
37
- **Want to verify the regression is real / reproducible before reporting?** Use a probe run instead of a real sweep:
37
+ **Want to verify the regression is real / reproducible before reporting?**
38
+ Propose the exact provider/query and get explicit approval, then use a probe
39
+ run instead of a real sweep:
38
40
 
39
41
  ```
40
42
  cnry run <project> --probe --provider <p> --query "<regressed-query>"
41
43
  ```
42
44
 
43
- Then `cnry runs get <id>` to inspect the snapshot. The probe's snapshot won't displace the latest scheduled sweep on the dashboard, won't generate insights, and won't fire notifications so you can re-test as many times as needed without polluting metrics. Promote to a real sweep (drop `--probe`) only if the operator explicitly wants the data to feed the dashboard.
45
+ Then `cnry runs get <id>` to inspect the snapshot. The probe's snapshot won't displace the latest scheduled sweep on the dashboard, won't generate insights, and won't fire notifications. It still costs provider quota and writes a snapshot, so approval for one probe does not authorize repeats; ask again unless the operator approved a specific bounded batch. Promote to a real sweep (drop `--probe`) only if the operator explicitly wants the data to feed the dashboard.
44
46
 
45
47
  ## Workflow 3: Weekly Review
46
48
 
@@ -12,7 +12,7 @@ You are **Aero** — an AEO analyst. You help operators understand whether AI an
12
12
  - **Evidence over opinion.** Numbers before interpretation. "ChatGPT stopped mentioning you for 'roof repair phoenix' between March 28 and April 2, and your mention share fell from 50% to 0%" beats "your visibility decreased" — then note the lost citation second.
13
13
  - **Proactive, not passive.** Regressions don't wait to be asked about. Surface them when you spot them. Flag competitors the moment they take mention share in answers you used to own, and when they appear in citations you own.
14
14
  - **Honest about uncertainty.** When the data is ambiguous, say so. Don't manufacture confidence. Don't promise fixes will appear in the next sweep — AEO changes take weeks.
15
- - **Cautious with writes.** Sweeps cost quota. Schedules shape downstream notifications. Queries define what gets tracked. Confirm intent before mutating state the operator will notice. When *you* need to test something (verify a fix, reproduce a regression), use `cnry run --probe` — same wire call, no dashboard/analytics/notification pollution.
15
+ - **Cautious with writes.** Sweeps and probes cost quota. Schedules shape downstream notifications. Queries define what gets tracked. Get explicit approval before every write or quota-consuming operation, including probes whose snapshots stay out of dashboard metrics. When an approved test needs a run, prefer `cnry run --probe` over a real sweep — same wire call, no dashboard/analytics/notification pollution.
16
16
  - **Canonry is the source of truth.** Read state back; never maintain a parallel copy in your head. Conclusions age, the data doesn't.
17
17
 
18
18
  ## Voice
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canonry
3
- description: "Agent-first AEO operating platform."
3
+ description: "Set up and operate Canonry AEO projects: inspect mention and citation coverage, diagnose regressions, run technical audits, and act through the Canonry CLI or MCP tools."
4
4
  metadata:
5
5
  {
6
6
  "agent":
@@ -16,14 +16,6 @@ metadata:
16
16
  "bins": ["canonry"],
17
17
  "label": "Install canonry globally",
18
18
  "command": "npm install -g @canonry/canonry"
19
- },
20
- {
21
- "id": "npx",
22
- "kind": "npx",
23
- "package": "@canonry/canonry",
24
- "bins": ["canonry"],
25
- "label": "Run canonry via npx",
26
- "command": "npx @canonry/canonry@latest init"
27
19
  }
28
20
  ],
29
21
  },
@@ -38,6 +30,50 @@ Agent-first open-source AEO (Answer Engine Optimization) operating platform. Tra
38
30
 
39
31
  **CLI:** invoke as `cnry` (short form) or `canonry` — both ship with the npm package and are interchangeable. Examples in this skill use `cnry`.
40
32
 
33
+ ## Runtime Preflight
34
+
35
+ Before using the native Codex or Claude Code plugin, verify the separately
36
+ installed Canonry runtime. It requires Node.js 22.14 or newer, and the native
37
+ plugin specifically needs the global package so `canonry-mcp` remains on
38
+ `PATH`; a one-off `npx` invocation is not sufficient:
39
+
40
+ ```bash
41
+ node --version
42
+ command -v cnry
43
+ cnry --version
44
+ ```
45
+
46
+ Then prepare the runtime in this order:
47
+
48
+ 1. With approval, install the global runtime if `command -v cnry` failed.
49
+ 2. Check only whether `$CANONRY_CONFIG_DIR/config.yaml` (when that variable is
50
+ set) or `~/.canonry/config.yaml` exists; do not print the file. If it does
51
+ not exist, pause and ask the operator to run `cnry init --skip-skills --skip-mcp`
52
+ in their own private terminal, then confirm completion without
53
+ pasting the output. Never execute `init` inside the agent session: it prompts
54
+ for provider credentials and prints the new full-access API key once. The
55
+ plugin already supplies the skills and MCP registration.
56
+ 3. After a fresh initialization, get approval and run `cnry start --format
57
+ json`; `start` waits for the health endpoint before returning. On an
58
+ existing installation, try the doctor command first and start only when the
59
+ transport is unavailable. Never stop or restart a healthy daemon just to
60
+ install the plugin.
61
+ 4. Run `cnry doctor --format json` after the daemon is ready. A successful JSON
62
+ response proves the health-check path works; individual checks may still
63
+ report actionable `warn` or `fail` statuses. Plugin installation state is
64
+ read live, so installing the plugin alone does not require a daemon restart.
65
+
66
+ Never ask the operator to paste credentials into the chat, print the raw API
67
+ key, or inspect `~/.canonry/config.yaml` for secrets. The plugin does not expand
68
+ the configured key's server-enforced scope, but it gives the client tools that
69
+ can exercise that scope. Fresh `cnry init` creates a full-instance `*` key, so
70
+ the default plugin has teammate-level access to every project and shared
71
+ instance settings. A write-capable key exposes write tools by default; a
72
+ read-only key restricts the catalog to reads, while a project-scoped key keeps
73
+ its project route boundary but is not tenant isolation: a write-capable scoped
74
+ key can still mutate shared instance settings. Do not work around a missing
75
+ tool or `403` by switching credentials.
76
+
41
77
  ## When to Use
42
78
 
43
79
  - Tracking brand **mentions** in AI answer text and **citations** in source links across providers
@@ -74,13 +110,13 @@ Configure `spec.brandAliases` on the project (or pass via `cnry apply`) so the m
74
110
 
75
111
  A canonry engagement follows the same loop regardless of project size:
76
112
 
77
- 1. **Diagnose** — Run a baseline sweep (`cnry run <project> --wait`) and a technical audit (`cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json`). The audit crawls every page in the project's sitemap (auto-discovered from `/sitemap.xml`, the sitemap index, or `robots.txt`) so readiness reflects the whole site, not just one page, and persists the score to the dashboard. Read Mention Coverage first, Citation Coverage second. See `references/aeo-analysis.md`.
113
+ 1. **Diagnose** — After explicit approval for the quota-consuming persisted runs, run a baseline sweep (`cnry run <project> --wait`) and a technical audit (`cnry technical-aeo run <project> --wait`, then `cnry technical-aeo score <project> --format json`). The audit crawls every page in the project's sitemap (auto-discovered from `/sitemap.xml`, the sitemap index, or `robots.txt`) so readiness reflects the whole site, not just one page, and persists the score to the dashboard. Read Mention Coverage first, Citation Coverage second. See `references/aeo-analysis.md`.
78
114
  2. **Prioritize** — Triage by impact: indexing gaps → schema gaps → content gaps → query strategy. Branded-term losses are urgent.
79
115
  3. **Execute** — Apply fixes via the canonry CLI or platform integrations. Use `--dry-run` on supported mutations (`cnry project delete`, `cnry query replace`, `cnry backfill ...`) to preview before committing. See `references/canonry-cli.md` for the full command catalog and `references/wordpress-integration.md` for the WordPress workflow.
80
- 4. **Monitor** — Re-run sweeps weekly (`cnry run --all --wait` fans out across every project). Correlate visibility shifts with deployments and competitor moves.
116
+ 4. **Monitor** — Re-run sweeps weekly only through an operator-approved schedule or after fresh explicit approval (`cnry run --all --wait` fans out across every project). Correlate visibility shifts with deployments and competitor moves.
81
117
  5. **Report** — Lead with data, not interpretation: "Lost the mention for `<query>` on Gemini between <date> and <date> — two competitors moved in. Here's what to fix." For a one-command client-facing summary, run `cnry report <project>` to generate a self-contained HTML bundle (mention + citation hero, competitor landscape, GSC + GA4 performance, insights, suggested next queries). Same payload is available via `--format json` and the `canonry_report` MCP tool.
82
118
 
83
- **Verifying without polluting metrics**: when you need to test something on your own initiative — "did the latest provider deploy work?", "is this regression reproducible?", "would this query actually surface us?" — use `cnry run <project> --probe --provider <p> --query "..."`. Probe runs write a snapshot you can inspect via `cnry runs get <id>` but are excluded from the dashboard, analytics, intelligence, report, and notifications. Use probes for *your* investigation; use real sweeps when the operator wants the data to feed metrics.
119
+ **Verifying without polluting metrics**: when a test would help — "did the latest provider deploy work?", "is this regression reproducible?", "would this query actually surface us?" — propose the exact provider/query and get explicit approval before using `cnry run <project> --probe --provider <p> --query "..."`. Probe runs still cost quota and write a snapshot, but are excluded from the dashboard, analytics, intelligence, report, and notifications. Approval for one probe does not authorize repeats unless the operator approved a bounded batch; use real sweeps only when the operator wants the data to feed metrics.
84
120
 
85
121
  ## Surgical Reads
86
122
 
@@ -132,6 +168,7 @@ Aero also wakes unprompted after every `run.completed` so insights and regressio
132
168
 
133
169
  ## Boundaries & Safety
134
170
 
171
+ - **Get explicit approval before every mutation or quota-consuming sweep** — reads and `--dry-run` previews are safe defaults
135
172
  - **Never touch live WordPress without explicit approval**
136
173
  - **Back up `~/.canonry/config.yaml` before any config edit**
137
174
  - **Never fabricate mention or citation data** — if a sweep hasn't run, say so; never coerce `answerMentioned` null → false (null = "not checked")
@@ -151,5 +188,5 @@ Aero also wakes unprompted after every `run.completed` so insights and regressio
151
188
 
152
189
  ---
153
190
 
154
- **Tools:** canonry v4+, @ainyc/aeo-audit v1.3+
191
+ **Tools:** canonry v4+, @ainyc/aeo-audit v1.3+
155
192
  **Website:** [canonry.ai](https://canonry.ai) | **Org:** [ainyc.ai](https://ainyc.ai) | **Reference:** [AINYC AEO Methodology](https://ainyc.ai/aeo-methodology)
@@ -15,6 +15,11 @@ cnry serve --embed --embed-allow-origin https://app.example.com # read-only em
15
15
  cnry --version
16
16
  ```
17
17
 
18
+ `cnry init` prompts for credentials and prints the new API key once. An agent
19
+ must pause and ask the operator to run it in a private terminal, without
20
+ pasting the output back. Do not run `init` or secret-bearing `bootstrap` inside
21
+ an agent transcript.
22
+
18
23
  ### Read-only embed mode (#716)
19
24
 
20
25
  Opt-in, OFF by default. Renders the dashboard "chromeless" (no nav/topbar/settings) so it can be iframed read-only, and emits a fail-closed framing contract. With `--embed` absent, served HTML + headers are byte-for-byte unchanged.
@@ -115,7 +120,10 @@ Run statuses: `queued` → `running` → `completed` / `failed` / `partial`
115
120
  | `backfill` | `cnry backfill ...` | partial (historical) | ✅ | — | — |
116
121
  | **`probe`** | `cnry run --probe ...` | ❌ | ❌ | ❌ | ❌ |
117
122
 
118
- Use `--probe` whenever you're testing on your own initiative — verifying a fix landed, reproducing a regression, sanity-checking a query — rather than producing data the user/dashboard will consume.
123
+ After explicit approval for the exact provider/query (or a bounded batch), use
124
+ `--probe` for verification rather than producing data the user/dashboard will
125
+ consume. A probe still costs provider quota and writes a snapshot; approval for
126
+ one probe does not authorize repeats.
119
127
 
120
128
  `snapshot` does not create a project or write to the DB. It generates category queries, runs providers, and produces a report for prospecting.
121
129
 
@@ -63,7 +63,9 @@ cnry google request-indexing <project> --all-unindexed
63
63
  - OAuth connection set up in canonry (`cnry settings` shows Google connection)
64
64
  - Officially intended for JobPosting/BroadcastEvent schema; in practice Google processes all URLs
65
65
 
66
- **After submitting:** Check coverage again after 48h. Once indexed, run a sweep — pages must be indexed before citation is possible.
66
+ **After submitting:** Check coverage again after 48h. Once indexed, ask for
67
+ explicit approval before running a quota-consuming sweep — pages must be
68
+ indexed before citation is possible.
67
69
 
68
70
  ---
69
71
 
@@ -160,4 +162,4 @@ Expected response: `202 Accepted`
160
162
  4. Submit sitemap to Bing WMT (manual, one-time per site)
161
163
  5. Send IndexNow batch for key URLs
162
164
  6. Re-check coverage after 48h
163
- 7. Run a sweep after pages confirmed indexed
165
+ 7. After explicit approval, run a sweep once pages are confirmed indexed
@@ -1 +1 @@
1
- import{r as a,u as h,j as e}from"./vendor-tanstack-Be8JdV5M.js";import{bQ as L,bR as P,be as E,m as u,B as x,g as v,T as M}from"./index-BiCDkOH4.js";const o=100;function k(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(()=>k(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(M,{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};
1
+ import{r as a,u as h,j as e}from"./vendor-tanstack-Be8JdV5M.js";import{bQ as L,bR as P,be as E,m as u,B as x,g as v,T as M}from"./index-C1ABEgkr.js";const o=100;function k(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(()=>k(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(M,{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};
@@ -1 +1 @@
1
- import{r as n,j as e}from"./vendor-tanstack-Be8JdV5M.js";import{c as P,c1 as V,ah as U,c2 as Y,c3 as K,c4 as J,g as i,aM as E,T as y,i as h,B as w,j as M,aq as X,c5 as Z,c6 as ee,ak as se,c7 as te}from"./index-BiCDkOH4.js";import{C as ae,D as ne,T as re,a as le}from"./trash-2-gFHWd1A1.js";import"./vendor-radix-CQ77EO2y.js";import"./vendor-recharts-DSxTZhaH.js";import"./vendor-markdown-6dwjPOKK.js";const ce=[["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"}]],ie=P("circle-alert",ce);const de=[["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"}]],oe=P("external-link",de);function m({children:t,label:u="More info",placement:a="top",className:d}){const o=n.useId(),[g,c]=n.useState(!1);return e.jsxs("span",{className:`relative inline-flex ${d??""}`,children:[e.jsx("button",{type:"button","aria-label":u,"aria-describedby":g?o:void 0,className:"inline-flex h-4 w-4 items-center justify-center rounded-full text-muted hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500",onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),onFocus:()=>c(!0),onBlur:()=>c(!1),children:e.jsx(le,{className:"h-3.5 w-3.5","aria-hidden":!0})}),g&&e.jsx("span",{id:o,role:"tooltip",className:`absolute z-50 w-64 rounded border border-strong bg-bg-elevated px-3 py-2 text-xs font-normal leading-relaxed text-strong shadow-lg ${a==="top"?"bottom-full mb-2":"top-full mt-2"} left-1/2 -translate-x-1/2 whitespace-normal`,children:t})]})}const xe="https://commoncrawl.org/web-graphs";function D(t){return t==null?"—":t>=1e12?`${(t/1e12).toFixed(1)} TB`:t>=1e9?`${(t/1e9).toFixed(1)} GB`:t>=1e6?`${(t/1e6).toFixed(1)} MB`:t>=1e3?`${(t/1e3).toFixed(1)} KB`:`${t} B`}function b(t){if(!t)return"—";const u=Date.now()-new Date(t).getTime(),a=Math.floor(u/6e4);if(a<1)return"just now";if(a<60)return`${a}m ago`;const d=Math.floor(a/60);return d<24?`${d}h ago`:`${Math.floor(d/24)}d ago`}function C(t){switch(t){case"ready":return"positive";case"failed":return"negative";case"downloading":case"querying":case"queued":return"caution"}}function ye(){const[t,u]=n.useState(null),[a,d]=n.useState(null),[o,g]=n.useState([]),[c,F]=n.useState([]),[p,$]=n.useState(null),[S,B]=n.useState(!0),[R,q]=n.useState(!1),[j,I]=n.useState(!1),[N,k]=n.useState(""),[T,v]=n.useState(!1),[L,l]=n.useState(null),[A,x]=n.useState(null),f=n.useCallback(async()=>{B(!0),l(null);try{const[s,r,z,H,O]=await Promise.all([V(),U().catch(()=>null),Y().catch(()=>[]),K().catch(()=>[]),J().catch(()=>null)]);u(s),d(r),g(z),F(H),$(O)}catch(s){l(s instanceof Error?s.message:"Failed to load backlinks status")}finally{B(!1)}},[]);n.useEffect(()=>{f()},[f]);async function Q(){q(!0),l(null),x(null);try{const s=await Z();x(s.alreadyPresent?`DuckDB already installed (${s.version}).`:`Installed DuckDB ${s.version}.`),await f()}catch(s){l(s instanceof Error?s.message:"Failed to install DuckDB")}finally{q(!1)}}async function G(){const s=N.trim()||void 0;I(!0),l(null),x(null);try{const r=await ee(s);x(s?`Queued sync for ${r.release}. Download + query runs in the background.`:`Queued sync for auto-discovered release ${r.release}. Download + query runs in the background.`),k(""),v(!1),await f()}catch(r){r instanceof se&&r.code==="MISSING_DEPENDENCY"?l("DuckDB is not installed. Install it first."):l(r instanceof Error?r.message:"Failed to trigger sync")}finally{I(!1)}}async function _(s){l(null),x(null);try{await te(s),x(`Pruned cached release ${s}.`),await f()}catch(r){l(r instanceof Error?r.message:"Failed to prune release")}}const W=a?.status==="ready"&&c.every(s=>s.release!==a.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(i,{className:"surface-card p-4 mb-6 border-caution-800/60",children:e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(E,{className:"h-5 w-5 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-sm text-neutral leading-relaxed",children:[e.jsx("p",{className:"font-medium text-caution-200",children:"Heads up — a release sync is a large download."}),e.jsxs("ul",{className:"mt-1.5 space-y-1 text-secondary",children:[e.jsxs("li",{children:[e.jsx("span",{className:"text-strong",children:"~16 GB"})," of gzipped vertex + edge files per release, stored at"," ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/cache/commoncrawl/"}),"."]}),e.jsxs("li",{children:[e.jsx("span",{className:"text-strong",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(i,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-sm text-secondary 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-strong",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-secondary 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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"1"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong font-medium",children:"Download (one-time, ~16 GB)"})," — vertex + edge files cached to"," ",e.jsx("code",{className:"text-neutral",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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"2"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong 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-strong",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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"3"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong 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."]})]})]})]})]}),L&&e.jsx(i,{className:"surface-card p-4 mb-4 border-negative-800/60",children:e.jsx("p",{className:"text-sm text-negative",children:L})}),A&&e.jsx(i,{className:"surface-card p-4 mb-4 border-positive-800/60",children:e.jsx("p",{className:"text-sm text-positive",children:A})}),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(m,{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-secondary",children:["It does ",e.jsx("span",{className:"text-strong",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-muted",children:["Installed on demand (not bundled) into ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/plugins/"})," so users who never run backlinks don’t pay the ~40 MB install cost."]})]})]})]}),t?.duckdbInstalled?e.jsx(y,{tone:"positive",children:"Installed"}):e.jsx(y,{tone:"caution",children:"Not installed"})]}),e.jsx(i,{className:"surface-card p-5",children:S?e.jsx("p",{className:"text-sm text-muted",children:"Checking…"}):t?.duckdbInstalled?e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(ae,{className:"h-5 w-5 text-positive-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-sm text-strong",children:["Version ",t.duckdbVersion??"unknown"," installed at"," ",e.jsx("code",{className:"text-neutral",children:t.pluginDir})]}),e.jsxs("p",{className:"text-xs text-muted mt-1",children:["Required spec: ",t.duckdbSpec]})]})]}):e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(ie,{className:"h-5 w-5 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"flex-1",children:[e.jsx("p",{className:"text-sm text-strong",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-muted 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."}),t&&e.jsxs("p",{className:"text-xs text-muted mt-1",children:["Will be installed into ",e.jsx("code",{className:"text-neutral",children:t.pluginDir})," (~40 MB)."]}),!h()&&e.jsx("div",{className:"mt-3",children:e.jsxs(w,{type:"button",size:"sm",disabled:R,onClick:M(Q),children:[e.jsx(ne,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),R?"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(m,{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."})]})]}),a&&e.jsx(y,{tone:C(a.status),children:a.status})]}),e.jsxs(i,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-xs text-muted max-w-3xl mb-4",children:["A release is one Common Crawl dump (e.g. ",e.jsx("code",{className:"text-secondary",children:"cc-main-2026-jan-feb-mar"}),"). Syncing it downloads the graph and populates backlinks for every project in this workspace."]}),a?e.jsxs("div",{className:"space-y-2 text-sm",children:[e.jsxs("p",{className:"text-strong",children:["Release ",e.jsx("code",{className:"text-neutral",children:a.release})]}),a.phaseDetail&&e.jsx("p",{className:"text-muted",children:a.phaseDetail}),e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-muted pt-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Projects"}),e.jsx("p",{className:"text-neutral mt-0.5",children:a.projectsProcessed??"—"})]}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-faint uppercase tracking-wide flex items-center gap-1",children:["Rows",e.jsx(m,{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-neutral mt-0.5",children:a.domainsDiscovered??"—"})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Started"}),e.jsx("p",{className:"text-neutral mt-0.5",children:b(a.downloadStartedAt??a.createdAt)})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Finished"}),e.jsx("p",{className:"text-neutral mt-0.5",children:b(a.queryFinishedAt)})]})]}),a.error&&e.jsx("p",{className:"text-sm text-negative-400 pt-2",children:a.error})]}):e.jsx("p",{className:"text-sm text-muted",children:"No release sync has run in this workspace yet."}),W&&e.jsx("div",{className:"mt-4 rounded border border-caution-800/60 bg-caution-950/20 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsx(E,{className:"h-4 w-4 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-xs text-neutral leading-relaxed",children:[e.jsx("p",{className:"font-medium text-caution-200",children:"Cached files for this release are missing."}),e.jsxs("p",{className:"mt-1 text-secondary",children:["The sync record in the database says this release finished successfully, but the ~16 GB dump at"," ",e.jsxs("code",{className:"text-neutral",children:["~/.canonry/cache/commoncrawl/",a?.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-base bg-bg-elevated/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-muted",children:"Auto-detected release"}),p?e.jsxs("p",{className:"text-sm text-strong mt-0.5",children:[e.jsx("code",{className:"text-heading",children:p.release}),e.jsxs("span",{className:"ml-2 text-xs text-muted",children:["— vertex ",D(p.vertexBytes),", edges ",D(p.edgesBytes)]})]}):e.jsx("p",{className:"text-sm text-muted mt-0.5",children:S?"Probing Common Crawl…":"Could not auto-detect — pass an explicit release below."}),e.jsxs("a",{href:xe,target:"_blank",rel:"noopener noreferrer",className:"mt-1 inline-flex items-center gap-1 text-xs text-secondary hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",children:["Browse all Common Crawl web-graph releases",e.jsx(oe,{className:"h-3 w-3","aria-hidden":!0})]})]}),!h()&&e.jsxs("div",{className:"flex items-center gap-2 shrink-0",children:[e.jsxs(w,{type:"button",size:"sm",disabled:j||!t?.duckdbInstalled||!p&&!N.trim(),onClick:M(G),children:[e.jsx(X,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),j?"Queuing…":"Run sync"]}),e.jsxs(m,{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-neutral",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-secondary",children:["First time for a release: ",e.jsx("span",{className:"text-strong",children:"~10–20 min download + ~5 min query"}),". Re-running the same release later: ",e.jsx("span",{className:"text-strong",children:"skips download, just re-queries"})," (~5 min)."]})]})]})]}),h()?null:T?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-strong bg-transparent px-2.5 py-1.5 text-sm text-strong placeholder-mono-600 focus:border-mono-500 focus:outline-none",placeholder:"cc-main-2026-jan-feb-mar",value:N,onChange:s=>k(s.target.value),disabled:j,autoFocus:!0}),e.jsx("button",{type:"button",className:"text-xs text-muted hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>{k(""),v(!1)},disabled:j,children:"Cancel"})]}):e.jsx("button",{type:"button",className:"text-xs text-muted hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>v(!0),disabled:j,children:"Use a different release →"})]}),!t?.duckdbInstalled&&e.jsx("p",{className:"text-xs text-faint 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(m,{label:"What is this?",children:[e.jsxs("span",{className:"block",children:["Raw Common Crawl dumps stored at"," ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/cache/commoncrawl/<release>/"}),". Each release takes ~16 GB."]}),e.jsxs("span",{className:"mt-2 block text-secondary",children:["These files are needed to re-run per-project extracts against a release without re-downloading. Pruning here ",e.jsx("span",{className:"text-strong",children:"does not delete your backlink data"})," — that lives in SQLite."]})]})]})]})}),e.jsx("p",{className:"text-xs text-muted 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(i,{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-base text-left text-xs uppercase tracking-wide text-faint",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"}),!h()&&e.jsx("th",{className:"px-4 py-2 font-medium sr-only",children:"Actions"})]})}),e.jsxs("tbody",{children:[c.map(s=>e.jsxs("tr",{className:"border-b border-mono-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-strong",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:s.syncStatus?e.jsx(y,{tone:C(s.syncStatus),children:s.syncStatus}):e.jsx("span",{className:"text-faint",children:"—"})}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:D(s.bytes)}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:b(s.lastUsedAt)}),!h()&&e.jsx("td",{className:"px-4 py-2 text-right",children:e.jsxs("div",{className:"inline-flex items-center gap-1",children:[e.jsxs(w,{type:"button",variant:"outline",size:"sm",onClick:()=>{_(s.release)},children:[e.jsx(re,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),"Prune"]}),e.jsx(m,{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)),c.length===0&&e.jsx("tr",{children:e.jsx("td",{className:"px-4 py-4 text-sm text-muted",colSpan:h()?4:5,children:"No cached releases on this machine. If you ran a sync from a different machine (or deleted the cache), the backlink data is still in the database — but you’ll need to re-sync a release to run new extracts."})})]})]})})]}),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(i,{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-base text-left text-xs uppercase tracking-wide text-faint",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-mono-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-strong",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:e.jsx(y,{tone:C(s.status),children:s.status})}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:s.projectsProcessed??"—"}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:s.domainsDiscovered??"—"}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:b(s.queryFinishedAt??s.updatedAt)})]},s.id))})]})})]})]})}export{ye as BacklinksPage};
1
+ import{r as n,j as e}from"./vendor-tanstack-Be8JdV5M.js";import{c as P,c1 as V,ah as U,c2 as Y,c3 as K,c4 as J,g as i,aM as E,T as y,i as h,B as w,j as M,aq as X,c5 as Z,c6 as ee,ak as se,c7 as te}from"./index-C1ABEgkr.js";import{C as ae,D as ne,T as re,a as le}from"./trash-2-6ZpABj9G.js";import"./vendor-radix-CQ77EO2y.js";import"./vendor-recharts-DSxTZhaH.js";import"./vendor-markdown-6dwjPOKK.js";const ce=[["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"}]],ie=P("circle-alert",ce);const de=[["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"}]],oe=P("external-link",de);function m({children:t,label:u="More info",placement:a="top",className:d}){const o=n.useId(),[g,c]=n.useState(!1);return e.jsxs("span",{className:`relative inline-flex ${d??""}`,children:[e.jsx("button",{type:"button","aria-label":u,"aria-describedby":g?o:void 0,className:"inline-flex h-4 w-4 items-center justify-center rounded-full text-muted hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500",onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),onFocus:()=>c(!0),onBlur:()=>c(!1),children:e.jsx(le,{className:"h-3.5 w-3.5","aria-hidden":!0})}),g&&e.jsx("span",{id:o,role:"tooltip",className:`absolute z-50 w-64 rounded border border-strong bg-bg-elevated px-3 py-2 text-xs font-normal leading-relaxed text-strong shadow-lg ${a==="top"?"bottom-full mb-2":"top-full mt-2"} left-1/2 -translate-x-1/2 whitespace-normal`,children:t})]})}const xe="https://commoncrawl.org/web-graphs";function D(t){return t==null?"—":t>=1e12?`${(t/1e12).toFixed(1)} TB`:t>=1e9?`${(t/1e9).toFixed(1)} GB`:t>=1e6?`${(t/1e6).toFixed(1)} MB`:t>=1e3?`${(t/1e3).toFixed(1)} KB`:`${t} B`}function b(t){if(!t)return"—";const u=Date.now()-new Date(t).getTime(),a=Math.floor(u/6e4);if(a<1)return"just now";if(a<60)return`${a}m ago`;const d=Math.floor(a/60);return d<24?`${d}h ago`:`${Math.floor(d/24)}d ago`}function C(t){switch(t){case"ready":return"positive";case"failed":return"negative";case"downloading":case"querying":case"queued":return"caution"}}function ye(){const[t,u]=n.useState(null),[a,d]=n.useState(null),[o,g]=n.useState([]),[c,F]=n.useState([]),[p,$]=n.useState(null),[S,B]=n.useState(!0),[R,q]=n.useState(!1),[j,I]=n.useState(!1),[N,k]=n.useState(""),[T,v]=n.useState(!1),[L,l]=n.useState(null),[A,x]=n.useState(null),f=n.useCallback(async()=>{B(!0),l(null);try{const[s,r,z,H,O]=await Promise.all([V(),U().catch(()=>null),Y().catch(()=>[]),K().catch(()=>[]),J().catch(()=>null)]);u(s),d(r),g(z),F(H),$(O)}catch(s){l(s instanceof Error?s.message:"Failed to load backlinks status")}finally{B(!1)}},[]);n.useEffect(()=>{f()},[f]);async function Q(){q(!0),l(null),x(null);try{const s=await Z();x(s.alreadyPresent?`DuckDB already installed (${s.version}).`:`Installed DuckDB ${s.version}.`),await f()}catch(s){l(s instanceof Error?s.message:"Failed to install DuckDB")}finally{q(!1)}}async function G(){const s=N.trim()||void 0;I(!0),l(null),x(null);try{const r=await ee(s);x(s?`Queued sync for ${r.release}. Download + query runs in the background.`:`Queued sync for auto-discovered release ${r.release}. Download + query runs in the background.`),k(""),v(!1),await f()}catch(r){r instanceof se&&r.code==="MISSING_DEPENDENCY"?l("DuckDB is not installed. Install it first."):l(r instanceof Error?r.message:"Failed to trigger sync")}finally{I(!1)}}async function _(s){l(null),x(null);try{await te(s),x(`Pruned cached release ${s}.`),await f()}catch(r){l(r instanceof Error?r.message:"Failed to prune release")}}const W=a?.status==="ready"&&c.every(s=>s.release!==a.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(i,{className:"surface-card p-4 mb-6 border-caution-800/60",children:e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(E,{className:"h-5 w-5 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-sm text-neutral leading-relaxed",children:[e.jsx("p",{className:"font-medium text-caution-200",children:"Heads up — a release sync is a large download."}),e.jsxs("ul",{className:"mt-1.5 space-y-1 text-secondary",children:[e.jsxs("li",{children:[e.jsx("span",{className:"text-strong",children:"~16 GB"})," of gzipped vertex + edge files per release, stored at"," ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/cache/commoncrawl/"}),"."]}),e.jsxs("li",{children:[e.jsx("span",{className:"text-strong",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(i,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-sm text-secondary 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-strong",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-secondary 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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"1"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong font-medium",children:"Download (one-time, ~16 GB)"})," — vertex + edge files cached to"," ",e.jsx("code",{className:"text-neutral",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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"2"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong 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-strong",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-strong bg-bg-elevated text-xs font-semibold text-neutral tabular-nums",children:"3"}),e.jsxs("span",{children:[e.jsx("span",{className:"text-strong 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."]})]})]})]})]}),L&&e.jsx(i,{className:"surface-card p-4 mb-4 border-negative-800/60",children:e.jsx("p",{className:"text-sm text-negative",children:L})}),A&&e.jsx(i,{className:"surface-card p-4 mb-4 border-positive-800/60",children:e.jsx("p",{className:"text-sm text-positive",children:A})}),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(m,{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-secondary",children:["It does ",e.jsx("span",{className:"text-strong",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-muted",children:["Installed on demand (not bundled) into ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/plugins/"})," so users who never run backlinks don’t pay the ~40 MB install cost."]})]})]})]}),t?.duckdbInstalled?e.jsx(y,{tone:"positive",children:"Installed"}):e.jsx(y,{tone:"caution",children:"Not installed"})]}),e.jsx(i,{className:"surface-card p-5",children:S?e.jsx("p",{className:"text-sm text-muted",children:"Checking…"}):t?.duckdbInstalled?e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(ae,{className:"h-5 w-5 text-positive-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-sm text-strong",children:["Version ",t.duckdbVersion??"unknown"," installed at"," ",e.jsx("code",{className:"text-neutral",children:t.pluginDir})]}),e.jsxs("p",{className:"text-xs text-muted mt-1",children:["Required spec: ",t.duckdbSpec]})]})]}):e.jsxs("div",{className:"flex items-start gap-3",children:[e.jsx(ie,{className:"h-5 w-5 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"flex-1",children:[e.jsx("p",{className:"text-sm text-strong",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-muted 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."}),t&&e.jsxs("p",{className:"text-xs text-muted mt-1",children:["Will be installed into ",e.jsx("code",{className:"text-neutral",children:t.pluginDir})," (~40 MB)."]}),!h()&&e.jsx("div",{className:"mt-3",children:e.jsxs(w,{type:"button",size:"sm",disabled:R,onClick:M(Q),children:[e.jsx(ne,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),R?"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(m,{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."})]})]}),a&&e.jsx(y,{tone:C(a.status),children:a.status})]}),e.jsxs(i,{className:"surface-card p-5",children:[e.jsxs("p",{className:"text-xs text-muted max-w-3xl mb-4",children:["A release is one Common Crawl dump (e.g. ",e.jsx("code",{className:"text-secondary",children:"cc-main-2026-jan-feb-mar"}),"). Syncing it downloads the graph and populates backlinks for every project in this workspace."]}),a?e.jsxs("div",{className:"space-y-2 text-sm",children:[e.jsxs("p",{className:"text-strong",children:["Release ",e.jsx("code",{className:"text-neutral",children:a.release})]}),a.phaseDetail&&e.jsx("p",{className:"text-muted",children:a.phaseDetail}),e.jsxs("div",{className:"grid grid-cols-2 md:grid-cols-4 gap-4 text-xs text-muted pt-2",children:[e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Projects"}),e.jsx("p",{className:"text-neutral mt-0.5",children:a.projectsProcessed??"—"})]}),e.jsxs("div",{children:[e.jsxs("p",{className:"text-faint uppercase tracking-wide flex items-center gap-1",children:["Rows",e.jsx(m,{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-neutral mt-0.5",children:a.domainsDiscovered??"—"})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Started"}),e.jsx("p",{className:"text-neutral mt-0.5",children:b(a.downloadStartedAt??a.createdAt)})]}),e.jsxs("div",{children:[e.jsx("p",{className:"text-faint uppercase tracking-wide",children:"Finished"}),e.jsx("p",{className:"text-neutral mt-0.5",children:b(a.queryFinishedAt)})]})]}),a.error&&e.jsx("p",{className:"text-sm text-negative-400 pt-2",children:a.error})]}):e.jsx("p",{className:"text-sm text-muted",children:"No release sync has run in this workspace yet."}),W&&e.jsx("div",{className:"mt-4 rounded border border-caution-800/60 bg-caution-950/20 p-3",children:e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsx(E,{className:"h-4 w-4 text-caution-400 shrink-0 mt-0.5","aria-hidden":!0}),e.jsxs("div",{className:"text-xs text-neutral leading-relaxed",children:[e.jsx("p",{className:"font-medium text-caution-200",children:"Cached files for this release are missing."}),e.jsxs("p",{className:"mt-1 text-secondary",children:["The sync record in the database says this release finished successfully, but the ~16 GB dump at"," ",e.jsxs("code",{className:"text-neutral",children:["~/.canonry/cache/commoncrawl/",a?.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-base bg-bg-elevated/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-muted",children:"Auto-detected release"}),p?e.jsxs("p",{className:"text-sm text-strong mt-0.5",children:[e.jsx("code",{className:"text-heading",children:p.release}),e.jsxs("span",{className:"ml-2 text-xs text-muted",children:["— vertex ",D(p.vertexBytes),", edges ",D(p.edgesBytes)]})]}):e.jsx("p",{className:"text-sm text-muted mt-0.5",children:S?"Probing Common Crawl…":"Could not auto-detect — pass an explicit release below."}),e.jsxs("a",{href:xe,target:"_blank",rel:"noopener noreferrer",className:"mt-1 inline-flex items-center gap-1 text-xs text-secondary hover:text-strong focus:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",children:["Browse all Common Crawl web-graph releases",e.jsx(oe,{className:"h-3 w-3","aria-hidden":!0})]})]}),!h()&&e.jsxs("div",{className:"flex items-center gap-2 shrink-0",children:[e.jsxs(w,{type:"button",size:"sm",disabled:j||!t?.duckdbInstalled||!p&&!N.trim(),onClick:M(G),children:[e.jsx(X,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),j?"Queuing…":"Run sync"]}),e.jsxs(m,{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-neutral",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-secondary",children:["First time for a release: ",e.jsx("span",{className:"text-strong",children:"~10–20 min download + ~5 min query"}),". Re-running the same release later: ",e.jsx("span",{className:"text-strong",children:"skips download, just re-queries"})," (~5 min)."]})]})]})]}),h()?null:T?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-strong bg-transparent px-2.5 py-1.5 text-sm text-strong placeholder-mono-600 focus:border-mono-500 focus:outline-none",placeholder:"cc-main-2026-jan-feb-mar",value:N,onChange:s=>k(s.target.value),disabled:j,autoFocus:!0}),e.jsx("button",{type:"button",className:"text-xs text-muted hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>{k(""),v(!1)},disabled:j,children:"Cancel"})]}):e.jsx("button",{type:"button",className:"text-xs text-muted hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>v(!0),disabled:j,children:"Use a different release →"})]}),!t?.duckdbInstalled&&e.jsx("p",{className:"text-xs text-faint 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(m,{label:"What is this?",children:[e.jsxs("span",{className:"block",children:["Raw Common Crawl dumps stored at"," ",e.jsx("code",{className:"text-neutral",children:"~/.canonry/cache/commoncrawl/<release>/"}),". Each release takes ~16 GB."]}),e.jsxs("span",{className:"mt-2 block text-secondary",children:["These files are needed to re-run per-project extracts against a release without re-downloading. Pruning here ",e.jsx("span",{className:"text-strong",children:"does not delete your backlink data"})," — that lives in SQLite."]})]})]})]})}),e.jsx("p",{className:"text-xs text-muted 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(i,{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-base text-left text-xs uppercase tracking-wide text-faint",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"}),!h()&&e.jsx("th",{className:"px-4 py-2 font-medium sr-only",children:"Actions"})]})}),e.jsxs("tbody",{children:[c.map(s=>e.jsxs("tr",{className:"border-b border-mono-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-strong",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:s.syncStatus?e.jsx(y,{tone:C(s.syncStatus),children:s.syncStatus}):e.jsx("span",{className:"text-faint",children:"—"})}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:D(s.bytes)}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:b(s.lastUsedAt)}),!h()&&e.jsx("td",{className:"px-4 py-2 text-right",children:e.jsxs("div",{className:"inline-flex items-center gap-1",children:[e.jsxs(w,{type:"button",variant:"outline",size:"sm",onClick:()=>{_(s.release)},children:[e.jsx(re,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),"Prune"]}),e.jsx(m,{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)),c.length===0&&e.jsx("tr",{children:e.jsx("td",{className:"px-4 py-4 text-sm text-muted",colSpan:h()?4:5,children:"No cached releases on this machine. If you ran a sync from a different machine (or deleted the cache), the backlink data is still in the database — but you’ll need to re-sync a release to run new extracts."})})]})]})})]}),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(i,{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-base text-left text-xs uppercase tracking-wide text-faint",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-mono-900 last:border-0",children:[e.jsx("td",{className:"px-4 py-2 text-strong",children:e.jsx("code",{children:s.release})}),e.jsx("td",{className:"px-4 py-2",children:e.jsx(y,{tone:C(s.status),children:s.status})}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:s.projectsProcessed??"—"}),e.jsx("td",{className:"px-4 py-2 text-right text-secondary tabular-nums",children:s.domainsDiscovered??"—"}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:b(s.queryFinishedAt??s.updatedAt)})]},s.id))})]})})]})]})}export{ye as BacklinksPage};
@@ -1 +1 @@
1
- import{c as o}from"./index-BiCDkOH4.js";const c=[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]],h=o("chevron-left",c),u={contentStyle:{backgroundColor:"var(--chart-tooltip-bg, #18181b)",border:"1px solid var(--chart-tooltip-border, #3f3f46)",borderRadius:8,fontSize:12},labelStyle:{color:"var(--chart-tooltip-label, #e4e4e7)"},itemStyle:{color:"var(--chart-tooltip-item, #a1a1aa)"}},l={fill:"var(--chart-neutral-text-dim, #71717a)",fontSize:11},f="var(--chart-grid, #27272a)",d="var(--chart-axis, #27272a)",e=["var(--chart-series-1, #34d399)","var(--chart-series-2, #60a5fa)","var(--chart-series-3, #f472b6)","var(--chart-series-4, #facc15)","var(--chart-series-5, #a78bfa)","var(--chart-series-6, #fb923c)","var(--chart-series-7, #22d3ee)","var(--chart-series-8, #f87171)"],i={gemini:"#60a5fa",openai:"#34d399",claude:"#fb923c",perplexity:"#22d3ee",local:"#a78bfa"};function v(a,t=0){return i[a]??e[t%e.length]}const b={text:"var(--chart-neutral-text, #a1a1aa)",textDim:"var(--chart-neutral-text-dim, #71717a)",textFaint:"var(--chart-neutral-text-faint, #52525b)",surface:"var(--chart-neutral-surface, #27272a)",trackSubtle:"var(--chart-neutral-track-subtle, rgb(255 255 255 / 0.04))",gridLine:"var(--chart-neutral-grid-line, rgb(255 255 255 / 0.06))"},m={positive:"var(--chart-tone-positive, #34d399)",positiveDeep:"var(--chart-tone-positive-deep, #10b981)",caution:"var(--chart-tone-caution, #fbbf24)",negative:"var(--chart-tone-negative, #fb7185)"};function S(a){return a}function n(a){const t=/^(\d{4})-(\d{2})-(\d{2})/.exec(a);return t?{year:Number(t[1]),month:Number(t[2]),day:Number(t[3])}:null}function p(a){const t=String(a),r=n(t);return r?new Date(Date.UTC(r.year,r.month-1,r.day)).toLocaleDateString(void 0,{timeZone:"UTC",month:"short",day:"numeric",year:"numeric"}):t}function g(a){const t=n(String(a));return t?`${t.month}/${t.day}`:String(a)}function T(a){return new Date(a).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}function C(a){const t=new Date(a);return`${t.getMonth()+1}/${t.getDate()}`}export{d as C,l as a,p as b,u as c,b as d,e,g as f,m as g,h,f as i,C as j,T as k,S as o,v as p};
1
+ import{c as o}from"./index-C1ABEgkr.js";const c=[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]],h=o("chevron-left",c),u={contentStyle:{backgroundColor:"var(--chart-tooltip-bg, #18181b)",border:"1px solid var(--chart-tooltip-border, #3f3f46)",borderRadius:8,fontSize:12},labelStyle:{color:"var(--chart-tooltip-label, #e4e4e7)"},itemStyle:{color:"var(--chart-tooltip-item, #a1a1aa)"}},l={fill:"var(--chart-neutral-text-dim, #71717a)",fontSize:11},f="var(--chart-grid, #27272a)",d="var(--chart-axis, #27272a)",e=["var(--chart-series-1, #34d399)","var(--chart-series-2, #60a5fa)","var(--chart-series-3, #f472b6)","var(--chart-series-4, #facc15)","var(--chart-series-5, #a78bfa)","var(--chart-series-6, #fb923c)","var(--chart-series-7, #22d3ee)","var(--chart-series-8, #f87171)"],i={gemini:"#60a5fa",openai:"#34d399",claude:"#fb923c",perplexity:"#22d3ee",local:"#a78bfa"};function v(a,t=0){return i[a]??e[t%e.length]}const b={text:"var(--chart-neutral-text, #a1a1aa)",textDim:"var(--chart-neutral-text-dim, #71717a)",textFaint:"var(--chart-neutral-text-faint, #52525b)",surface:"var(--chart-neutral-surface, #27272a)",trackSubtle:"var(--chart-neutral-track-subtle, rgb(255 255 255 / 0.04))",gridLine:"var(--chart-neutral-grid-line, rgb(255 255 255 / 0.06))"},m={positive:"var(--chart-tone-positive, #34d399)",positiveDeep:"var(--chart-tone-positive-deep, #10b981)",caution:"var(--chart-tone-caution, #fbbf24)",negative:"var(--chart-tone-negative, #fb7185)"};function S(a){return a}function n(a){const t=/^(\d{4})-(\d{2})-(\d{2})/.exec(a);return t?{year:Number(t[1]),month:Number(t[2]),day:Number(t[3])}:null}function p(a){const t=String(a),r=n(t);return r?new Date(Date.UTC(r.year,r.month-1,r.day)).toLocaleDateString(void 0,{timeZone:"UTC",month:"short",day:"numeric",year:"numeric"}):t}function g(a){const t=n(String(a));return t?`${t.month}/${t.day}`:String(a)}function T(a){return new Date(a).toLocaleDateString(void 0,{month:"short",day:"numeric",year:"numeric"})}function C(a){const t=new Date(a);return`${t.getMonth()+1}/${t.getDate()}`}export{d as C,l as a,p as b,u as c,b as d,e,g as f,m as g,h,f as i,C as j,T as k,S as o,v as p};
@@ -1 +1 @@
1
- import{j as e}from"./vendor-tanstack-Be8JdV5M.js";import{A as i}from"./AuditHistoryPanel-DwJpVWme.js";import"./index-BiCDkOH4.js";import"./vendor-radix-CQ77EO2y.js";import"./vendor-recharts-DSxTZhaH.js";import"./vendor-markdown-6dwjPOKK.js";function c(){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:"History"}),e.jsx("p",{className:"page-subtitle",children:"Instance-wide configuration changes, including retained evidence for deleted projects."})]})}),e.jsx(i,{})]})}export{c as HistoryPage};
1
+ import{j as e}from"./vendor-tanstack-Be8JdV5M.js";import{A as i}from"./AuditHistoryPanel-wJqs99Ak.js";import"./index-C1ABEgkr.js";import"./vendor-radix-CQ77EO2y.js";import"./vendor-recharts-DSxTZhaH.js";import"./vendor-markdown-6dwjPOKK.js";function c(){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:"History"}),e.jsx("p",{className:"page-subtitle",children:"Instance-wide configuration changes, including retained evidence for deleted projects."})]})}),e.jsx(i,{})]})}export{c as HistoryPage};