Skip to content

Open a domain from Traffic, and score which ones have legs - #247

Merged
ralyodio merged 1 commit into
masterfrom
feat/dashboard-domain-drilldown
Sep 12, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/dashboard-domain-drilldown

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

[2] Traffic ranked properties by visitors, which is the one number on this fleet that lies — a "visitor" is any hit the tracker did not call a crawler, so the busiest row is routinely the least interesting one. This makes each row openable, and adds a score that answers the question the list was being read for.

Open a property

On Traffic, / pick a site and Enter — or a click on the row — opens it. One domain on its own:

  • traffic — pageviews, visits, humans against bots, AI referrals, human share, its share of the fleet by both denominators, a sparkline of human visits, arrival channels and top pages.
  • money — the burn prorated onto it by pageview share and by visit share (they disagree by orders of magnitude here, so picking one quietly would be a lie), ad earnings joined by project id, ad spend joined by where the campaign points, and CoinPay commission when it can be attributed.

Esc, or 2 comes back. Opening a domain fires no new request: the detail is rebuilt from the snapshot the list was drawn from, so the two can never disagree. Selection is held by name, not by row index, so a refresh or a re-sort cannot open a different site than the one highlighted.

The risk-to-viral score

score = 100 × viral × (1 − risk/2)

viral = momentum .40 + discovery .30 + humanity .20 + money .10
risk  = volatility .40 + concentration .30 + bot dependence .20 + unmonetised .10

momentum is human visits in the recent half of the window against the earlier half (flat 0.5, doubling 1.0); discovery is the share arriving via search, social, an AI assistant, an ad or a link rather than direct; humanity is humans over humans plus bots; money is revenue per 1,000 human visits against a $2 target. volatility is the coefficient of variation of the human series (scale-free, so a small site is not punished for being small) and concentration is the largest single channel's share, rescaled so an even spread is not a risk.

It is arithmetic over numbers already on the screen, never a model, and the domain screen prints the raw figure under every component. Robustness is explicit: a component with no data is dropped and its weight redistributed, never counted as a zero; a site whose stats call failed is unscored, not scored zero; ~ marks a sample under 25 human visits; nothing can return NaN or Infinity. s cycles the order (score → visitors → pageviews), --sort=score starts there, and --json carries .sites[].score with every component.

On a realistic fixture the ordering is the point: a growing, human, well-spread property scores 56 while a 99%-crawler property with declining human visits and one channel scores 11 — inverted from the visitor ranking, where the second is 10× the first.

Server

  • GET /api/tracker/v1/stats?detail=1 adds series and mix. The mix exists because who=humans filters the RPC to p_kind='human', so its bot column is zero by construction — a share taken from it would call a crawler farm 100% human. detail=1 therefore does one extra unfiltered read, skipped when the caller already asked for everything, and is off by default so crawlproof stats pays for neither. Same bearer auth, same owner scoping.
  • /api/ads/v1/earnings gains destination_url per campaign and project_id per slot — the only fields in that model that name a domain. Both already owner-filtered.

Gaps, named rather than papered over

  • Earn rail: the pool is network-wide (earn_pool / earn_ledger key on the pass payment's own ref, no project column), so there is no per-domain share to report. The screen says so where the money line would be.
  • CoinPay commission is attributable to a domain only when the merchant account has exactly one business and it is that one; otherwise the screen prints why rather than dividing fleet revenue by a guess.

Tests

tests/dashboard-score.test.ts (23), tests/dashboard-site.test.ts (18) and tests/dashboard-screens.test.ts (18, real hqtui renders + the key handler the app binds), plus the API helpers in tests/tracker-api-stats.test.ts. Full suite 2209 passed, 7 skipped, 1 file skipped; npm run typecheck clean; packages/cli/build.mjs bundles. npm run lint is broken on master (next lint was removed in Next 16 and now reads "lint" as a directory) — unchanged by this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f

The Traffic screen ranked properties by visitors, which is the one number
on this fleet that lies: a "visitor" is any hit not classified as a
crawler, so the busiest row is routinely the least interesting one. Two
things follow from that.

A property can now be opened. On Traffic, arrows pick a site and Enter —
or a click on the row — opens one domain on its own: its pageviews,
visits, humans against bots, AI referrals and arrival channels, next to
the burn prorated onto it by both denominators, its ad earnings and spend
joined exactly rather than shared out, and the commission when there is
one merchant business to attribute it to. Esc, left or 2 comes back. It
fires no new request — the whole detail is rebuilt from the snapshot the
list was drawn from, so the two can never disagree.

And each property carries a risk-to-viral score, as a column on the list
and taken apart on the domain screen:

  score = 100 × viral × (1 − risk/2)
  viral = momentum .40 + discovery .30 + humanity .20 + money .10
  risk  = volatility .40 + concentration .30 + bots .20 + unmonetised .10

Arithmetic over numbers already on the screen, never a model, and every
component prints the figure behind it. A component with no data is
dropped and its weight redistributed rather than counted as a zero; a
site whose stats call failed is not scored at all; ~ marks a sample under
25 human visits. s cycles the order, --sort=score starts there.

Humanity is the reason for the one new server field. `who=humans` filters
the RPC to p_kind='human', so its bot column is zero by construction and
a share computed from it would call a 99%-crawler property 100% human.
`GET /api/tracker/v1/stats?detail=1` therefore adds the series and a
second, unfiltered read for the mix — skipped entirely when the caller
already asked for everything, and off by default so `crawlproof stats`
pays for neither. /api/ads/v1/earnings gains destination_url per campaign
and project_id per slot, which are the only fields that name a domain.

Gaps named rather than papered over: the earn rail is a network-wide pool
with no project column, so there is no per-domain share to show; and
CoinPay commission is only attributable when the merchant account has
exactly one business.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread lib/dashboard/score.ts
label: "Bot dependence",
value: humanity === null ? null : clamp01(1 - humanity),
weight: RISK_WEIGHTS.botDependence,
detail: humanity === null ? "mix unknown" : `${pct(humanity === null ? null : 1 - humanity)} of hits are crawlers`,
@ralyodio
ralyodio marked this pull request as ready for review September 12, 2026 11:29
@ralyodio
ralyodio merged commit 77b05c6 into master Sep 12, 2026
9 of 10 checks passed
@ralyodio
ralyodio deleted the feat/dashboard-domain-drilldown branch September 12, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants