@canonry/canonry 7.32.0 → 7.43.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +24 -0
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +204 -4
  3. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +14 -2
  4. package/assets/assets/{AuditHistoryPanel-cQyVA85P.js → AuditHistoryPanel-C5oZsvsR.js} +1 -1
  5. package/assets/assets/{BacklinksPage-UGZ_9N7F.js → BacklinksPage-CF5sGSTQ.js} +1 -1
  6. package/assets/assets/{HistoryPage-UJVFZ5LK.js → HistoryPage-DumL6cl5.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-ChpOxQfx.js → MeasurementPropertyPage-DCegOp_w.js} +3 -3
  8. package/assets/assets/ProjectPage-CWDk48Y6.js +8 -0
  9. package/assets/assets/RunsPage-Bb1VMWYu.js +1 -0
  10. package/assets/assets/{SettingsPage-DeKxjuTr.js → SettingsPage-CHGh169l.js} +1 -1
  11. package/assets/assets/{SiteHealthSection-COeVsgop.js → SiteHealthSection-bGzH9m9-.js} +3 -3
  12. package/assets/assets/{TrafficPage-DmMmeJKK.js → TrafficPage-cfnEZ2Mu.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-CUehseZe.js → TrafficSourceDetailPage-BQS0MAXE.js} +1 -1
  14. package/assets/assets/{extract-error-message-BsxdS7Po.js → extract-error-message-IWhACkO_.js} +1 -1
  15. package/assets/assets/index-BDY3-V1S.js +86 -0
  16. package/assets/assets/{react-sigma_core.esm.min--MbbBWzJ.js → react-sigma_core.esm.min-BQt189AI.js} +1 -1
  17. package/assets/assets/{v2-overview-adapter-D-IScpdp.js → v2-overview-adapter-ZC3VuoTT.js} +1 -1
  18. package/assets/index.html +1 -1
  19. package/dist/{chunk-WRHA54BN.js → chunk-65BHDVYO.js} +5604 -3984
  20. package/dist/{chunk-FW4GX2LO.js → chunk-BPFRGC7K.js} +3 -3
  21. package/dist/{chunk-2C6NWFN4.js → chunk-BWR5MNL5.js} +2 -2
  22. package/dist/chunk-C2I7M3B2.js +22948 -0
  23. package/dist/{chunk-42XUY243.js → chunk-E43JM3QV.js} +11430 -10405
  24. package/dist/chunk-HGMYEDAQ.js +79 -0
  25. package/dist/{chunk-QFXD7GR5.js → chunk-KOIOZ7QW.js} +193 -19
  26. package/dist/{chunk-ZIETXJYU.js → chunk-LYKDNJXR.js} +158 -104
  27. package/dist/chunk-WRUKKEGR.js +562 -0
  28. package/dist/chunk-WU2BRWXY.js +546 -0
  29. package/dist/{chunk-K5EGSSXC.js → chunk-WXX3NL4C.js} +108 -41
  30. package/dist/cli-catalog-J3O6W726.js +15 -0
  31. package/dist/cli-commands-QST2CPDG.js +15 -0
  32. package/dist/cli.js +136 -22525
  33. package/dist/{demo-server-G5OK7SWE.js → demo-server-KA7DWTY6.js} +5 -4
  34. package/dist/index.js +7 -6
  35. package/dist/{intelligence-service-3ZUVTIO7.js → intelligence-service-LHZZTJPH.js} +2 -2
  36. package/dist/mcp.js +30 -15
  37. package/package.json +10 -10
  38. package/assets/assets/ProjectPage-CvxuPSiC.js +0 -8
  39. package/assets/assets/RunsPage-C6U_YcDY.js +0 -1
  40. package/assets/assets/index-ahEr6Oc-.js +0 -86
@@ -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> # mention + citation summary + domain info; says when the next sweep is refused or skips providers
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 (includes both real and probe runs; filter on `trigger` if you only want one)
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
  ```
@@ -449,19 +532,25 @@ cnry query import <project> queries.txt
449
532
  cnry query generate <project> --provider gemini --count 10 --save
450
533
 
451
534
  cnry competitor add <project> competitor1.com competitor2.com
535
+ cnry competitor add <project> rival.example --dry-run --format json # preview latest stored-evidence impact without writing
452
536
  cnry competitor add <project> spoketuneworks.example --alias TuneSpoke # one domain; repeat --alias for more names
537
+ cnry competitor add <project> competitor1.com competitor2.com --acknowledge-bulk-add # operator acknowledgement for measured growth over 5 domains in 24 hours
453
538
  cnry competitor list <project> # text shows each competitor's aliases and auto names; JSON rows carry `aliases`, `autoAliases`, `blockedAliases`
454
539
  cnry competitor aliases <project> qvx.example # read one competitor's curated, auto-detected (with evidence) and blocked names
455
540
  cnry competitor aliases <project> qvx.example --set QVX # exact list (repeat --set); --clear empties it
541
+ cnry competitor aliases <project> qvx.example --set QVX --dry-run --format json # preview an alias identity change
456
542
  cnry competitor aliases <project> qvx.example --add "QVX Stores" # appends server-side in one call
457
543
  cnry competitor aliases <project> qvx.example --remove qvx # read, edit, write back (a concurrent edit can be overwritten)
458
544
  cnry competitor aliases <project> spoketuneworks.example --block "Spoke Crew" # never auto-apply this name (removes it if auto)
459
545
  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
546
+ cnry competitor aliases detect <project> --format json # dry run: names the stored answers tie to each site, with evidence and impact
461
547
  cnry competitor aliases detect <project> --apply # run detection now and store it, in any mode (after each sweep the project's mode decides)
462
548
  cnry competitor landscape <project> --query-class non-brand --window 30d --format json
549
+ cnry competitor suggest <project> --window 30d --min-answers 3 --query-class non-brand --format json
463
550
  ```
