Open a domain from Traffic, and score which ones have legs - #247
Merged
Merged
Conversation
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
ThreatCrush Security Scan39 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
| 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`, |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[2] Trafficranked 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 andEnter— or a click on the row — opens it. One domain on its own:Esc,←or2comes 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
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.scycles the order (score → visitors → pageviews),--sort=scorestarts there, and--jsoncarries.sites[].scorewith 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=1addsseriesandmix. The mix exists becausewho=humansfilters the RPC top_kind='human', so its bot column is zero by construction — a share taken from it would call a crawler farm 100% human.detail=1therefore does one extra unfiltered read, skipped when the caller already asked for everything, and is off by default socrawlproof statspays for neither. Same bearer auth, same owner scoping./api/ads/v1/earningsgainsdestination_urlper campaign andproject_idper slot — the only fields in that model that name a domain. Both already owner-filtered.Gaps, named rather than papered over
earn_pool/earn_ledgerkey 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.Tests
tests/dashboard-score.test.ts(23),tests/dashboard-site.test.ts(18) andtests/dashboard-screens.test.ts(18, real hqtui renders + the key handler the app binds), plus the API helpers intests/tracker-api-stats.test.ts. Full suite 2209 passed, 7 skipped, 1 file skipped;npm run typecheckclean;packages/cli/build.mjsbundles.npm run lintis broken on master (next lintwas 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