Skip to content

Expand catalogue 140 → 5,162 + harden validator + Pages polish - #2

Merged
nschmeller merged 5 commits into
mainfrom
claude/expand-th18-catalogue-acno3
Apr 27, 2026
Merged

nschmeller merged 5 commits into
mainfrom
claude/expand-th18-catalogue-acno3

Conversation

@nschmeller

Copy link
Copy Markdown
Owner

Summary

Branch grew the catalogue from 140 → 5,162 entries across TH4–TH18, hardened the validator with cryptographic-shape checks + client-style probes, and added a few site-quality wins (pagination, favicon, repo footer link).

Catalogue

  • 140 → 5,162 entries across TH4–TH18 (TH18: 4 → 415).
  • Polite throttled crawl of three reachable catalogue sites (cocbases 3,267 / basemelon 1,834 / blueprintcoc 376) plus the existing GitHub-sourced seeds (~140). Zero overlap between the three sites.
  • Every entry attributes its source in builder and links to a per-base preview image. 393 imageless entries removed and 62 fail-validation entries pruned (image-URL duplicates, broken oneclash CDN paths, Flutter relative-path assets).
  • Categories collapsed 11 → 7 canonical types (War / Trophy / Farm / Hybrid / Progress / Fun / Home Village).

Validator (scripts/validate-bases.py)

Per-entry errors (fatal):

  • E1–E4 URL shape, payload bytes, slot ∈ {1,2,3}, TH-vs-URL match (existing).
  • E5 collection index < 1,000 (real ids: 0–93 observed).
  • E6 HMAC-tag Shannon entropy ≥ 2.5 bits/byte.
  • E7 reject pathological tags (all-zero, all-one-byte, monotonic, all-printable).
  • E8 HMAC-tag uniqueness across the catalogue (cryptographically impossible from real Supercell signatures — verified zero across 5,162).
  • E9 / E10 required fields, id/link uniqueness.
  • E11 type whitelist.
  • E12 image required + YouTube/TikTok/Instagram thumbnail blacklist.
  • E13 image URL unique across the catalogue.
  • E14 normalised-link uniqueness (lang-prefix and trailing-slash insensitive).

Catalogue-wide warnings (W1–W3) for lang-prefix whitelist, builder/id-slug agreement, image-host vs source agreement.