464
551
 
552
+ `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.
553
+
465
554
  ### Query class
466
555
 
467
556
  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 +570,7 @@ A competitor is stored as its registrable domain, and every competitor mention m
481
570
 
482
571
  - 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
572
  - 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`.
573
+ - 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
574
  - 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
575
  - 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
576
  - 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 +654,7 @@ cnry schedule set <project> --preset daily # or: weekly, twice-daily, daily@
564
654
  cnry schedule set <project> --cron "0 9 * * *" --timezone America/New_York
565
655
  cnry schedule set <project> --kind data-refresh --preset daily # refresh all connected GSC/Bing/GA/GBP integrations (no --source)
566
656
  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
657
+ 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
658
  cnry schedule set <project> --kind site-audit --preset weekly # Technical AEO: bounded full-site crawl and audit (no --source/--provider)
569
659
  cnry schedule show <project>
570
660
  cnry schedule list <project> --format json # all configured schedule kinds
@@ -1614,6 +1704,7 @@ Compact reference for the composite / keyed commands agents read most (shapes ca
1614
1704
 
1615
1705
  | Command | JSON output shape (top-level keys → DTO) | `jsonl` |
1616
1706
  |---|---|---|
1707
+ | `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
1708
  | `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
1709
  | `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
1710
  | `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 +1787,112 @@ breakdown keys, most mixed plus unfavorable first). Text output lists them under
1696
1787
  `Most criticized properties: N of total` with favorable, mixed and unfavorable
1697
1788
  counts; read one with `--scope property --scope-key <key> --outcome mixed,unfavorable`
1698
1789
  (in a market view, pass that market as `--market-key`).
