@canonry/canonry 7.32.0 → 7.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/agent-operations.md +9 -0
- package/assets/agent-workspace/skills/aero/references/site-health.md +12 -0
- package/assets/agent-workspace/skills/canonry/SKILL.md +9 -0
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +24 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +207 -4
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +14 -2
- package/assets/assets/{AuditHistoryPanel-cQyVA85P.js → AuditHistoryPanel-CvANw3ds.js} +1 -1
- package/assets/assets/{BacklinksPage-UGZ_9N7F.js → BacklinksPage-C6x9ELjk.js} +1 -1
- package/assets/assets/{HistoryPage-UJVFZ5LK.js → HistoryPage-r36HX1cC.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-ChpOxQfx.js → MeasurementPropertyPage-ComoCzR4.js} +3 -3
- package/assets/assets/ProjectPage-CIeU-buk.js +8 -0
- package/assets/assets/RunsPage-DI6eJATZ.js +1 -0
- package/assets/assets/{SettingsPage-DeKxjuTr.js → SettingsPage-Bw7GdXxs.js} +1 -1
- package/assets/assets/{SiteHealthSection-COeVsgop.js → SiteHealthSection-C-JaNKpS.js} +3 -3
- package/assets/assets/{TrafficPage-DmMmeJKK.js → TrafficPage-Cnsp276v.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CUehseZe.js → TrafficSourceDetailPage-CBIaJk7e.js} +1 -1
- package/assets/assets/{extract-error-message-BsxdS7Po.js → extract-error-message-C_Owa0n9.js} +1 -1
- package/assets/assets/index-ZS9sSTMS.js +86 -0
- package/assets/assets/{react-sigma_core.esm.min--MbbBWzJ.js → react-sigma_core.esm.min-ugyh-ooq.js} +1 -1
- package/assets/assets/{v2-overview-adapter-D-IScpdp.js → v2-overview-adapter-BLSrVQ7K.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/chunk-3JVF3ULD.js +550 -0
- package/dist/chunk-45Z5RJZW.js +562 -0
- package/dist/{chunk-QFXD7GR5.js → chunk-5LY7EG4U.js} +238 -20
- package/dist/chunk-EJRV7RBZ.js +23066 -0
- package/dist/{chunk-WRHA54BN.js → chunk-G47XJVAS.js} +5885 -3900
- package/dist/{chunk-42XUY243.js → chunk-GZ2KIONF.js} +11507 -10391
- package/dist/chunk-HGMYEDAQ.js +79 -0
- package/dist/{chunk-2C6NWFN4.js → chunk-JVBI6VBT.js} +2 -2
- package/dist/{chunk-ZIETXJYU.js → chunk-MPWBFKGK.js} +201 -104
- package/dist/{chunk-K5EGSSXC.js → chunk-QQYQDTFZ.js} +142 -47
- package/dist/{chunk-FW4GX2LO.js → chunk-SW54HXSZ.js} +3 -3
- package/dist/cli-catalog-PNJEQV5H.js +15 -0
- package/dist/cli-commands-4UT2CRXX.js +15 -0
- package/dist/cli.js +136 -22525
- package/dist/{demo-server-G5OK7SWE.js → demo-server-WRHR25HF.js} +5 -4
- package/dist/index.js +7 -6
- package/dist/{intelligence-service-3ZUVTIO7.js → intelligence-service-QXXRUB2G.js} +2 -2
- package/dist/mcp.js +30 -15
- package/package.json +10 -10
- package/assets/assets/ProjectPage-CvxuPSiC.js +0 -8
- package/assets/assets/RunsPage-C6U_YcDY.js +0 -1
- package/assets/assets/index-ahEr6Oc-.js +0 -86
|
@@ -87,6 +87,15 @@ Ports and database settings still need a restart.
|
|
|
87
87
|
changed separately from why it might have changed. A hypothesis is not a
|
|
88
88
|
measured cause. Propose bounded verification if stored evidence is insufficient.
|
|
89
89
|
|
|
90
|
+
**Site Health:** read `canonry_site_health_issues` for stored technical findings,
|
|
91
|
+
affected-page counts and example URLs, then `canonry_site_health_issue_pages`
|
|
92
|
+
for a selected issue's remaining pages. Pin subsequent reads to the returned
|
|
93
|
+
run. Inspect coverage and omissions: a passing score can still have findings,
|
|
94
|
+
an older scan may lack cross-page evidence, and a partial scan cannot establish
|
|
95
|
+
site-wide absence. Advisory findings require judgment before proposing a repair.
|
|
96
|
+
Audit evidence informs technical repairs and existing-page recommendations;
|
|
97
|
+
it does not itself establish a new-blog opportunity or authorize any edit.
|
|
98
|
+
|
|
90
99
|
**Prospecting:** generate a one-shot company snapshot without creating a project.
|
|
91
100
|
Inspect stored provider settings first. Agree on the company, domain, selected
|
|
92
101
|
providers, and queries before starting the quota-spending snapshot action.
|
|
@@ -30,6 +30,8 @@ receive the selected graph node, so resolve the requested page or ask for it.
|
|
|
30
30
|
|
|
31
31
|
| Question | Read |
|
|
32
32
|
|---|---|
|
|
33
|
+
| Which technical issues need investigation, including on passing pages? | `canonry_site_health_issues` for grouped findings, counts, example URLs and coverage |
|
|
34
|
+
| Which pages have a selected issue? | `canonry_site_health_issue_pages`; pin the list's run and follow the affected-page cursor |
|
|
33
35
|
| Which URLs were discovered and audited? | `canonry_technical_aeo_crawl_pages`; filter audit/fetch/indexability state and follow its cursor |
|
|
34
36
|
| How is the site organized? | `canonry_technical_aeo_structure` for one path level |
|
|
35
37
|
| What links to/from this page? | `canonry_technical_aeo_link_neighbors`; inbound and outbound truncation are independent |
|
|
@@ -46,6 +48,12 @@ are 25 nodes and 50 edges; a bounded neighborhood is not the whole site.
|
|
|
46
48
|
|
|
47
49
|
## Interpret states before findings
|
|
48
50
|
|
|
51
|
+
- Issue reads preserve separate page-finding and cross-page coverage. Older
|
|
52
|
+
scans may expose page findings while cross-page evidence is unavailable.
|
|
53
|
+
Unavailable is not zero; incomplete coverage cannot establish site-wide
|
|
54
|
+
absence. A bounded issue list and its example URLs are samples until their
|
|
55
|
+
corresponding cursors are exhausted. Advisory findings, including short
|
|
56
|
+
titles and duplicate metadata, need context before selecting a repair.
|
|
49
57
|
- `hasData: false`, no crawl, details unavailable, page not found, and page
|
|
50
58
|
not audited are different states. None means a zero score or a passing
|
|
51
59
|
page. `scores-only` permits discussing scores but not inventing findings.
|
|
@@ -69,6 +77,10 @@ are 25 nodes and 50 edges; a bounded neighborhood is not the whole site.
|
|
|
69
77
|
|
|
70
78
|
## Compare scans and prioritize
|
|
71
79
|
|
|
80
|
+
The page/link changes tool does not compare audit finding codes or duplicate
|
|
81
|
+
metadata groups. Do not interpret absence from its output as an issue being
|
|
82
|
+
fixed. Inspect each scan's issue evidence and compatible coverage separately.
|
|
83
|
+
|
|
72
84
|
Use the changes tool's resolved run IDs and filters; keep them fixed when
|
|
73
85
|
paging. Its first page carries the exact summary; continuation pages omit
|
|
74
86
|
summary/total. Retain that first summary and do not treat its absence on later
|
|
@@ -78,6 +78,15 @@ Ports and database settings still need a restart.
|
|
|
78
78
|
changed separately from why it might have changed. A hypothesis is not a
|
|
79
79
|
measured cause. Propose bounded verification if stored evidence is insufficient.
|
|
80
80
|
|
|
81
|
+
**Site Health:** read `canonry_site_health_issues` for stored technical findings,
|
|
82
|
+
affected-page counts and example URLs, then `canonry_site_health_issue_pages`
|
|
83
|
+
for a selected issue's remaining pages. Pin subsequent reads to the returned
|
|
84
|
+
run. Inspect coverage and omissions: a passing score can still have findings,
|
|
85
|
+
an older scan may lack cross-page evidence, and a partial scan cannot establish
|
|
86
|
+
site-wide absence. Advisory findings require judgment before proposing a repair.
|
|
87
|
+
Audit evidence informs technical repairs and existing-page recommendations;
|
|
88
|
+
it does not itself establish a new-blog opportunity or authorize any edit.
|
|
89
|
+
|
|
81
90
|
**Prospecting:** generate a one-shot company snapshot without creating a project.
|
|
82
91
|
Inspect stored provider settings first. Agree on the company, domain, selected
|
|
83
92
|
providers, and queries before starting the quota-spending snapshot action.
|
|
@@ -102,6 +102,23 @@ Before anything else, separate the two signals. `cnry analytics <project> --feat
|
|
|
102
102
|
|
|
103
103
|
> **Known gap:** `cnry health` is citation-only today (no mention dimension). For the mention-first read, use `cnry overview` / `cnry get <project> scores.mentionCoverage.value` / `cnry get <project> scores.mentionShare.value` until health is extended.
|
|
104
104
|
|
|
105
|
+
### Stored competitor suggestions
|
|
106
|
+
|
|
107
|
+
`competitor suggest` and the competitor suggestions API read stored answer and
|
|
108
|
+
citation evidence only. They do not run a provider sweep. Treat `namingAnswers`
|
|
109
|
+
and `citingAnswers` as separate answer counts, and use `pairedAnswers`
|
|
110
|
+
only when the name and source occur in the same answer. Probe and non-terminal
|
|
111
|
+
runs are reported as excluded evidence and do not enter candidate counts. An
|
|
112
|
+
exact suggestion read refuses with `VALIDATION_ERROR` above 10,000 eligible
|
|
113
|
+
answer rows instead of sampling; narrow the window or select one Advanced
|
|
114
|
+
market with `groupKey`.
|
|
115
|
+
|
|
116
|
+
Suggestions preserve the selected query class and Advanced market scope. Keep
|
|
117
|
+
branded and non-brand results separate; never pool their shares into one
|
|
118
|
+
percentage. Review `unresolvedNames` and answer samples before adding an alias
|
|
119
|
+
because a recommended name can be a partner, place, service, or unrelated
|
|
120
|
+
entity.
|
|
121
|
+
|
|
105
122
|
### Step 1: Check indexing
|
|
106
123
|
Not mentioned / not cited ≠ bad content. Often the page isn't indexed yet, which starves both signals.
|
|
107
124
|
```bash
|
|
@@ -223,3 +240,10 @@ coverage, frozen scope, and per-query source evidence before interpreting a scor
|
|
|
223
240
|
The feature is default-off, has no theme setup, and has not passed independent
|
|
224
241
|
human evaluation for either query class. Native Aero reads these scores but
|
|
225
242
|
cannot enable sentiment or submit a backfill; client reports do not consume them.
|
|
243
|
+
|
|
244
|
+
Competitor impact previews use the latest stored landscape and its exact run
|
|
245
|
+
IDs, query population and tracked basis. Compare project and competitor mention
|
|
246
|
+
counts before comparing percentages. A competitor change restates every stored
|
|
247
|
+
period; an apply-mode add may move again after alias detection. Use an explicit
|
|
248
|
+
preview before a separate curated write, and do not substitute its result for
|
|
249
|
+
the overview card's separate instrument.
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
The CLI is invoked as `cnry` (short form) or `canonry` — both ship with the `@canonry/canonry` npm package and behave identically. This reference uses `cnry`.
|
|
4
4
|
|
|
5
|
+
## Find a command
|
|
6
|
+
|
|
7
|
+
`cnry help` lists every command group in this binary. Use `cnry help --format json`
|
|
8
|
+
for the complete catalog, or `cnry help ga --format jsonl` for one command per line
|
|
9
|
+
in a group. `cnry --help --format json` and `cnry ga --help --format json` provide
|
|
10
|
+
the same discovery output. The catalog includes access, flags, dry-run support and
|
|
11
|
+
supported rolling windows. Contextual help prints `Access: stored`, `Access: live`
|
|
12
|
+
or `Access: write`; live means the command calls a provider now, while write means
|
|
13
|
+
it changes server or local state. `--trace` reports the response access header for
|
|
14
|
+
API calls. Help is local and does not contact the server.
|
|
15
|
+
|
|
5
16
|
## Server Management
|
|
6
17
|
|
|
7
18
|
```bash
|
|
@@ -42,6 +53,34 @@ install's server, or restart it, to apply the saved providers. Any other
|
|
|
42
53
|
rejected reload reports `failed` and exits nonzero; the saved config remains
|
|
43
54
|
available. Bootstrap never reloads a remote server automatically.
|
|
44
55
|
|
|
56
|
+
### Stored-only mode
|
|
57
|
+
|
|
58
|
+
Use `cnry --stored-only <command>` or set `CANONRY_STORED_ONLY=1` when the
|
|
59
|
+
workflow may read saved evidence but must not perform provider work or change
|
|
60
|
+
Canonry operational, configuration, or business state. The guard runs before
|
|
61
|
+
telemetry, anonymous first-run setup, skills synchronization, update checks,
|
|
62
|
+
configuration writes, or API access. A refused command exits 1 with
|
|
63
|
+
`STORED_ONLY_REFUSED`; `doctor` remains a live diagnostic and is refused unless
|
|
64
|
+
run outside stored-only mode. The local client refuses live and write operations
|
|
65
|
+
before any network call on older servers; newer servers also honor
|
|
66
|
+
`x-canonry-stored-only: 1` before their handlers run.
|
|
67
|
+
|
|
68
|
+
Access follows the invocation: competitor change previews, alias reads, and
|
|
69
|
+
alias detection without `--apply` work in stored-only mode. The corresponding
|
|
70
|
+
writes remain refused. `get --from doctor` is a live diagnostic; other stored
|
|
71
|
+
evidence sources and the settings read are allowed. Use `cnry help <group>
|
|
72
|
+
--access stored` or `<group> --help --access stored` to filter help. A known
|
|
73
|
+
group with no matching commands returns an empty result; `--access` is only a
|
|
74
|
+
help/catalog option.
|
|
75
|
+
|
|
76
|
+
Results exports are allowed because they read stored evidence; writing the
|
|
77
|
+
requested export artifact is not a Canonry state mutation. Ordinary server
|
|
78
|
+
request and authentication accounting remains separate from caller business
|
|
79
|
+
mutations. The MCP adapter has the equivalent `--stored-only`,
|
|
80
|
+
`--scope=stored-only`, `CANONRY_MCP_SCOPE=stored-only`, and
|
|
81
|
+
`CANONRY_STORED_ONLY=1` forms, and a later read-only or all-scope option cannot
|
|
82
|
+
widen a stored-only request.
|
|
83
|
+
|
|
45
84
|
`cnry dashboard reset-password` clears a forgotten dashboard password. It edits
|
|
46
85
|
`config.yaml` under `$CANONRY_CONFIG_DIR` on this machine and never calls the
|
|
47
86
|
server, so run it where the server's config lives. A missing file is a
|
|
@@ -84,6 +123,10 @@ cnry project list # list all projects
|
|
|
84
123
|
cnry project create <name> --domain <url> --country US --language en
|
|
85
124
|
cnry project show <name> # project detail
|
|
86
125
|
cnry project update <name> # update project settings
|
|
126
|
+
cnry project update <name> --add-lead-event book_demo --remove-lead-event old_event
|
|
127
|
+
cnry project update <name> --brand-term "Sample Brand" --add-brand-term "Sample Pro"
|
|
128
|
+
cnry project update <name> --marketing-host offers.example --remove-marketing-host old.example
|
|
129
|
+
cnry project update <name> --display-name "Sample Brand" --expect-revision 4
|
|
87
130
|
cnry project update <name> --dispatch-mode claude=batch # scheduled sweeps batch this provider (repeatable; see Batch mode below)
|
|
88
131
|
cnry project update <name> --clear-dispatch-mode claude # back to sync
|
|
89
132
|
cnry project update <name> --negative-review-max-stars 2 # review.negative for 1-2 star Google reviews only (1-4, or "default" for 3)
|
|
@@ -93,9 +136,23 @@ cnry project update <name> --add-qualified-alias "Acme NYC" # sentiment tre
|
|
|
93
136
|
cnry project update <name> --remove-qualified-alias "Acme NYC" # repeatable; create takes --qualified-alias
|
|
94
137
|
cnry project delete <name> # delete a project
|
|
95
138
|
cnry project delete <name> --dry-run # preview cascade impact (GET /delete-preview) without writing
|
|
96
|
-
cnry status <project> #
|
|
139
|
+
cnry status <project> # stored run, integration and health status for one project
|
|
140
|
+
cnry status --all [--problems] # stored status for every project and connected integration
|
|
97
141
|
```
|
|
98
142
|
|
|
143
|
+
Project updates use PATCH and preserve omitted settings and config source. Measurement
|
|
144
|
+
flags are repeatable: `--lead-event`, `--brand-term`, and `--marketing-host` replace
|
|
145
|
+
only their named list; `--add-*` and `--remove-*` adjust it. Lead event matching is
|
|
146
|
+
case-sensitive; brand-term and host removals ignore case. Values use the shared
|
|
147
|
+
measurement validation. `project show` includes these lists in human output.
|
|
148
|
+
|
|
149
|
+
The CLI sends the revision it read, or validates an explicit positive
|
|
150
|
+
`--expect-revision`. A stale revision returns `PROJECT_REVISION_CONFLICT` without
|
|
151
|
+
writing. A server whose PATCH route is unavailable uses the legacy full PUT after
|
|
152
|
+
a second read confirms the revision is unchanged. That old-server fallback cannot
|
|
153
|
+
provide atomic compare-and-swap protection; upgrade the server for that guarantee.
|
|
154
|
+
The native MCP tool `canonry_project_update` accepts the same partial request.
|
|
155
|
+
|
|
99
156
|
### Brand aliases
|
|
100
157
|
|
|
101
158
|
`spec.aliases: string[]` on the project (set via `cnry apply`, `cnry project update <name> --add-alias <name>`, or the dashboard) widens the mention detector. Use it when the answer text says "Meta" but the canonical brand is "Facebook", or for a product or former name ("AcmeCloud Pro"). Matching ignores case, spacing and punctuation, so "AcmeCloud" already matches "Acme Cloud"; it is the same answer-text scan that powers `answerMentioned`.
|
|
@@ -154,10 +211,20 @@ cnry run cancel <project> [run-id] # force-cancel stuck runs
|
|
|
154
211
|
cnry run completeness <run-id> # answered vs missing answers per provider, and whether a fill is allowed
|
|
155
212
|
cnry run fill <run-id> --dry-run # what a fill would record, without spending quota
|
|
156
213
|
cnry run fill <run-id> [--provider claude] --wait # record a partial run's missing answers under the SAME run id
|
|
157
|
-
cnry runs <project> --limit 10 # list recent runs
|
|
214
|
+
cnry runs <project> --limit 10 # list recent runs, including probes
|
|
215
|
+
cnry run list <project> --kind traffic-sync --status failed --since 2026-10-01T00:00:00Z --limit 20 --format json
|
|
158
216
|
cnry run show <id> # show run details
|
|
159
217
|
```
|
|
160
218
|
|
|
219
|
+
`run list <project>` and `runs <project>` read the same stored history. Filters
|
|
220
|
+
include `--kind`, `--status`, `--since`, `--exclude-kind`, and repeatable or
|
|
221
|
+
comma-separated `--exclude-kinds`. The inclusive `--since` timestamp accepts
|
|
222
|
+
UTC or an offset. Omit it for all dates; omit `--limit` for all matching runs.
|
|
223
|
+
The newest limited rows retain chronological order. Traffic sync rows include
|
|
224
|
+
`sourceId` when stored, and human output shows failed and partial errors.
|
|
225
|
+
JSON is the API array; JSONL emits each run with its project. With no second
|
|
226
|
+
positional, `run list` retains the sweep shorthand for a project named `list`.
|
|
227
|
+
|
|
161
228
|
Run statuses: `queued` → `running` → `completed` / `failed` / `partial`
|
|
162
229
|
|
|
163
230
|
`partial` = some providers failed (usually rate limits) — successful snapshots are still saved.
|
|
@@ -251,7 +318,10 @@ Two independent signals per (query × provider): **mention** (`answerMentioned`
|
|
|
251
318
|
cnry evidence <project> # per-query [C/c][M/m] cell + Mentioned: X / Y / Cited: X / Y
|
|
252
319
|
cnry evidence <project> --format json # JSON output
|
|
253
320
|
cnry history <project> # audit trail
|
|
321
|
+
cnry history <project> --entity-type competitor --format json
|
|
322
|
+
cnry history <project> --action-prefix competitors. --exclude-entity-type traffic_source --since 2026-10-01T00:00:00Z --until 2026-10-02T00:00:00Z
|
|
254
323
|
cnry export <project> --include-results # export as YAML
|
|
324
|
+
cnry export <project> --include-secrets # include webhook URLs; requires administrator authority or a wildcard key
|
|
255
325
|
cnry backfill answer-mentions # recompute answerMentioned (primary) from stored answers (honors aliases)
|
|
256
326
|
cnry backfill answer-mentions --dry-run
|
|
257
327
|
cnry backfill answer-visibility # recompute citationState (secondary) from stored answers
|
|
@@ -260,6 +330,19 @@ cnry backfill insights <project> # recompute insights for complete
|
|
|
260
330
|
cnry backfill insights <project> --since 2026-04-01 --dry-run
|
|
261
331
|
```
|
|
262
332
|
|
|
333
|
+
History filters compose on project reads and `history --all`. `--action` remains
|
|
334
|
+
exact; `--action-prefix` matches a literal prefix. Repeat `--exclude-entity-type`
|
|
335
|
+
or supply a comma list to remove noisy entity types before paging. `--entity-id`
|
|
336
|
+
selects one entity. Time bounds are inclusive, normalize offsets to UTC, and
|
|
337
|
+
reject invalid values. `--actor api` includes legacy API, API-key, and user
|
|
338
|
+
actors; other actors match exactly. Empty filters are rejected. Human output shows actor and client origin; instance-wide rows include the project name. Select either a project or `--all`.
|
|
339
|
+
New competitor replacement records include `added`, `removed`, and any discarded
|
|
340
|
+
aliases beside the resulting `competitors` list. Older records remain unchanged.
|
|
341
|
+
|
|
342
|
+
Project exports redact notification webhook URLs by default as `redacted://<host>/<notification-id>`.
|
|
343
|
+
Applying that export preserves the existing notification ID, signing secret, source, enabled state and creation time. The HTTPS `/redacted` placeholder from notification listings also preserves the matched stored destination. A real URL change appears in preview and audit as a URL-change marker without exposing its path or secret.
|
|
344
|
+
Use `--include-secrets` only when the credential has administrator authority. The CLI prints a redaction note to stderr when URLs remain redacted.
|
|
345
|
+
|
|
263
346
|
Output uses a two-glyph cell per (query × provider): `[C/c][M/m]` — uppercase = present, lowercase = absent, `–` = no snapshot. **C/c = cited** (secondary), **M/m = mentioned** (primary). Always print the legend before the table; never collapse the two signals into one cell.
|
|
264
347
|
|
|
265
348
|
```
|
|
@@ -365,6 +448,8 @@ cnry technical-aeo run <project> --check-dead-links --wait # opt in to dead-li
|
|
|
365
448
|
cnry technical-aeo progress <project> --run-id <id> [--format json] # exact durable phase and pages found / checked / failed counters; never a synthesized percentage
|
|
366
449
|
cnry technical-aeo crawl <project> [--run-id <id>] [--format json] # crawl metadata, budgets, completeness, and termination
|
|
367
450
|
cnry site-health overview <project> [--run-id <id>] [--format json] # operator-facing alias for crawl metadata
|
|
451
|
+
cnry site-health issues <project> [--run-id <id>] [--cursor <cursor>] [--limit <n>] [--format json|jsonl] # stored findings including passing-score issues, affected-page counts and example URLs
|
|
452
|
+
cnry site-health issue-pages <project> --issue-id <id> [--run-id <id>] [--cursor <cursor>] [--limit <n>] [--format json|jsonl] # affected URLs for one issue; pin the run from the issue list
|
|
368
453
|
cnry site-health page-audit <project> (--node-key <key>|--url <url>) [--run-id <id>] [--format json] # exact audit score, factor findings, recommendations, and crawl provenance for one graph page
|
|
369
454
|
cnry site-health subgraph <project> [--node-key <key>|--url <url>] [--hops <n>] [--max-nodes <n>] [--max-edges <n>] [--format json] # focused semantic graph (MCP defaults to 25 nodes / 50 edges)
|
|
370
455
|
cnry site-health path <project> (--to-node-key <key>|--to-url <url>) [--from-node-key <key>|--from-url <url>] [--max-depth <n>] [--format json] # directed shortest followable-link path
|
|
@@ -385,6 +470,7 @@ cnry schedule set <project> --kind site-audit --preset weekly # keep it fresh
|
|
|
385
470
|
For agent site readiness, begin with `cnry technical-aeo score <project> --format json`. Use `cnry site-health overview <project>` only to add crawl metadata; it never replaces the score. Use `cnry site-health page-audit` (MCP: `canonry_site_health_page_audit`) to tie a selected graph page's audit score to exact findings and fixes. Link score is importance, not an audit verdict. Then request a focused neighborhood, a shortest path, or scan-to-scan changes. Do not ask an agent to materialize the interactive graph: it can exceed the MCP tool-result limit. The matching traversal tools are `canonry_site_health_subgraph`, `canonry_site_health_path`, and `canonry_site_health_changes`; the subgraph tool defaults to a small focused result and should be expanded only when needed.
|
|
386
471
|
|
|
387
472
|
- The score is only available after at least one audit runs — `score` returns `hasData: false` until then.
|
|
473
|
+
- `issues` and `issue-pages` read stored evidence only; `technical-aeo` aliases expose the same commands. JSONL begins with provenance, coverage, total and continuation metadata, including for an empty result. An unavailable cross-page source in an older scan is not a measured zero, and a partial scan cannot establish site-wide absence. Follow the cursor before claiming a complete list. Issue dispositions guide review; advisory metadata findings do not automatically justify an edit or a blog post.
|
|
388
474
|
- Reads without `--run-id` use the newest scan that published a crawl, including one that stopped at its page budget (`complete: false`, `termination: max-pages`). A failed, cancelled, or still-running scan never replaces it. `changes` compares complete crawls only.
|
|
389
475
|
- Graph reads are server-paged and bounded. Use `--run-id` to inspect a specific retained run.
|
|
390
476
|
- A subgraph with `countAccuracy: "lower-bound"` hit a traversal cap: totals and omissions are minimums, not site-wide counts. Its `complete` and `termination` fields apply to every observation.
|
|
@@ -449,19 +535,25 @@ cnry query import <project> queries.txt
|
|
|
449
535
|
cnry query generate <project> --provider gemini --count 10 --save
|
|
450
536
|
|
|
451
537
|
cnry competitor add <project> competitor1.com competitor2.com
|
|
538
|
+
cnry competitor add <project> rival.example --dry-run --format json # preview latest stored-evidence impact without writing
|
|
452
539
|
cnry competitor add <project> spoketuneworks.example --alias TuneSpoke # one domain; repeat --alias for more names
|
|
540
|
+
cnry competitor add <project> competitor1.com competitor2.com --acknowledge-bulk-add # operator acknowledgement for measured growth over 5 domains in 24 hours
|
|
453
541
|
cnry competitor list <project> # text shows each competitor's aliases and auto names; JSON rows carry `aliases`, `autoAliases`, `blockedAliases`
|
|
454
542
|
cnry competitor aliases <project> qvx.example # read one competitor's curated, auto-detected (with evidence) and blocked names
|
|
455
543
|
cnry competitor aliases <project> qvx.example --set QVX # exact list (repeat --set); --clear empties it
|
|
544
|
+
cnry competitor aliases <project> qvx.example --set QVX --dry-run --format json # preview an alias identity change
|
|
456
545
|
cnry competitor aliases <project> qvx.example --add "QVX Stores" # appends server-side in one call
|
|
457
546
|
cnry competitor aliases <project> qvx.example --remove qvx # read, edit, write back (a concurrent edit can be overwritten)
|
|
458
547
|
cnry competitor aliases <project> spoketuneworks.example --block "Spoke Crew" # never auto-apply this name (removes it if auto)
|
|
459
548
|
cnry competitor aliases <project> spoketuneworks.example --unblock "Spoke Crew" # release it to detection again
|
|
460
|
-
cnry competitor aliases detect <project> --format json # dry run: names the stored answers tie to each site, with evidence
|
|
549
|
+
cnry competitor aliases detect <project> --format json # dry run: names the stored answers tie to each site, with evidence and impact
|
|
461
550
|
cnry competitor aliases detect <project> --apply # run detection now and store it, in any mode (after each sweep the project's mode decides)
|
|
462
551
|
cnry competitor landscape <project> --query-class non-brand --window 30d --format json
|
|
552
|
+
cnry competitor suggest <project> --window 30d --min-answers 3 --query-class non-brand --format json
|
|
463
553
|
```
|
|
464
554
|
|
|
555
|
+
`competitor suggest` reads stored answer and citation evidence and ranks possible competitors. It never starts a sweep or writes data. Use `--format jsonl` for one candidate per line. The response keeps naming counts separate from citation counts and includes the query class, answer floor, run IDs, sample URLs, exclusions, and count units. An exact suggestion read refuses with `VALIDATION_ERROR` above 10,000 eligible answer rows instead of sampling; narrow `--window` or use `--group-key` for one Advanced market. A Simple project without a usable brand alias uses the pooled `all` class unless an explicit class filter is requested. Exact reads also refuse more than 200,000 characters in one answer, 20 million evidence characters, or 200,000 source references with narrowing guidance. Scans yield so other requests remain responsive. Unknown untracked candidates with only unpaired prose matches are excluded as `unpaired-name`; mention counts stay unchanged for comparison with a bare-add preview. Human output shows both-answer and paired-answer counts and exclusion reasons.
|
|
556
|
+
|
|
465
557
|
### Query class
|
|
466
558
|
|
|
467
559
|
Every tracked query is `branded` (it names the project) or `non-brand`, and the two are never counted together. The server decides the class; read it, never work it out from the query text.
|
|
@@ -481,6 +573,7 @@ A competitor is stored as its registrable domain, and every competitor mention m
|
|
|
481
573
|
|
|
482
574
|
- Rules: trimmed, deduplicated case-insensitively (first spelling kept), at most 10 per competitor, 80 characters each, at least 3 letters or digits. Rejected with a 400 naming each alias and the name it overlaps: a brand key (letters and digits, case and punctuation folded) that contains, or sits inside, one of the project's own names or written hosts ("Acme", "Cycles", `acmecycles.example`, `shop.acmecycles.example` or "Acme Cycles Outlet" for the project "AcmeCycles" at `acmecycles.example`), or a name another tracked competitor answers to (its aliases, domain label or written host). Answers split and join the words of a name freely, so "Tune" cannot belong to one competitor while another answers to "Tune Spoke", "TuneSpoke" or `tunespoke.example`: an answer writing "Tune Spoke" would count both. Joined-word lookalikes ("Tune" and "Tuner") are refused too; pick a longer alias.
|
|
483
575
|
- A new competitor whose domain name overlaps another competitor's alias that way (adding `tunespoke.example` or `spoketuneworks.example` while another competitor carries "Tune", or `tune.example` while one carries "Tune Spoke") is rejected on `competitor add` and apply until you remove or restate that alias; discovery promote leaves it out with reason `claimed-by-alias`. No surface starts tracking the project's own site, a subdomain of it, or a parent of an owned domain. A project identity change that claims a competitor alias drops it, audited as `droppedCompetitorAliases`.
|
|
576
|
+
- On a measured project, a competitor add that would put more than 5 new or recently added domains inside the trailing 24 hours is refused with `bulk-add-unacknowledged`. Review stored evidence with `competitor suggest` or `competitor add --dry-run`, then pass `--acknowledge-bulk-add` for an operator-approved write. Projects without a completed or partial non-probe sweep are not guarded. The rule covers REST, CLI, MCP and dashboard writes; declarative apply and discovery promote remain outside this guard.
|
|
484
577
|
- Advanced markets: a competitor an active or draft market pins counts as another competitor, and so does one a superseded revision pins when the project's runs were measured under that revision (`supersededRevision`), since those runs are still scored with it. An alias overlapping its names is rejected (`market-competitor`, naming the `markets`), a stored one is dropped and audited, and a new competitor whose domain name a pin's label or curated alias claims is rejected (`claimed-by-alias`, naming the `markets`) until you remove that name from the pin. The other way round, pinning a competitor (or giving a pin a new name) fails with `claimed-by-alias` when a curated alias already answers to it, and with `other-competitor` when its label or alias overlaps a tracked competitor's own domain label or host; `pin-competitor`, `upsert-competitor`, `upsert-group` and draft publish all check the pins they add or rename.
|
|
485
578
|
- Not checked: two names that only share a part ("Tune Spoke" and "Spoke Works" both match the phrase "Tune Spoke Works"), and an alias equal to a subdomain label of another competitor's host. Avoid such pairs by hand.
|
|
486
579
|
- Any spelling of a tracked domain (`www.`, a subdomain, or a row an older build's discovery promote stored as a subdomain) finds its stored row; discovery promote now stores the registrable domain. A competitor stored as two rows refuses alias edits; the error names both rows and the aliases to restate: `competitor remove` (which removes every row of it), then `competitor add <project> <domain> --alias ...`.
|
|
@@ -564,7 +657,7 @@ cnry schedule set <project> --preset daily # or: weekly, twice-daily, daily@
|
|
|
564
657
|
cnry schedule set <project> --cron "0 9 * * *" --timezone America/New_York
|
|
565
658
|
cnry schedule set <project> --kind data-refresh --preset daily # refresh all connected GSC/Bing/GA/GBP integrations (no --source)
|
|
566
659
|
cnry schedule set <project> --kind backlinks-sync --preset weekly # re-probe Common Crawl; sync only when a newer rolling window is published (no --source/--provider)
|
|
567
|
-
cnry doctor <project> --check site.reachability # probe the homepage now; the server also probes every 10 min and pages health.degraded after two failed passes
|
|
660
|
+
cnry doctor --project <project> --check site.reachability # probe the homepage now; the server also probes every 10 min and pages health.degraded after two failed passes
|
|
568
661
|
cnry schedule set <project> --kind site-audit --preset weekly # Technical AEO: bounded full-site crawl and audit (no --source/--provider)
|
|
569
662
|
cnry schedule show <project>
|
|
570
663
|
cnry schedule list <project> --format json # all configured schedule kinds
|
|
@@ -1614,6 +1707,7 @@ Compact reference for the composite / keyed commands agents read most (shapes ca
|
|
|
1614
1707
|
|
|
1615
1708
|
| Command | JSON output shape (top-level keys → DTO) | `jsonl` |
|
|
1616
1709
|
|---|---|---|
|
|
1710
|
+
| `cnry status --all [--problems]` | `InstanceStatusDto` @ `contracts/status.ts`: `{ generatedAt, summary{projects,ok,failing,warning,stale,neverSynced}, projects[{project,displayName,state,health,integrations[]}] }`. Stored reads only; a successful read exits 0 even when rows are failing. | One integration per line as `{project, health, ...row}`; a project without integration rows emits `{project,state,health}`. Single-project `status <p>` keeps its JSON envelope and emits one compact record for JSONL. Stored health failures and warnings affect project state, summary and `--problems`; explicit project reads always retain health. |
|
|
1617
1711
|
| `cnry doctor [--project p] [--all] [--report-month YYYY-MM]` | `{ scope, project, reportMonths?, generatedAt, durationMs, summary{total,ok,warn,fail,skipped}, checks[] }` — `DoctorReportDto` @ `contracts/doctor.ts`. `checks[]` = `CheckResultDto{ id, category, scope, title, status(ok\|warn\|fail\|skipped), notificationPolicy?(health\|silent), code, summary, remediation?, details?, durationMs }`. `report.*` checks are silent report-readiness advisories: they never page or change health state. `--report-month` picks the month they grade (default: the current month, plus the closed month through day 3, when only the closed month is graded). With `--all`: an object keyed by `__global__` + each project name, each value a full report. | ✅ one check / line as `{project, …check}`; still exits non-zero if any `fail` |
|
|
1618
1712
|
| `cnry analytics <p> [--feature metrics\|gaps\|sources] [--window 7d\|30d\|90d\|all]` | Object **keyed by feature**: `{ metrics?, gaps?, sources? }` (all three present with no `--feature`; one with `--feature X`). `metrics`=`BrandMetricsDto{ window, buckets[], overall, byProvider, trend, mentionTrend, queryChanges[] }`; `gaps`=`GapAnalysisDto{ cited[], gap[], uncited[], mentionedQueries[], mentionGap[], notMentioned[], runId, window }` (each `[]`=`GapQuery`); `sources`=`SourceBreakdownDto` (same shape as `cnry sources`, below). @ `contracts/analytics.ts` | → degrades to the `json` document |
|
|
1619
1713
|
| `cnry sources <p> [--rank] [--limit N] [--by-provider] [--window …]` | `SourceBreakdownDto{ overall[], byQuery, ranked, byProvider, runId, window, limit }` @ `contracts/analytics.ts`. `ranked`/each `byProvider[name]` = `RankedSourceList{ totalCitedSlots, domainTotal, entries[], truncatedDomainCount, truncatedCitedSlots, bySurfaceClass[] }`; `entries[]`=`SourceRankEntry{ domain, count, percentage, category, label, surfaceClass }`; `bySurfaceClass[]`=`SurfaceClassCount{ surfaceClass, label, count, percentage, domainCount }`. `surfaceClass` ∈ own \| direct-competitor \| ota-aggregator \| editorial-media \| other. | ✅ streams `ranked.entries` one / line as `{project, …entry}` |
|
|
@@ -1696,3 +1790,112 @@ breakdown keys, most mixed plus unfavorable first). Text output lists them under
|
|
|
1696
1790
|
`Most criticized properties: N of total` with favorable, mixed and unfavorable
|
|
1697
1791
|
counts; read one with `--scope property --scope-key <key> --outcome mixed,unfavorable`
|
|
1698
1792
|
(in a market view, pass that market as `--market-key`).
|
|
1793
|
+
|
|
1794
|
+
### Stored traffic samples
|
|
1795
|
+
|
|
1796
|
+
```bash
|
|
1797
|
+
cnry traffic samples <project> --kind ai-referral --since 2026-10-10T00:00:00Z --until 2026-10-11T00:00:00Z --format json
|
|
1798
|
+
cnry traffic samples <project> --source <id> --path /blog --limit 100 --offset 100 --format jsonl
|
|
1799
|
+
```
|
|
1800
|
+
|
|
1801
|
+
A stored sampled tail with 30-day retention, not visits or sessions. Use `traffic
|
|
1802
|
+
events` for totals. Default `--kind all` includes crawler, AI user-fetch, and AI
|
|
1803
|
+
referral samples; unknown requests require `--kind unknown`. Time bounds are
|
|
1804
|
+
inclusive ISO timestamps or `YYYY-MM-DD` dates (midnight UTC), defaulting to the last 24 hours. `--since-minutes`
|
|
1805
|
+
selects a positive relative duration and cannot be combined with `--since`. Path
|
|
1806
|
+
prefixes are literal. Limit is 1 to 500; offset starts at zero. Project/source
|
|
1807
|
+
attribution applies equally to Simple and Advanced portfolios; Property, Target,
|
|
1808
|
+
and market attribution is unavailable. No provider call or write. Signed-in
|
|
1809
|
+
viewers and embeds cannot read samples; API read keys retain project scope.
|
|
1810
|
+
|
|
1811
|
+
### Stored GA acquisition
|
|
1812
|
+
|
|
1813
|
+
```bash
|
|
1814
|
+
cnry ga acquisition <project> --start 2026-09-01 --end 2026-09-30 --channel "Organic Search" --landing-page /pricing --format json
|
|
1815
|
+
cnry ga acquisition <project> --group-by source,medium --limit 100 --offset 100 --format jsonl
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
Reads stored GA4 sessions only, with no Google call. `--window 7d|30d|90d|all`
|
|
1819
|
+
defaults to all history. Use either a rolling window or explicit `--start` and `--end`; combining them is rejected. `--source` and `--medium` match exactly; `--channel` and `--host`
|
|
1820
|
+
match exactly without case sensitivity. `--landing-page` normalizes the supplied path like stored pages, removing trailing slashes and tracking parameters while retaining meaningful query parameters. `--path-prefix` matches a path or its
|
|
1821
|
+
descendants, treating percent and underscore literally. Group keys are date,
|
|
1822
|
+
channel, source, medium, host, landing-page (API names are also accepted). Default
|
|
1823
|
+
grouping uses all six. `totalSessions` covers every filtered row, independent of
|
|
1824
|
+
grouping and pagination; `totalRows` counts groups. Maximum page size is 1000.
|
|
1825
|
+
|
|
1826
|
+
### Stored read windows and calendar dates
|
|
1827
|
+
|
|
1828
|
+
Stored reads accept a positive day count such as `30` or its lowercase token
|
|
1829
|
+
`30d`, restricted to the command's available stored windows:
|
|
1830
|
+
|
|
1831
|
+
| Read | Windows |
|
|
1832
|
+
| --- | --- |
|
|
1833
|
+
| Analytics, sources, competitor landscape, GA acquisition, traffic and referral/session histories, Google performance reads | `7d`, `30d`, `90d`, `all` |
|
|
1834
|
+
| GA measurement analysis | `30d`, `60d`, `90d` |
|
|
1835
|
+
| GA search landing pages | `7d`, `28d`, `90d` |
|
|
1836
|
+
| Google Ads performance | `7d`, `14d`, `30d` |
|
|
1837
|
+
| Traffic analytics | `7d`, `14d`, `30d`, `90d` |
|
|
1838
|
+
| Organic evidence | `60d`, `90d` |
|
|
1839
|
+
|
|
1840
|
+
Defaults remain specific to each command. `--help` prints its full set. Traffic
|
|
1841
|
+
analytics and organic evidence accept `--window`; `--period` remains an alias.
|
|
1842
|
+
Use either `--window` or explicit calendar bounds, never both.
|
|
1843
|
+
For reads with calendar bounds, use inclusive `--start` and `--end` in
|
|
1844
|
+
`YYYY-MM-DD` form. Traffic referral assessment retains `--start-date` and
|
|
1845
|
+
`--end-date`; ads insights retains `--from` and `--to`. Supplying an alias and its
|
|
1846
|
+
canonical flag together is an error. Invalid calendar dates or reversed ranges
|
|
1847
|
+
fail before an API request. Explicit dates retain their existing precedence over
|
|
1848
|
+
rolling windows. Google performance `--days` remains a separate span selector.
|
|
1849
|
+
These changes do not alter sync/backfill depths, run/history ISO timestamps, or
|
|
1850
|
+
calendar-month report selections.
|
|
1851
|
+
|
|
1852
|
+
### Applying project configuration
|
|
1853
|
+
|
|
1854
|
+
```bash
|
|
1855
|
+
canonry apply <file...> --dry-run [--format json]
|
|
1856
|
+
canonry apply <file...> [--format json]
|
|
1857
|
+
```
|
|
1858
|
+
|
|
1859
|
+
`--dry-run` calls the server preview route for every YAML document and reports
|
|
1860
|
+
field, query, competitor, schedule and notification changes. It performs no
|
|
1861
|
+
database writes and prints `No DB writes performed. Re-run without --dry-run to
|
|
1862
|
+
apply.` in text mode. JSON and JSONL emit the same `{ dryRun, files,
|
|
1863
|
+
changedCount, errorCount }` document. A server without the preview route exits
|
|
1864
|
+
with `APPLY_PREVIEW_UNSUPPORTED`; the command never falls back to apply.
|
|
1865
|
+
|
|
1866
|
+
### Preview competitor impact
|
|
1867
|
+
|
|
1868
|
+
Add, remove/delete and curated alias set/add/remove/clear accept `--dry-run`.
|
|
1869
|
+
JSON and JSONL include `dryRun`, the planned changes and the server's `impact`;
|
|
1870
|
+
text prints before/after mention share and counts. No competitors, names or
|
|
1871
|
+
audit rows change. The latest stored landscape is the estimate, while a real
|
|
1872
|
+
change restates every stored period. Advanced scope includes all markets; the
|
|
1873
|
+
response names its exact runs and query class. This is not the overview card.
|
|
1874
|
+
|
|
1875
|
+
`project update <project> --competitor-auto-aliases apply --dry-run` previews
|
|
1876
|
+
the mode and detected-name impact. Other change flags are refused. Detection
|
|
1877
|
+
is a stored read; `competitor aliases detect --dry-run --apply` is invalid.
|
|
1878
|
+
Block/unblock, market pin writes, project identity edits, apply and discovery
|
|
1879
|
+
promotion are not covered. Ordinary writes keep their request count and output.
|
|
1880
|
+
Preview POSTs require a write-capable key; the detection GET accepts read keys.
|
|
1881
|
+
### Batch curated competitors and wait for stored fields
|
|
1882
|
+
|
|
1883
|
+
`canonry competitor import <project> <file> [--wait] [--wait-timeout 120]` sends
|
|
1884
|
+
one append request for the whole file. A JSON file is an array of domains or
|
|
1885
|
+
`{ "domain": "rival.example", "aliases": ["Rival Name"] }` entries. Other files
|
|
1886
|
+
contain one `domain[,alias,alias]` per line; blank lines and `#` comments are
|
|
1887
|
+
ignored. Import only a curated set, not every cited domain. Repeating an import
|
|
1888
|
+
is idempotent and reports `addedCount: 0` when nothing is newly tracked.
|
|
1889
|
+
|
|
1890
|
+
`competitor add` and `competitor aliases` also accept `--wait` with a positive
|
|
1891
|
+
integer timeout in seconds (default 120). This waits for stored answer fields
|
|
1892
|
+
to be recomputed after name changes, such as `competitor add demo rival.example
|
|
1893
|
+
--alias "Rival Name" --wait`. A bare-domain add sets no recomputation mark.
|
|
1894
|
+
Answer-derived alias detection has a separate lifecycle and is not covered by
|
|
1895
|
+
this wait. Cloud already completes the backfill within the write request.
|
|
1896
|
+
|
|
1897
|
+
Timeout exits 2 with `OPERATION_IN_PROGRESS` and `writeCommitted: true`; read
|
|
1898
|
+
`project show` to inspect `answerFieldsRecompute` rather than treating the write
|
|
1899
|
+
as failed. Older servers that omit the field return `WAIT_STATUS_UNAVAILABLE`
|
|
1900
|
+
after the committed write. Without `--wait`, add timing and output are unchanged.
|
|
1901
|
+
The existing `canonry_project_get` MCP tool returns this additive field.
|
|
@@ -730,7 +730,7 @@ transactional receipt protects each event ID for ten minutes. Receipt claims,
|
|
|
730
730
|
hourly rollups, samples, and source progress commit in one transaction.
|
|
731
731
|
|
|
732
732
|
Direct push sends one selected event per batch. It does not batch or sample at
|
|
733
|
-
the edge. Canonry
|
|
733
|
+
the edge. Canonry retains at most 100 direct-push samples per source per UTC hour, while
|
|
734
734
|
it continues to add all classified events to hourly rollups.
|
|
735
735
|
|
|
736
736
|
The Worker transports the raw client IP, full query string, and full referer
|
|
@@ -1052,7 +1052,7 @@ The doctor checks are adapter-agnostic. When they fail or warn:
|
|
|
1052
1052
|
| `traffic.source.queue-backlog` | `traffic.queue-backlog.within-drain-budget` | 1–1,000 Queue messages remain. If no new messages arrive, the next scheduled sync can drain them. Run a manual sync to accelerate. |
|
|
1053
1053
|
| `traffic.source.queue-backlog` | `traffic.queue-backlog.remaining` | More than 1,000 Queue messages remain. Run a manual sync. If the backlog persists, shorten the traffic-sync schedule. |
|
|
1054
1054
|
| `traffic.source.credentials` | `traffic.credentials.resolve-failed` | Reconnect from the host that owns the source credentials. Queue pull requires a non-empty Account Queues Edit token paired by source ID. |
|
|
1055
|
-
| `traffic.source.cache-blindspot` | `traffic.cache-blindspot.wordpress-plugin` | Cache-served requests never execute PHP and produce no event, so a source can look active while real page views go uncounted. Exclude selected AI user agents from every page-cache and CDN layer, or switch to a log/edge source. Warns only, not a failure.
|
|
1055
|
+
| `traffic.source.cache-blindspot` | `traffic.cache-blindspot.wordpress-plugin` | Cache-served requests never execute PHP and produce no event, so a source can look active while real page views go uncounted. Exclude selected AI user agents from every page-cache and CDN layer, or switch to a log/edge source. Warns only, not a failure. |
|
|
1056
1056
|
| `traffic.source.worker-version` | `traffic.worker-version.waiting-for-first-event` | Send a smoke-test request through the Worker. For Queue delivery, run a sync to ingest it. Then run the doctor again. |
|
|
1057
1057
|
| `traffic.source.worker-version` | `traffic.worker-version.stale` | Regenerate and redeploy the Worker from the credential-owning host with the source's existing delivery mode, then verify the route or Queue binding. |
|
|
1058
1058
|
|
|
@@ -1169,3 +1169,15 @@ Use GA4 for landed browser visits or edge/access logs for cache-independent
|
|
|
1169
1169
|
request coverage. The `traffic.source.cache-blindspot` doctor check is a static
|
|
1170
1170
|
warning; it does not request or impersonate traffic against the public origin,
|
|
1171
1171
|
and it never sends a `health.degraded` alert.
|
|
1172
|
+
|
|
1173
|
+
### Inspect stored samples
|
|
1174
|
+
|
|
1175
|
+
`cnry traffic samples <project> --kind ai-referral --format json` exposes exact
|
|
1176
|
+
stored timestamps, normalized paths, status, referer host, user agent and
|
|
1177
|
+
classifier evidence. It never exposes IP hashes. These are a sampled tail with
|
|
1178
|
+
30-day retention: pull syncs keep up to 100 per sync, backfills 500 per run, and
|
|
1179
|
+
direct push up to 100 per source per UTC hour. There is no universal hourly cap.
|
|
1180
|
+
`totalRows` counts matching samples, not visits. Use `traffic events` for rollup
|
|
1181
|
+
totals; `traffic status` already reports last-24h sample counts per source across
|
|
1182
|
+
all kinds. The new read excludes unknown requests by default and requires them
|
|
1183
|
+
explicitly with `--kind unknown`. Viewers and embeds cannot read sample rows.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a,u as h,j as e}from"./vendor-tanstack-ZXXkndVj.js";import{
|
|
1
|
+
import{r as a,u as h,j as e}from"./vendor-tanstack-ZXXkndVj.js";import{dq as L,dr as P,cm as E,y as u,B as x,w as v,T as M}from"./index-ZS9sSTMS.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|refused/.test(s)?"negative":/create|add|connect|completed/.test(s)?"positive":/update|replace|sync|apply/.test(s)?"caution":"neutral"}function D(s){return[s.action,s.actor,s.entityType,s.entityId,s.actorSession,s.userAgent,s.diff==null?"":JSON.stringify(s.diff)].filter(Boolean).join(" ").toLowerCase()}function H({projectName:s}){const[n,l]=a.useState(0),[c,y]=a.useState("90d"),[m,N]=a.useState(""),[p,w]=a.useState(""),S=a.useMemo(()=>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{H as A};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as n,j as e}from"./vendor-tanstack-ZXXkndVj.js";import{dC as _,aG as O,dD as U,dE as J,dF as K,w as i,T as u,z as d,B as k,E as F,dG as V,dH as Y,aJ as X,dI as Z}from"./index-ahEr6Oc-.js";import{H as p}from"./Hint-DICI5Jq9.js";import{I as ee,T as se,E as te,j as ae,N as ne}from"./vendor-lucide-Y6jANGfG.js";import"./vendor-radix-twS1M3FF.js";import"./vendor-recharts-D-cUuLZo.js";import"./vendor-markdown-B4giJcws.js";import"./vendor-yaml-GWJz5H2G.js";const re="https://commoncrawl.org/web-graphs";function w(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 j(t){if(!t)return"—";const f=Date.now()-new Date(t).getTime(),a=Math.floor(f/6e4);if(a<1)return"just now";if(a<60)return`${a}m ago`;const o=Math.floor(a/60);return o<24?`${o}h ago`:`${Math.floor(o/24)}d ago`}function S(t){switch(t){case"ready":return"positive";case"failed":return"negative";case"downloading":case"querying":case"queued":return"caution"}}function ue(){const[t,f]=n.useState(null),[a,o]=n.useState(null),[y,P]=n.useState([]),[g,L]=n.useState([]),[x,T]=n.useState(null),[C,B]=n.useState(!0),[D,R]=n.useState(!1),[h,E]=n.useState(!1),[N,b]=n.useState(""),[$,v]=n.useState(!1),[I,l]=n.useState(null),[A,c]=n.useState(null),m=n.useCallback(async()=>{B(!0),l(null);try{const[s,r,z,Q,W]=await Promise.all([_(),O().catch(()=>null),U().catch(()=>[]),J().catch(()=>[]),K().catch(()=>null)]);f(s),o(r),P(z),L(Q),T(W)}catch(s){l(s instanceof Error?s.message:"Failed to load backlinks status")}finally{B(!1)}},[]);n.useEffect(()=>{m()},[m]);async function q(){R(!0),l(null),c(null);try{const s=await V();c(s.alreadyPresent?`DuckDB already installed (${s.version}).`:`Installed DuckDB ${s.version}.`),await m()}catch(s){l(s instanceof Error?s.message:"Failed to install DuckDB")}finally{R(!1)}}async function M(){const s=N.trim()||void 0;E(!0),l(null),c(null);try{const r=await Y(s);c(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.`),b(""),v(!1),await m()}catch(r){r instanceof X&&r.code==="MISSING_DEPENDENCY"?l("DuckDB is not installed. Install it first."):l(r instanceof Error?r.message:"Failed to trigger sync")}finally{E(!1)}}async function G(s){l(null),c(null);try{await Z(s),c(`Pruned cached release ${s}.`),await m()}catch(r){l(r instanceof Error?r.message:"Failed to prune release")}}const H=a?.status==="ready"&&g.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:"Backlink data"}),e.jsx("p",{className:"page-subtitle",children:"Manage the shared backlink source and review its latest data."})]})}),I&&e.jsx(i,{className:"surface-card p-4 mb-4 border-negative-800/60",children:e.jsx("p",{className:"text-sm text-negative",children:I})}),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:"Readiness"}),e.jsx("h2",{children:"Backlink source"})]}),t?.duckdbInstalled?e.jsx(u,{tone:"positive",children:"Installed"}):e.jsx(u,{tone:"caution",children:"Not installed"})]}),e.jsx(i,{className:"surface-card p-4",children:C?e.jsx("p",{className:"text-sm text-muted",children:"Checking…"}):t?.duckdbInstalled?e.jsx("p",{className:"text-sm text-secondary",children:"Ready to sync a Common Crawl release."}):e.jsxs("div",{className:"flex items-center justify-between gap-4",children:[e.jsx("p",{className:"text-sm text-secondary",children:"Install the local query engine to enable release syncs."}),!d()&&e.jsxs(k,{type:"button",size:"sm",disabled:D,onClick:F(q),children:[e.jsx(ee,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),D?"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(p,{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(u,{tone:S(a.status),children:a.status})]}),e.jsxs(i,{className:"surface-card p-5",children:[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-secondary",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(p,{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:j(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:j(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."}),H&&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(se,{className:"h-4 w-4 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:"Cached files for this release are missing."}),e.jsx("p",{className:"mt-1 text-secondary",children:"Sync this release again, or sync the newest release, before refreshing individual projects."})]})]})}),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"}),x?e.jsxs("p",{className:"text-sm text-strong mt-0.5",children:[e.jsx("code",{className:"text-heading",children:x.release}),e.jsxs("span",{className:"ml-2 text-xs text-muted",children:["Vertex ",w(x.vertexBytes),", edges ",w(x.edgesBytes)]})]}):e.jsx("p",{className:"text-sm text-muted mt-0.5",children:C?"Checking Common Crawl…":"Could not find a release. Choose one below."}),e.jsxs("a",{href:re,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(te,{className:"h-3 w-3","aria-hidden":!0})]})]}),!d()&&e.jsxs("div",{className:"flex items-center gap-2 shrink-0",children:[e.jsxs(k,{type:"button",size:"sm",disabled:h||!t?.duckdbInstalled||!x&&!N.trim(),onClick:F(M),children:[e.jsx(ae,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),h?"Queuing…":"Run sync"]}),e.jsxs(p,{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)."]})]})]})]}),d()?null:$?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=>b(s.target.value),disabled:h,autoFocus:!0}),e.jsx("button",{type:"button",className:"text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>{b(""),v(!1)},disabled:h,children:"Cancel"})]}):e.jsx("button",{type:"button",className:"text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>v(!0),disabled:h,children:"Use a different release →"})]}),!t?.duckdbInstalled&&e.jsx("p",{className:"text-sm text-secondary mt-2",children:"Install DuckDB first to enable sync."})]})]}),e.jsxs("details",{className:"page-section-divider group",children:[e.jsx("summary",{className:"cursor-pointer text-sm font-medium text-secondary hover:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",children:"Technical details and cache"}),e.jsxs("div",{className:"mt-4",children:[e.jsx("p",{className:"text-sm text-secondary mb-3",children:"Cached releases support project re-extracts. Pruning a release does not remove saved backlink results."}),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"}),!d()&&e.jsx("th",{className:"px-4 py-2 font-medium sr-only",children:"Actions"})]})}),e.jsxs("tbody",{children:[g.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(u,{tone:S(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:w(s.bytes)}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:j(s.lastUsedAt)}),!d()&&e.jsx("td",{className:"px-4 py-2 text-right",children:e.jsxs("div",{className:"inline-flex items-center gap-1",children:[e.jsxs(k,{type:"button",variant:"outline",size:"sm",onClick:()=>{G(s.release)},children:[e.jsx(ne,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),"Prune"]}),e.jsx(p,{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)),g.length===0&&e.jsx("tr",{children:e.jsx("td",{className:"px-4 py-4 text-sm text-muted",colSpan:d()?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."})})]})]})}),e.jsx("p",{className:"mt-3 text-xs text-muted",children:"Common Crawl files are stored locally. DuckDB reads them; Canonry stores results in its normal database."})]})]}),y.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:y.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(u,{tone:S(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:j(s.queryFinishedAt??s.updatedAt)})]},s.id))})]})})]})]})}export{ue as BacklinksPage};
|
|
1
|
+
import{r as n,j as e}from"./vendor-tanstack-ZXXkndVj.js";import{dF as _,aJ as O,dG as U,dH as J,dI as K,w as i,T as u,z as d,B as k,E as F,dJ as V,dK as Y,aM as X,dL as Z}from"./index-ZS9sSTMS.js";import{H as p}from"./Hint-DICI5Jq9.js";import{I as ee,T as se,E as te,j as ae,N as ne}from"./vendor-lucide-Y6jANGfG.js";import"./vendor-radix-twS1M3FF.js";import"./vendor-recharts-D-cUuLZo.js";import"./vendor-markdown-B4giJcws.js";import"./vendor-yaml-GWJz5H2G.js";const re="https://commoncrawl.org/web-graphs";function w(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 j(t){if(!t)return"—";const f=Date.now()-new Date(t).getTime(),a=Math.floor(f/6e4);if(a<1)return"just now";if(a<60)return`${a}m ago`;const o=Math.floor(a/60);return o<24?`${o}h ago`:`${Math.floor(o/24)}d ago`}function S(t){switch(t){case"ready":return"positive";case"failed":return"negative";case"downloading":case"querying":case"queued":return"caution"}}function ue(){const[t,f]=n.useState(null),[a,o]=n.useState(null),[y,P]=n.useState([]),[g,L]=n.useState([]),[x,T]=n.useState(null),[C,B]=n.useState(!0),[D,R]=n.useState(!1),[h,I]=n.useState(!1),[N,b]=n.useState(""),[$,v]=n.useState(!1),[E,l]=n.useState(null),[A,c]=n.useState(null),m=n.useCallback(async()=>{B(!0),l(null);try{const[s,r,z,Q,W]=await Promise.all([_(),O().catch(()=>null),U().catch(()=>[]),J().catch(()=>[]),K().catch(()=>null)]);f(s),o(r),P(z),L(Q),T(W)}catch(s){l(s instanceof Error?s.message:"Failed to load backlinks status")}finally{B(!1)}},[]);n.useEffect(()=>{m()},[m]);async function q(){R(!0),l(null),c(null);try{const s=await V();c(s.alreadyPresent?`DuckDB already installed (${s.version}).`:`Installed DuckDB ${s.version}.`),await m()}catch(s){l(s instanceof Error?s.message:"Failed to install DuckDB")}finally{R(!1)}}async function M(){const s=N.trim()||void 0;I(!0),l(null),c(null);try{const r=await Y(s);c(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.`),b(""),v(!1),await m()}catch(r){r instanceof X&&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 G(s){l(null),c(null);try{await Z(s),c(`Pruned cached release ${s}.`),await m()}catch(r){l(r instanceof Error?r.message:"Failed to prune release")}}const H=a?.status==="ready"&&g.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:"Backlink data"}),e.jsx("p",{className:"page-subtitle",children:"Manage the shared backlink source and review its latest data."})]})}),E&&e.jsx(i,{className:"surface-card p-4 mb-4 border-negative-800/60",children:e.jsx("p",{className:"text-sm text-negative",children:E})}),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:"Readiness"}),e.jsx("h2",{children:"Backlink source"})]}),t?.duckdbInstalled?e.jsx(u,{tone:"positive",children:"Installed"}):e.jsx(u,{tone:"caution",children:"Not installed"})]}),e.jsx(i,{className:"surface-card p-4",children:C?e.jsx("p",{className:"text-sm text-muted",children:"Checking…"}):t?.duckdbInstalled?e.jsx("p",{className:"text-sm text-secondary",children:"Ready to sync a Common Crawl release."}):e.jsxs("div",{className:"flex items-center justify-between gap-4",children:[e.jsx("p",{className:"text-sm text-secondary",children:"Install the local query engine to enable release syncs."}),!d()&&e.jsxs(k,{type:"button",size:"sm",disabled:D,onClick:F(q),children:[e.jsx(ee,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),D?"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(p,{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(u,{tone:S(a.status),children:a.status})]}),e.jsxs(i,{className:"surface-card p-5",children:[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-secondary",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(p,{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:j(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:j(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."}),H&&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(se,{className:"h-4 w-4 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:"Cached files for this release are missing."}),e.jsx("p",{className:"mt-1 text-secondary",children:"Sync this release again, or sync the newest release, before refreshing individual projects."})]})]})}),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"}),x?e.jsxs("p",{className:"text-sm text-strong mt-0.5",children:[e.jsx("code",{className:"text-heading",children:x.release}),e.jsxs("span",{className:"ml-2 text-xs text-muted",children:["Vertex ",w(x.vertexBytes),", edges ",w(x.edgesBytes)]})]}):e.jsx("p",{className:"text-sm text-muted mt-0.5",children:C?"Checking Common Crawl…":"Could not find a release. Choose one below."}),e.jsxs("a",{href:re,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(te,{className:"h-3 w-3","aria-hidden":!0})]})]}),!d()&&e.jsxs("div",{className:"flex items-center gap-2 shrink-0",children:[e.jsxs(k,{type:"button",size:"sm",disabled:h||!t?.duckdbInstalled||!x&&!N.trim(),onClick:F(M),children:[e.jsx(ae,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),h?"Queuing…":"Run sync"]}),e.jsxs(p,{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)."]})]})]})]}),d()?null:$?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=>b(s.target.value),disabled:h,autoFocus:!0}),e.jsx("button",{type:"button",className:"text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>{b(""),v(!1)},disabled:h,children:"Cancel"})]}):e.jsx("button",{type:"button",className:"text-sm text-secondary hover:text-neutral focus:text-neutral focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",onClick:()=>v(!0),disabled:h,children:"Use a different release →"})]}),!t?.duckdbInstalled&&e.jsx("p",{className:"text-sm text-secondary mt-2",children:"Install DuckDB first to enable sync."})]})]}),e.jsxs("details",{className:"page-section-divider group",children:[e.jsx("summary",{className:"cursor-pointer text-sm font-medium text-secondary hover:text-strong focus:outline-none focus-visible:ring-1 focus-visible:ring-mono-500 rounded",children:"Technical details and cache"}),e.jsxs("div",{className:"mt-4",children:[e.jsx("p",{className:"text-sm text-secondary mb-3",children:"Cached releases support project re-extracts. Pruning a release does not remove saved backlink results."}),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"}),!d()&&e.jsx("th",{className:"px-4 py-2 font-medium sr-only",children:"Actions"})]})}),e.jsxs("tbody",{children:[g.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(u,{tone:S(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:w(s.bytes)}),e.jsx("td",{className:"px-4 py-2 text-secondary",children:j(s.lastUsedAt)}),!d()&&e.jsx("td",{className:"px-4 py-2 text-right",children:e.jsxs("div",{className:"inline-flex items-center gap-1",children:[e.jsxs(k,{type:"button",variant:"outline",size:"sm",onClick:()=>{G(s.release)},children:[e.jsx(ne,{className:"h-4 w-4 mr-1.5","aria-hidden":!0}),"Prune"]}),e.jsx(p,{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)),g.length===0&&e.jsx("tr",{children:e.jsx("td",{className:"px-4 py-4 text-sm text-muted",colSpan:d()?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."})})]})]})}),e.jsx("p",{className:"mt-3 text-xs text-muted",children:"Common Crawl files are stored locally. DuckDB reads them; Canonry stores results in its normal database."})]})]}),y.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:y.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(u,{tone:S(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:j(s.queryFinishedAt??s.updatedAt)})]},s.id))})]})})]})]})}export{ue as BacklinksPage};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as e}from"./vendor-tanstack-ZXXkndVj.js";import{A as s}from"./AuditHistoryPanel-
|
|
1
|
+
import{j as e}from"./vendor-tanstack-ZXXkndVj.js";import{A as s}from"./AuditHistoryPanel-CvANw3ds.js";import"./index-ZS9sSTMS.js";import"./vendor-radix-twS1M3FF.js";import"./vendor-recharts-D-cUuLZo.js";import"./vendor-lucide-Y6jANGfG.js";import"./vendor-markdown-B4giJcws.js";import"./vendor-yaml-GWJz5H2G.js";function l(){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:"Changes to projects and settings."})]})}),e.jsx(s,{})]})}export{l as HistoryPage};
|