[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-81371":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":13,"stars30d":13,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":16,"rankGlobal":10,"rankLanguage":10,"license":17,"archived":18,"fork":18,"defaultBranch":19,"hasWiki":20,"hasPages":20,"topics":21,"createdAt":10,"pushedAt":10,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":15,"starSnapshotCount":15,"syncStatus":33,"lastSyncTime":34,"discoverSource":35},81371,"trackerdex","milouk\u002Ftrackerdex","milouk","A bestiary of internet trackers, populated by your Pi-hole. ~19,000 entities, deterministic 16x16 RPG sprites, built on Tracker Radar + Pi-hole v6.","https:\u002F\u002Fmilouk.me\u002Fprojects\u002Ftrackerdex\u002F",null,"TypeScript",40,1,39,0,0.9,"GNU General Public License v2.0",false,"main",true,[22,23,24,25,26,27,28,29],"dns","homelab","pi-hole","privacy","react","self-hosted","trackers","typescript","2026-06-12 02:04:14","\u003Cp align=\"center\">\n  \u003Cimg src=\"public\u002Ffavicon.svg\" alt=\"\" height=\"96\">\n\u003C\u002Fp>\n\n\u003Ch1 align=\"center\">\n  Trackerdex\n  \u003Cbr>\n  \u003Csub>\u003Csup>a bestiary of internet trackers, populated by your Pi-hole\u003C\u002Fsup>\u003C\u002Fsub>\n\u003C\u002Fh1>\n\n\u003Cp align=\"center\">\n  built for \u003Ca href=\"https:\u002F\u002Fpi-hole.net\">Pi-hole\u003C\u002Fa> &nbsp;\n  \u003Ca href=\"https:\u002F\u002Fpi-hole.net\">\u003Cimg alt=\"Pi-hole\" src=\"https:\u002F\u002Fgithub.com\u002Fpi-hole\u002Fgraphics\u002Fraw\u002Fmaster\u002FVortex\u002FVortex.png\" height=\"22\">\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\u002Factions\u002Fworkflows\u002Fbuild.yml\">\u003Cimg alt=\"build\" src=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\u002Factions\u002Fworkflows\u002Fbuild.yml\u002Fbadge.svg\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fmilouk.me\u002Fprojects\u002Ftrackerdex\u002F\">\u003Cimg alt=\"pages\" src=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\u002Factions\u002Fworkflows\u002Fpages.yml\u002Fbadge.svg\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\u002Fpkgs\u002Fcontainer\u002Ftrackerdex\">\u003Cimg alt=\"ghcr\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fghcr.io-milouk%2Ftrackerdex-2496ED?logo=docker&logoColor=white\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\">\u003Cimg alt=\"stars\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fstars\u002Fmilouk\u002Ftrackerdex?style=flat&logo=github\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex\u002Fcommits\u002Fmain\">\u003Cimg alt=\"last commit\" src=\"https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Flast-commit\u002Fmilouk\u002Ftrackerdex?logo=github\">\u003C\u002Fa>\n  \u003Ca href=\"LICENSE\">\u003Cimg alt=\"license\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-GPL--2.0-blue.svg\">\u003C\u002Fa>\n  \u003Ca href=\"https:\u002F\u002Fko-fi.com\u002Fmilouk\">\u003Cimg alt=\"ko-fi\" src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fko--fi-buy_me_a_coffee-FF5E5B?logo=ko-fi&logoColor=white\">\u003C\u002Fa>\n\u003C\u002Fp>\n\n\u003Cp align=\"center\">\u003Cem>Gotta block 'em all!\u003C\u002Fem>\u003C\u002Fp>\n\n> A companion to [**Pi-hole**](https:\u002F\u002Fpi-hole.net\u002F). Every blocked DNS query\n> turns into a *catch* in your personal dex of ~19,000 internet trackers,\n> each rendered as a deterministic 16×16 RPG character. Tiers (legendary \u002F\n> rare \u002F uncommon \u002F common) reflect how widely each tracker is deployed\n> across the web. Watch your dex fill up over hours of normal browsing.\n\nLive data via the [Pi-hole v6 REST API](https:\u002F\u002Fdocs.pi-hole.net\u002Fapi\u002F).\nReal entity catalogue from\n[DuckDuckGo Tracker Radar](https:\u002F\u002Fgithub.com\u002Fduckduckgo\u002Ftracker-radar).\nSprite engine ported from [daboth\u002Fpagan](https:\u002F\u002Fgithub.com\u002Fdaboth\u002Fpagan).\n\n**🛰️ Live demo: \u003Chttps:\u002F\u002Fmilouk.me\u002Fprojects\u002Ftrackerdex\u002F>**\n\nZero-config self-host: `docker run -p 8080:80 ghcr.io\u002Fmilouk\u002Ftrackerdex:latest` — done.\n\n## What is it\n\nTrackerdex turns your Pi-hole's `\u002Fapi\u002Fqueries` and `\u002Fapi\u002Fstats\u002Ftop_domains`\nendpoints into a Pokédex-style game where the \"monsters\" are real ad\nnetworks, analytics platforms, social trackers, CDNs, and data brokers.\n\nEvery blocked DNS query is an encounter. The first time your network\nblocks a tracker we have on file, you *catch* it — its sprite is unlocked,\nits silhouette becomes a real character, and an entry joins your dex.\n\nThe dex is shared across users (the same domain → the same character for\neveryone), but your **catch state** lives only in your browser's\n`localStorage`. No accounts, no telemetry, no cloud — just you and your\nPi-hole.\n\n## Themes\n\nDark default, light optional. Toggle in the topbar.\n\n### Dark\n\n![Dark theme](docs\u002Fscreenshots\u002Fdark.png)\n\n### Light\n\n![Light theme](docs\u002Fscreenshots\u002Flight.png)\n\n### Detail · sprite overview\n\nClick any catch for the full file: signal strength bars, fake astronomical\ncoordinates, full-size sprite (with shiny variant if you've broken\n15,000 encounters), the tracker's owned domains, and a 24-hour encounter\nsparkline.\n\n![Sprite overview](docs\u002Fscreenshots\u002Fsprite-overview.png)\n\n## Quickstart\n\n### Docker — auto-connect (recommended)\n\nPass your Pi-hole password as an env var and trackerdex logs in for you on\nevery page load. The container's bundled nginx reverse-proxies `\u002Fapi\u002F*` to\nthe `pihole` container on the same network, so the browser only ever talks\nto its own origin (no CORS, no Pi-hole config changes).\n\n```bash\ndocker run -d \\\n  --name trackerdex \\\n  --network \u003Cyour-pihole-network> \\\n  -p 8080:80 \\\n  -e PIHOLE_PASSWORD=\u003Cyour-pi-hole-app-password> \\\n  ghcr.io\u002Fmilouk\u002Ftrackerdex:latest\n```\n\nOpen `http:\u002F\u002F\u003Chost>:8080` → you land directly on your dex.\n\n### Docker — manual connect\n\nSkip the env var and you'll get the connect screen on first visit; type\nyour Pi-hole URL + password, the session token is kept in `localStorage`.\n\n```bash\ndocker run -d --name trackerdex -p 8080:80 ghcr.io\u002Fmilouk\u002Ftrackerdex:latest\n```\n\n### docker-compose\n\n```yaml\nservices:\n  trackerdex:\n    image: ghcr.io\u002Fmilouk\u002Ftrackerdex:latest\n    container_name: trackerdex\n    restart: unless-stopped\n    ports:\n      - \"8080:80\"\n    env_file:\n      - .\u002Ftrackerdex.env       # PIHOLE_PASSWORD=...\n    # If your Pi-hole isn't reachable as `pihole` on the same docker\n    # network, also set:\n    #   PIHOLE_UPSTREAM=http:\u002F\u002F\u003Ccontainer-or-host>[:port]\n```\n\n`.\u002Ftrackerdex.env`:\n\n```dotenv\nPIHOLE_PASSWORD=your-pi-hole-app-password\n# Optional — only set if you need the SPA to point at a different host\n# than the one the page is served from. Leave unset for same-origin\n# (the default; works with the bundled nginx \u002Fapi\u002F* proxy).\n# PIHOLE_HOST=https:\u002F\u002Fpihole.example.com\n```\n\n### Environment variables\n\n- **`PIHOLE_PASSWORD`** — *required for auto-connect, default unset.*\n  When set, the container's entrypoint authenticates against Pi-hole\n  itself at startup and writes only the resulting session ID into\n  `\u002Fconfig.json`. The SPA picks that up and lands on the dex without\n  showing the connect screen. A background refresh loop renews the\n  session before it expires. **The password never reaches the browser.**\n\n- **`PIHOLE_HOST`** — *optional, default unset (same-origin).*\n  Override the URL the SPA hits for the API. Leave blank to use\n  same-origin (recommended; nginx proxies `\u002Fapi\u002F*` to the `pihole`\n  upstream). Set if your Pi-hole is reachable elsewhere and you'd rather\n  the browser hit it directly.\n\n- **`PIHOLE_UPSTREAM`** — *optional, default `http:\u002F\u002Fpihole`.*\n  Where the container's entrypoint sends its auth handshake. Override\n  if your Pi-hole container has a different name on the docker network.\n\n> **What's in `\u002Fconfig.json`.** Only `{ piholeHost, sid, expiresAt }`.\n> SIDs are short-lived (Pi-hole's default validity is 30 min) and bound\n> to your Pi-hole instance. If you're upgrading from v1.0.x, this is a\n> security improvement: prior versions wrote `PIHOLE_PASSWORD` directly\n> into `\u002Fconfig.json`, which leaked the password on public deploys.\n\n### From source (development)\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fmilouk\u002Ftrackerdex.git\ncd trackerdex\nnpm install\nnpm run build:dex      # downloads Tracker Radar (~5 MB dex.json)\necho 'VITE_PIHOLE_URL=http:\u002F\u002Fpihole.lan' > .env.local\nnpm run dev            # http:\u002F\u002Flocalhost:5173\n```\n\nIn dev mode, Vite proxies `\u002Fapi\u002F*` to `VITE_PIHOLE_URL` so the browser\ntreats Pi-hole as same-origin — no CORS dance.\n\n## How it works\n\n```text\n                  ┌────────────────────┐\n                  │  Tracker Radar     │  build-time fetch\n                  │  (~3.8k entities,  │  via npm run build:dex\n                  │   ~38k domains)    │\n                  └────────┬───────────┘\n                           ▼\n                  ┌────────────────────┐\n                  │   public\u002Fdex.json  │  flat domain→entity index\n                  │   ~5 MB            │  + per-entity metadata\n                  └────────┬───────────┘\n                           ▼ static fetch\n        Pi-hole v6  ───►  trackerdex SPA  ◄─── pagan-derived sprites\n        \u002Fapi\u002Fqueries        │                   (in-browser canvas)\n        \u002Fapi\u002Fstats\u002F...      ▼\n                       localStorage\n                       (catch progress)\n```\n\n1. **Build time**: pull DuckDuckGo Tracker Radar's\n   `domain_map.json` (~38k subdomains → 3.8k parent companies) and\n   `entity_prevalence.json`. Join into one flat `domain → entity` index\n   with tiered metadata. Output: `public\u002Fdex.json`.\n\n2. **First run**: the SPA `POST \u002Fapi\u002Fauth` with your Pi-hole password,\n   stashes the session ID, then bulk-seeds your catches from\n   `\u002Fapi\u002Fstats\u002Ftop_domains?blocked=true&count=1000`.\n\n3. **Live polling**: every 8 seconds, fetch the latest queries from\n   `\u002Fapi\u002Fqueries?from=\u003Csince>`, filter to blocked statuses (GRAVITY,\n   REGEX, DENYLIST, …), strip subdomains via the Public Suffix List, look\n   up the parent entity, and increment the encounter counter.\n\n4. **Sprite rendering** is pure browser code: each entity name is hashed\n   (chained 32-bit FNV-1a) to seed pagan's algorithm — body silhouette,\n   hair, clothing, weapon, optional shield + decoration — composited into\n   a 16×16 deterministic character. Same name in, same character out.\n\n## Tiers and rarity\n\nTier comes from Tracker Radar's prevalence stat — what fraction of the\ncrawled web includes this tracker. **Higher tier = more influential, not\nharder to catch.**\n\n| Tier      | Threshold | Count  | Examples                            |\n|-----------|-----------|--------|-------------------------------------|\n| LEGENDARY | ≥ 5%      | 41     | Google, Cloudflare, Meta, Adobe     |\n| RARE      | ≥ 0.5%    | 190    | Criteo, Index Exchange, MediaMath   |\n| UNCOMMON  | ≥ 0.05%   | 475    | Smaller ad-tech, regional networks  |\n| COMMON    | rest      | 18,384 | Long-tail \u002F single-site trackers    |\n\nA tracker becomes **shiny** at ≥15,000 cumulative encounters in your\nnetwork — its sprite flips to a different deterministic loadout (same for\nevery user; everyone's shiny Google looks identical).\n\n## CORS & hosting\n\nThe trackerdex container ships with an nginx reverse proxy that forwards\n`\u002Fapi\u002F*` to a Pi-hole upstream on the docker network, so the SPA only\never talks to its own origin — **no CORS configuration on Pi-hole's side\nis needed**, and `PIHOLE_PASSWORD` never has to be exposed to a different\nhost.\n\n```text\nbrowser ──► trackerdex (nginx) ──► \u002Fapi\u002F* proxy ──► pihole container\n                  ▲                                       │\n                  └─────────── same-origin ───────────────┘\n```\n\nThe default upstream is `http:\u002F\u002Fpihole\u002F` (the conventional container\nname). If your Pi-hole has a different name on the network, override\nwith `PIHOLE_UPSTREAM=http:\u002F\u002F\u003Cname>[:port]`.\n\nIf you'd rather have the browser talk to Pi-hole directly (skipping the\nproxy), set `PIHOLE_HOST=https:\u002F\u002Fpihole.example.com` — but you'll then\nneed to allowlist trackerdex's origin in Pi-hole's\n`\u002Fetc\u002Fpihole\u002Fpihole-FTL.toml` (or via Traefik\u002FCaddy headers if you\nfront-end them with a reverse proxy).\n\n> **App passwords.** Generate an app password under *Pi-hole → Settings →\n> API* rather than using your main admin password. Trackerdex's\n> `\u002Fconfig.json` (and `localStorage`) carries only that app password,\n> never your main one.\n\n## Tech stack\n\n- **Frontend**: TypeScript, React 19, Vite, no runtime UI framework\n- **Sprites**: TypeScript port of [daboth\u002Fpagan](https:\u002F\u002Fgithub.com\u002Fdaboth\u002Fpagan)\n  with all 22 `.pgn` templates — body, hair, torso, boots, 6 one-handers,\n  5 two-handers, 4 shields, shield deco\n- **Data**: [DuckDuckGo Tracker Radar](https:\u002F\u002Fgithub.com\u002Fduckduckgo\u002Ftracker-radar)\n- **Hash**: chained 32-bit FNV-1a (deterministic, sync, non-cryptographic)\n- **PSL**: [`tldts`](https:\u002F\u002Fgithub.com\u002Fremusao\u002Ftldts) for registrable-domain extraction\n- **Persistence**: `localStorage` only\n- **Deploy**: nginx + Docker (multi-arch ghcr image)\n\n## For nerds\n\nA grab bag of implementation lore for the curious.\n\n### Sprite generation\n\nEvery tracker is rendered with a TypeScript port of\n[daboth\u002Fpagan](https:\u002F\u002Fgithub.com\u002Fdaboth\u002Fpagan) (GPL-2.0). At runtime,\nfor each entity name:\n\n1. Hash the name with chained 32-bit **FNV-1a**\n   ([src\u002Futils\u002Fhash.ts](src\u002Futils\u002Fhash.ts)). Each round mixes the previous\n   output back in so successive 8-char chunks aren't correlated. We keep\n   running until we have ≥48 hex chars — that's what pagan's grinder\n   expects.\n2. Slice the hash: the first 48 chars become 8 RGB colors; chars 0–6\n   pick a \"clothing aspect\" (one of 16 combos of HAIR\u002FPANTS\u002FTOP\u002FBOOTS);\n   chars 6–12 pick a \"weapon loadout\" (one of 71 — 5 two-handers + 6\n   one-handers + 36 dual-wields + 24 weapon+shield pairs).\n3. For each chosen layer, parse the matching `.pgn` template (18×18\n   ASCII grid; `o` = fixed pixel, `+` = optional based on a hash digit\n   modulo 2). Mirror around column 8 for body parts; weapons stay\n   asymmetric. Hand-designed templates ship under\n   [src\u002Fsprite-templates\u002F](src\u002Fsprite-templates\u002F) — 22 of them total.\n4. Composite in pagan's order: **body → torso → hair → subfield →\n   boots → weapon A → weapon B → shield deco**. Each layer reads its own\n   color from the 8-color palette (palette index per layer is hard-wired\n   to match the reference).\n\nOutput: a 16×16 grid of RGB tuples or `null` (transparent), rendered to\ncanvas at the requested scale (6 in cards, 16 in detail).\n\nSprites are **cached globally** — the algorithm runs at most once per\nunique seed for the lifetime of the page. The cache is **pre-warmed on\nidle** for the top 240 entries right after `dex.json` loads, so the\nfirst scroll has no jank.\n\n### The shiny mechanic\n\nA tracker becomes shiny at **≥15,000 cumulative encounters** in your\nnetwork. The sprite seed flips from `\"Google\"` to `\"Google::shiny1\"`,\nproducing a different deterministic loadout (different aspect, weapons,\ncolors — same algorithm). Because the seed is fully deterministic,\n**everyone's shiny Google looks identical**. There's no per-user\nrandomization anywhere in the project.\n\n### Pagan algorithm parity\n\nThe TS port is faithful enough that the same entity name produces the\nsame output as the Python reference. The arithmetic in the grinder\n(`mapDecision = numDecisions \u002F (MAX+1) × (digitsum+1)` and\n`chooseFromList`'s \"return the last entry whose index is strictly less\nthan the decision\") is reproduced verbatim — including the edge case\nwhere decisions ≤ 0 return an empty list (which means \"no aspect \u002F no\nweapon\"; rare in practice with a hash chunk of `000000`, but the parity\nmatters).\n\nSpot-check sample (matches between Python ref and our TS):\n\n| Entity     | Aspect                       | Weapons              |\n|------------|------------------------------|----------------------|\n| Google     | hair + pants                 | hammer + dagger      |\n| Cloudflare | hair + pants + boots + top   | round shield + mace  |\n| Adobe      | hair + pants + boots + top   | wand (two-handed)    |\n| LiveRamp   | boots                        | shield + hammer      |\n\n### The astronomical coordinates\n\nThe detail page shows fake **RA** (Right Ascension) and **DEC**\n(Declination) numbers to sell the \"signal observatory\" aesthetic. They\naren't real positions:\n\n- `RA  = (entityName.length × 13) mod 360 °`\n- `DEC = prevalence × 90 °`\n\nPure flavor. Hover the labels for an in-app tooltip that says so.\n\n### Domain rollup\n\nPi-hole logs queries at the **subdomain** level (e.g.\n`pubads.g.doubleclick.net`). Trackerdex strips to the **registrable\ndomain** via the Public Suffix List (`tldts`), then resolves it through\na flat `domain → entityId` map shipped in `dex.json`. So\n`pubads.g.doubleclick.net` → `doubleclick.net` → `Google`. This is why\n~50 different Google subdomains all map to the same monster.\n\nThe map is built once by [scripts\u002Fbuild-dex.ts](scripts\u002Fbuild-dex.ts)\nfrom DuckDuckGo Tracker Radar:\n\n- `domain_map.json` — ~38k subdomains keyed to entity name\n- `entity_prevalence.json` — ~3.8k entities with web prevalence (% of\n  crawled web that includes them)\n\nJoined into `public\u002Fdex.json` (~5 MB raw, ~700 KB gzipped).\n\n### Tier thresholds\n\nComputed at build time from each entity's prevalence:\n\n| Tier      | ≥ Prevalence | Count   | Glyph |\n|-----------|--------------|---------|-------|\n| LEGENDARY | 5%           | 41      | ◆◆◆   |\n| RARE      | 0.5%         | 190     | ◆◆·   |\n| UNCOMMON  | 0.05%        | 475     | ◆··   |\n| COMMON    | rest         | 18,384  | ···   |\n\nHigher tier ≠ harder to catch — quite the opposite. LEGENDARY trackers\nare everywhere, so they're the easiest to catch. The naming is\n\"legendary in scale,\" not Pokémon-style legendary rarity. There's a\ntooltip on the RARITY sidebar header that says so.\n\n### Demo mode\n\nWhen you click **EXPLORE THE DEMO** on the connect screen,\n[src\u002Fdemo.ts](src\u002Fdemo.ts) builds a deterministic catch state:\n\n- 100% of legendaries, 70% rares, 30% uncommons, 4% commons (decided by\n  `FNV1a(entity.id) mod 100 \u003C tier.pct`).\n- Encounter counts span each tier's range so a few legendaries cross\n  the 15k shiny threshold.\n- First-seen and last-seen timestamps are derived from the same hash.\n\nSynthetic live events use a pre-computed weighted cumulative array\nsampled at a 4.5-second tick:\n\n```text\nLEGENDARY 8× : RARE 3× : UNCOMMON 2× : COMMON 1×\n```\n\nDemo state is in-memory only; clicking *DISCONNECT* restores your real\ncatches from `localStorage`. The `localStorage` write is also skipped\nwhile in demo mode so synthetic data can't leak into a real user's\nsaved state.\n\n### Pi-hole v6 specifics\n\n[src\u002Fpihole.ts](src\u002Fpihole.ts) speaks the v6 REST API:\n\n- `POST \u002Fapi\u002Fauth { password }` → session ID, sent on subsequent\n  requests via the `X-FTL-SID` header.\n- `GET \u002Fapi\u002Fstats\u002Ftop_domains?blocked=true&count=1000` — bulk-seeds\n  the dex on connect.\n- `GET \u002Fapi\u002Fqueries?from=\u003Csince>` polled every **8 seconds**.\n\nStatuses considered \"blocked\":\n\n```text\nGRAVITY · REGEX · DENYLIST · *_CNAME variants\nEXTERNAL_BLOCKED_IP · EXTERNAL_BLOCKED_NULL · EXTERNAL_BLOCKED_NXRA\nDBBUSY · SPECIAL_DOMAIN\n```\n\nIn dev, [vite.config.ts](vite.config.ts) proxies `\u002Fapi\u002F*` to\n`VITE_PIHOLE_URL` so the browser sees Pi-hole as same-origin — no CORS\nconfig required on the Pi-hole side.\n\n### Performance plumbing\n\nMost of this you won't notice, which is the point:\n\n- **Sprite cache** — `Map\u003Cseed, Sprite>`, no eviction (~40 KB live\n  for 19k entities).\n- **Idle pre-warm** — top 240 sprites pre-generated via\n  `requestIdleCallback` after dex loads.\n- **`\u003Clink rel=\"preload\">`** on `dex.json` so the 5 MB fetch starts\n  during JS download instead of after.\n- **`React.memo`** on Card \u002F Sidebar \u002F PanelHead with stable\n  callbacks — feed updates and catch increments don't cascade through\n  240 visible cards.\n- **Debounced `localStorage` writes** — 400 ms; bulk-seeding 1000\n  entries doesn't trigger 1000 `JSON.stringify` cycles.\n- **Marquee paused** when the tab is hidden — Page Visibility API\n  toggles `animation-play-state: paused` on the live ticker.\n- **`useLayoutEffect`** for canvas painting — no flash of empty\n  canvas during scroll-driven remounts.\n- **Render cap** — 240 cards on screen max, sort applied first; the\n  \"+N more\" placeholder card tells you to narrow the filter.\n- **Domain resolver** — exact `domainMap` hit tried first; PSL parse\n  is the fallback.\n- **Synthetic ticker** — weighted cumulative array, O(log n) per tick.\n\n### Sizes\n\n```text\nJS bundle:   343 KB raw  \u002F 115 KB gzipped\nCSS:          20 KB raw  \u002F   4 KB gzipped\nHTML:        1.0 KB raw  \u002F  ~0.5 KB gzipped\ndex.json:    5.2 MB raw  \u002F ~700 KB gzipped\n```\n\nThe dex is the heavyweight; everything else is small.\n\n### Why GPL-2.0?\n\nBecause pagan is GPL-2.0 and we ship its `.pgn` templates verbatim.\nWithout the templates the procgen wouldn't have hand-designed body\nparts; with them, the project must inherit the license. Forks must\nremain GPL-2.0 or compatible. If you need MIT-style licensing, fork\njust the non-pagan modules (`pihole.ts`, `dex.ts`, the Observatory UI,\netc.) — all of which are independently authored.\n\n## Credits\n\n- Tracker data: [DuckDuckGo Tracker Radar](https:\u002F\u002Fgithub.com\u002Fduckduckgo\u002Ftracker-radar) (Apache-2.0).\n- Sprite engine and `.pgn` templates: [daboth\u002Fpagan](https:\u002F\u002Fgithub.com\u002Fdaboth\u002Fpagan) (GPL-2.0).\n- Built on top of [Pi-hole](https:\u002F\u002Fpi-hole.net\u002F) v6.\n\nNot affiliated with The Pokémon Company. The \"dex\" framing is parody.\n\n## License\n\nGPL-2.0-or-later — see [LICENSE](.\u002FLICENSE).\n\nThis project is GPL-2.0 because it incorporates pagan's templates and\nalgorithm verbatim. Forks must remain GPL-2.0 or compatible.\n","Trackerdex 是一个基于 Pi-hole 的互联网追踪器图鉴，将你拦截的约19,000个追踪器转换为16x16像素的RPG风格角色。项目使用TypeScript编写，集成了Pi-hole v6 REST API和DuckDuckGo Tracker Radar的数据源，通过确定性算法生成独特的角色形象，并根据追踪器在网络中的普及程度将其分为传奇、稀有、不常见和常见四个等级。适合那些希望以游戏化方式了解和管理个人网络隐私安全的家庭实验室用户或自托管爱好者使用。",2,"2026-06-11 04:04:48","CREATED_QUERY"]