1790
+
1791
+ ### Stored traffic samples
1792
+
1793
+ ```bash
1794
+ cnry traffic samples <project> --kind ai-referral --since 2026-10-10T00:00:00Z --until 2026-10-11T00:00:00Z --format json
1795
+ cnry traffic samples <project> --source <id> --path /blog --limit 100 --offset 100 --format jsonl
1796
+ ```
1797
+
1798
+ A stored sampled tail with 30-day retention, not visits or sessions. Use `traffic
1799
+ events` for totals. Default `--kind all` includes crawler, AI user-fetch, and AI
1800
+ referral samples; unknown requests require `--kind unknown`. Time bounds are
1801
+ inclusive ISO timestamps or `YYYY-MM-DD` dates (midnight UTC), defaulting to the last 24 hours. `--since-minutes`
1802
+ selects a positive relative duration and cannot be combined with `--since`. Path
1803
+ prefixes are literal. Limit is 1 to 500; offset starts at zero. Project/source
1804
+ attribution applies equally to Simple and Advanced portfolios; Property, Target,
1805
+ and market attribution is unavailable. No provider call or write. Signed-in
1806
+ viewers and embeds cannot read samples; API read keys retain project scope.
1807
+
1808
+ ### Stored GA acquisition
1809
+
1810
+ ```bash
1811
+ cnry ga acquisition <project> --start 2026-09-01 --end 2026-09-30 --channel "Organic Search" --landing-page /pricing --format json
1812
+ cnry ga acquisition <project> --group-by source,medium --limit 100 --offset 100 --format jsonl
1813
+ ```
1814
+
1815
+ Reads stored GA4 sessions only, with no Google call. `--window 7d|30d|90d|all`
1816
+ 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`
1817
+ 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
1818
+ descendants, treating percent and underscore literally. Group keys are date,
1819
+ channel, source, medium, host, landing-page (API names are also accepted). Default
1820
+ grouping uses all six. `totalSessions` covers every filtered row, independent of
1821
+ grouping and pagination; `totalRows` counts groups. Maximum page size is 1000.
1822
+
1823
+ ### Stored read windows and calendar dates
1824
+
1825
+ Stored reads accept a positive day count such as `30` or its lowercase token
1826
+ `30d`, restricted to the command's available stored windows:
1827
+
1828
+ | Read | Windows |
1829
+ | --- | --- |
1830
+ | Analytics, sources, competitor landscape, GA acquisition, traffic and referral/session histories, Google performance reads | `7d`, `30d`, `90d`, `all` |
1831
+ | GA measurement analysis | `30d`, `60d`, `90d` |
1832
+ | GA search landing pages | `7d`, `28d`, `90d` |
1833
+ | Google Ads performance | `7d`, `14d`, `30d` |
1834
+ | Traffic analytics | `7d`, `14d`, `30d`, `90d` |
1835
+ | Organic evidence | `60d`, `90d` |
1836
+
1837
+ Defaults remain specific to each command. `--help` prints its full set. Traffic
1838
+ analytics and organic evidence accept `--window`; `--period` remains an alias.
1839
+ Use either `--window` or explicit calendar bounds, never both.
1840
+ For reads with calendar bounds, use inclusive `--start` and `--end` in
1841
+ `YYYY-MM-DD` form. Traffic referral assessment retains `--start-date` and
1842
+ `--end-date`; ads insights retains `--from` and `--to`. Supplying an alias and its
1843
+ canonical flag together is an error. Invalid calendar dates or reversed ranges
1844
+ fail before an API request. Explicit dates retain their existing precedence over
1845
+ rolling windows. Google performance `--days` remains a separate span selector.
1846
+ These changes do not alter sync/backfill depths, run/history ISO timestamps, or
1847
+ calendar-month report selections.
1848
+
1849
+ ### Applying project configuration
1850
+
1851
+ ```bash
1852
+ canonry apply <file...> --dry-run [--format json]
1853
+ canonry apply <file...> [--format json]
1854
+ ```
1855
+
1856
+ `--dry-run` calls the server preview route for every YAML document and reports
1857
+ field, query, competitor, schedule and notification changes. It performs no
1858
+ database writes and prints `No DB writes performed. Re-run without --dry-run to
1859
+ apply.` in text mode. JSON and JSONL emit the same `{ dryRun, files,
1860
+ changedCount, errorCount }` document. A server without the preview route exits
1861
+ with `APPLY_PREVIEW_UNSUPPORTED`; the command never falls back to apply.
1862
+
1863
+ ### Preview competitor impact
1864
+
1865
+ Add, remove/delete and curated alias set/add/remove/clear accept `--dry-run`.
1866
+ JSON and JSONL include `dryRun`, the planned changes and the server's `impact`;
1867
+ text prints before/after mention share and counts. No competitors, names or
1868
+ audit rows change. The latest stored landscape is the estimate, while a real
1869
+ change restates every stored period. Advanced scope includes all markets; the
1870
+ response names its exact runs and query class. This is not the overview card.
1871
+
1872
+ `project update <project> --competitor-auto-aliases apply --dry-run` previews
1873
+ the mode and detected-name impact. Other change flags are refused. Detection
1874
+ is a stored read; `competitor aliases detect --dry-run --apply` is invalid.
1875
+ Block/unblock, market pin writes, project identity edits, apply and discovery
1876
+ promotion are not covered. Ordinary writes keep their request count and output.
1877
+ Preview POSTs require a write-capable key; the detection GET accepts read keys.
1878
+ ### Batch curated competitors and wait for stored fields
1879
+
1880
+ `canonry competitor import <project> <file> [--wait] [--wait-timeout 120]` sends
1881
+ one append request for the whole file. A JSON file is an array of domains or
1882
+ `{ "domain": "rival.example", "aliases": ["Rival Name"] }` entries. Other files
1883
+ contain one `domain[,alias,alias]` per line; blank lines and `#` comments are
1884
+ ignored. Import only a curated set, not every cited domain. Repeating an import
1885
+ is idempotent and reports `addedCount: 0` when nothing is newly tracked.
1886
+
1887
+ `competitor add` and `competitor aliases` also accept `--wait` with a positive
1888
+ integer timeout in seconds (default 120). This waits for stored answer fields
1889
+ to be recomputed after name changes, such as `competitor add demo rival.example
1890
+ --alias "Rival Name" --wait`. A bare-domain add sets no recomputation mark.
1891
+ Answer-derived alias detection has a separate lifecycle and is not covered by
1892
+ this wait. Cloud already completes the backfill within the write request.
1893
+
1894
+ Timeout exits 2 with `OPERATION_IN_PROGRESS` and `writeCommitted: true`; read
1895
+ `project show` to inspect `answerFieldsRecompute` rather than treating the write
1896
+ as failed. Older servers that omit the field return `WAIT_STATUS_UNAVAILABLE`
1897
+ after the committed write. Without `--wait`, add timing and output are unchanged.
1898
+ 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 stores at most 100 raw samples per source per UTC hour, while
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. A standing note: it stays for as long as the WordPress source exists, so it is shown by `cnry doctor` but never sends a health alert. |
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{dm as L,dn as P,cl as E,y as u,B as x,w as v,T as M}from"./index-ahEr6Oc-.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
+ 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-BDY3-V1S.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-BDY3-V1S.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-cQyVA85P.js";import"./index-ahEr6Oc-.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};
1
+ import{j as e}from"./vendor-tanstack-ZXXkndVj.js";import{A as s}from"./AuditHistoryPanel-C5oZsvsR.js";import"./index-BDY3-V1S.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};