@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.
- package/README.md +15 -10
- package/assets/agent-workspace/skills/aero/SKILL.md +15 -7
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -1
- package/assets/agent-workspace/skills/aero/references/orchestration.md +5 -3
- package/assets/agent-workspace/skills/aero/soul.md +1 -1
- package/assets/agent-workspace/skills/canonry/SKILL.md +50 -13
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -1
- package/assets/agent-workspace/skills/canonry/references/indexing.md +4 -2
- package/assets/assets/{AuditHistoryPanel-DwJpVWme.js → AuditHistoryPanel-wJqs99Ak.js} +1 -1
- package/assets/assets/{BacklinksPage-K8FQUBxI.js → BacklinksPage-BBDExYo_.js} +1 -1
- package/assets/assets/{ChartPrimitives-6oFKrJFk.js → ChartPrimitives-DSr5ldMf.js} +1 -1
- package/assets/assets/{HistoryPage-DwOAoN1Z.js → HistoryPage-Ca-GGnTI.js} +1 -1
- package/assets/assets/{ProjectPage-CdgFUk7h.js → ProjectPage-BuaSDSwf.js} +1 -1
- package/assets/assets/{RunRow-lt9IS99U.js → RunRow-O9TbOdQY.js} +1 -1
- package/assets/assets/{RunsPage-DitYcIdx.js → RunsPage-C1Sqd7AN.js} +1 -1
- package/assets/assets/{SettingsPage-CAOrh65Q.js → SettingsPage-DPFh62BV.js} +1 -1
- package/assets/assets/{TrafficPage-BqzNvx75.js → TrafficPage-5vKoQyoP.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-D17oADs8.js +1 -0
- package/assets/assets/{arrow-left-AmG4gtUR.js → arrow-left-De5P5AIE.js} +1 -1
- package/assets/assets/extract-error-message-BqMd-fQ6.js +1 -0
- package/assets/assets/{index-BiCDkOH4.js → index-C1ABEgkr.js} +53 -53
- package/assets/assets/{trash-2-gFHWd1A1.js → trash-2-6ZpABj9G.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-ZKUSY4PI.js → chunk-GYAKRVE2.js} +25 -5
- package/dist/{chunk-BLSHHRRU.js → chunk-K4FDDCYG.js} +29 -0
- package/dist/{chunk-NCFH26X5.js → chunk-LPEE5AYY.js} +198 -25
- package/dist/{chunk-IHRW6WWV.js → chunk-WWZE7VDQ.js} +10 -6
- package/dist/cli.js +226 -38
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-SH5Z6JFT.js → intelligence-service-2QBAGT5X.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
- package/assets/assets/TrafficSourceDetailPage-DXbygZUA.js +0 -1
- 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 [
|
|
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
|
|
58
|
+
## Or use any shell-capable coding agent
|
|
58
59
|
|
|
59
|
-
|
|
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.
|
|
66
|
-
3.
|
|
67
|
-
4. Read
|
|
68
|
-
5.
|
|
69
|
-
6.
|
|
70
|
-
7.
|
|
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
|
-
| **
|
|
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
|
-
|
|
6
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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?**
|
|
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
|
|
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.
|
|
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: "
|
|
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** —
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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};
|