Client-style liveness (--liveness):

  • L1 final HTTP status is 2xx.
  • L2 redirect chain preserves TH<n>%3A<v>%3A<blob> verbatim.
  • L3 response body is the genuine Supercell Next.js share-app (contains both clashofclans:// and Clash of Clans).
  • Auto-retries non-en URLs with en/ to dodge Supercell's broken Polish-locale stub.

Image probe (--check-images):

  • L4 Range-fetch first 1024 bytes; verify PNG/JPEG/WebP/GIF/SVG magic bytes; extract dimensions; reject anything below 200×200 (placeholders, tracker pixels).

Performance + ergonomics:

  • Two-pool parallel runner (liveness + image-check on separate threads with their own 16-worker host budgets).
  • --diff-against PATH: only run per-entry checks on entries new in HEAD; catalogue-wide uniqueness still scans the full file.
  • Progress logging every 100 entries / 2 seconds on stderr.

CI (.github/workflows/validate-bases.yml)

  • Triggers only when bases.json changes (PRs and pushes to main).
  • Materialises baseline from target branch via git show <ref>:bases.json.
  • Runs --diff-against + --liveness + --check-images + --strict-liveness.
  • Typical PR run: ~2 seconds. Full local audit: ~6 minutes (Cloudfront body downloads dominate).

The existing deploy.yml workflow (push-to-main → GH Pages) was also tightened: dropped the --liveness flag from CI because the share endpoint serves byte-identical responses for real and fake ids in 2026, and the probe alone would take >1 hour serially on 5k entries.

Site (Rust/Yew/WASM)

  • Pagination (60 cards/page + "Show more") — without this the default tab would render 535 cards on first paint.
  • Rc<Vec<Base>> to stop deep-copying the catalogue on every state-handle clone.
  • use_memo on derived collections (per-TH counts, type list).
  • Hoisted search.trim().to_lowercase() out of the per-base filter.
  • SVG favicon (orange-gradient crenellated wall).
  • Footer links to the GitHub repo.
  • Cargo.toml cleanup — removed unused serde_json, gloo-utils, js-sys, and unused web-sys features.

What we can't do

None of the L1-L4 probes prove a layout id is cryptographically valid — that requires Supercell's private HMAC key, which only the in-app handler verifies. Both link.clashofclans.com and api.clashofclans.com/v1/ return identical responses for real and fake ids (verified empirically). Structural E1-E14 + curation by upstream sources is the strongest programmatic confidence the catalogue can offer; the validator's docstring says so.

Test plan

  • python3 scripts/validate-bases.py bases.jsonOK: 5162 base(s) validated structurally.
  • python3 scripts/validate-bases.py bases.json --liveness --check-imagesLiveness: 5162 alive / 0 stale. Images: 5162 ok / 0 bad.
  • cargo check → no warnings.
  • validate-bases.yml workflow exercises on this PR.
  • deploy.yml workflow rebuilds + deploys to Pages on merge.

https://claude.ai/code/session_01HWFs7faLdxZP3sTpwbGeVz


Generated by Claude Code

@nschmeller
nschmeller force-pushed the claude/expand-th18-catalogue-acno3 branch from 824d660 to 712cc9a Compare April 27, 2026 02:09
claude added 5 commits April 27, 2026 02:31
Per-entry errors (fatal)
  E1-E4   URL shape, payload bytes, slot ∈ {1,2,3}, TH-vs-URL match.
  E5      collection index < 1_000 (real ids observed: 0..93).
  E6      HMAC-tag Shannon entropy ≥ 2.5 bits/byte (real HMAC output
          is ~3.8-4.0; very low entropy = structured input).
  E7      reject pathological tags (all-zero, all-one-byte, monotonic
          ramp, ≥15/16 printable-ASCII bytes -- P ≈ 1e-6 from random).
  E8      HMAC-tag uniqueness across the catalogue.  A collision is
          cryptographically impossible from real Supercell signatures.
  E9      required fields present and well-typed.
  E10     `id` and `link` unique across the catalogue.
  E11     `type` ∈ {War, Trophy, Farm, Hybrid, Progress, Fun,
          Home Village} -- 7-category whitelist.
  E12     `image` required + YouTube/TikTok/Instagram thumbnail
          blacklist (those host video frames or social-share cards,
          not a base preview).
  E13     `image` URL unique across the catalogue (was W4; promoted
          because two cards on the same screenshot are misleading).
  E14     normalised-link uniqueness (lang-prefix and trailing-slash
          insensitive) -- catches the same blob committed twice with
          `/en?` vs `/en/?`, which E10's exact-string check misses.

Per-entry warnings (--strict promotes to errors)
  W1  lang prefix on observed Supercell whitelist.
  W2  auto-generated id slug matches the upstream id encoded in
      `builder` (cocbases layout id, basemelon design #).
  W3  builder hostname matches the image-host association table.

Client-style liveness (--liveness, parallel)
  L1  final HTTP status is 2xx.
  L2  redirect chain preserves `TH<n>%3A<v>%3A<blob>` verbatim
      (catches DNS hijacks that strip the query, bad rewriters).
  L3  body contains both `clashofclans://` and `Clash of Clans` --
      Supercell Next.js share-app fingerprint; a captive portal /
      parked-domain page / DNS hijack will be missing one.
  Auto-retries non-en URLs with `en/` to dodge Supercell's broken
  Polish-locale stub.

Image probe (--check-images, parallel)
  L4  Range-fetch first 1024 bytes; verify PNG/JPEG/WebP/GIF/SVG
      magic bytes; extract dimensions; reject anything below 200x200
      (placeholders, tracker pixels).

Performance + ergonomics
  - Two-pool runner: liveness and image-check spawn as concurrent
    threads with their own 16-worker host budgets, so a combined
    pass takes max(t_live, t_img) instead of the sum.
  - Progress logging every 100 entries / 2 seconds on stderr.
  - --diff-against PATH: per-entry checks only run on entries new
    in HEAD; catalogue-wide uniqueness still scans the full file.
    Used by CI to keep PR latency at a few seconds even as the
    catalogue grows.
  - decode_link_payload(): fast tag-only decoder used for the
    catalogue-wide E8 check on entries that are out of scope --
    skips the per-entry validation work we'd discard anyway.

scripts/add-base.sh -- bound-check town_hall to 1..20 in-tool so
contributors get immediate feedback; the validator caught it later
but earlier is friendlier.

NB: none of L1-L4 prove a layout id is *cryptographically valid* --
that requires Supercell's private HMAC key, which only the in-app
handler verifies.  Both link.clashofclans.com and
api.clashofclans.com/v1/ return identical responses for real and fake
ids (verified empirically).  Structural E1-E14 + curation by upstream
sources is the strongest programmatic confidence we can offer.
Two workflows:

  validate-bases.yml -- reusable, workflow_call only.  Materialises
    the previous bases.json from HEAD~1 (the synthetic merge commit's
    target-branch parent on PRs; the previous main commit on push)
    and runs the validator with --diff-against + --liveness +
    --check-images + --strict-liveness.  Per-entry checks (E1-E13,
    W1-W3, L1-L4) only run on entries new in HEAD; catalogue-wide
    uniqueness (E8/E10/E14) still scans the full file.

  deploy.yml -- triggers on pull_request, push to main, and
    workflow_dispatch.  Three jobs:

      validate-bases  -- workflow_call into the reusable validator.
      build           -- depends on validate-bases.  Trunk-builds
                         the WASM bundle and uploads the Pages
                         artifact.
      deploy          -- depends on build.  Gated by
                         `if: github.ref == 'refs/heads/main'` so
                         a workflow_dispatch from a feature branch
                         doesn't accidentally publish half-finished
                         work.

  Concurrency is per-ref with cancel-in-progress only on PRs, so
  rapid PR commits supersede each other but main pushes never cancel
  one another.

  Python is pinned to 3.14.

Net effect: PRs run validate + build (so contributors see the same
checks main runs); only the main branch ever publishes to Pages.
A push that introduces a structurally-bad layout link, a duplicate
image, or a broken/non-base image fails fast before any Pages
artifact is built.

Typical PR run: ~2 seconds (only new entries get the L1-L4 probes).
Full audit on the 5_000-entry catalogue: ~6 minutes (Cloudfront
body downloads dominate).
22 with duplicate image URLs (E13)
  cocbases occasionally serves the same generic preview for several
  layout ids; blueprintcoc article-link entries share the article
  hero image.  We keep the first occurrence and drop the rest.

40 with broken images (L4)
  - 34 cocbases entries pointing at media.oneclash.com URLs that
    return HTTP 403 due to triple-encoded path segments
    (e.g. `Clash%252520of%252520Clans_2023-09-05...`).
  - 6 entries (chiraggamer7, gaming-with-akhil) using Flutter
    relative-path assets like `Image/Townhall 16/b12.png` that
    are not http(s) URLs and don't render in the browser.

Catalogue: 5_224 → 5_162.  All remaining entries pass:
  structural E1-E14, L1-L3 share-link liveness (with en/ retry
  for the broken pl/ Supercell locale), L4 image probe (HTTP 2xx
  + valid magic bytes + ≥200x200 dimensions).
src/main.rs
  - pagination (PAGE_SIZE=60 + "Show more"). The largest TH tab
    has 535 cards; without this the default tab would render all
    of them on first paint.
  - bases wrapped in Rc so cloning the state handle each render
    doesn't deep-copy a 5_000-entry vector.
  - per-TH counts and the type list memoised with use_memo so
    search keystrokes don't re-walk the catalogue.
  - search.trim().to_lowercase() hoisted out of the per-base
    filter closure.
  - aria-label on search input + type select (placeholder isn't
    a label).  aria-live="polite" on the result count and copy
    button so screen readers announce updates.  Per-card
    aria-label on Open / Copy actions.
  - autocomplete="off" + spellcheck="false" on search (instant-
    filter convention; builder names like "KLAWKLA" aren't
    dictionary words and would get red squiggles).

favicon.svg + index.html
  - Tiny inline SVG (orange-gradient crenellated wall).  index.html
    declares it via <link rel="icon" type="image/svg+xml"> and Trunk
    copies it into dist/.
  - Open Graph metadata for rich previews in Slack / Discord /
    iMessage / WhatsApp / LinkedIn / Bluesky / Mastodon / GitHub
    PR comments / etc.
  - <meta name="theme-color"> matches the accent gradient.
  - <noscript> fallback so JS-disabled visitors don't see a blank
    page.
  - <title> is no longer just "Clash Bases".

styles.css
  - .load-more-wrap to host the pagination button.
  - Footer link styling (blends with the existing palette).
  - :focus-visible ring on tabs / buttons / footer link for
    keyboard users.
  - prefers-reduced-motion: drops hover-zoom and slide transforms
    for users who've opted out at the OS level.

Cargo.toml
  - rust-version bumped 1.85 → 1.95.
  - Removed unused deps (serde_json, gloo-utils, js-sys) and unused
    web-sys features (Location, UrlSearchParams).
  - strip = "symbols" in [profile.release] to shave WASM bytes.
README.md
  - Catalogue counts updated (5_617 → 5_162 after the prune;
    per-TH histogram regenerated from current bases.json).
  - Removed the per-source breakdown table -- it goes stale fast,
    the validator's stdout is the source of truth.
  - Field reference: `image` is now required (E12); `town_hall`
    is 1..20; `type` is the 7-element whitelist (was a non-
    exhaustive list including stale categories like "CWL" and
    "Builder Hall").
  - Rust prerequisite bumped 1.85 → 1.95 to match Cargo.toml.

.gitignore
  - Replace the narrow `scripts/__pycache__/` with the standard
    Python catchall `__pycache__/` + `*.py[co]`.
@nschmeller
nschmeller force-pushed the claude/expand-th18-catalogue-acno3 branch from e4aee96 to 6040e7d Compare April 27, 2026 02:32
@nschmeller
nschmeller merged commit 83784bf into main Apr 27, 2026
3 checks passed
@nschmeller
nschmeller deleted the claude/expand-th18-catalogue-acno3 branch April 27, 2026 02:33
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