Skip to content

feat(games): game pills show the game's app icon, never a wordmark sliver - #398

Merged
sivert-io merged 1 commit into
mainfrom
feat/game-app-icons
Sep 25, 2026
Merged

sivert-io merged 1 commit into
mainfrom
feat/game-app-icons

Conversation

@sivert-io

Copy link
Copy Markdown
Collaborator

Depends on Auto-Tournament/packs#7 (the icons and the appIcon field). The bundled snapshot here is synced from that branch; merge the two together — an instance without this PR refuses appIcon in the feed as an unknown field.

Problem

The 20 px game pills ("Popular here: League of Legends / Dota 2 / Trackmania", "Your games: Counter-Strike 2 / Rocket League / Valorant") drew the game's catalogue picture. For anything Wikidata supplied that is the wide wordmark, cropped to an unreadable sliver ("AGI GEN", "ROC LEA").

What changes

Packs carry an app icon

  • Optional appIcon in the pack schema: a relative path to a square PNG or WebP (../app-icons/<slug>.webp).
  • checkAppIcon checks the bytes, not the name: PNG or WebP (VP8, VP8L and VP8X headers read), square, 32–512 px, at most 25 KB. Refused, not fixed up, on every way in: upload ({ pack, icon, appIcon: <base64> }), community index, catalog feed, bundled snapshot.
  • Stored in game_packs.app_icon (base64) + app_icon_type (the auto-migrator adds the columns).
  • Served on GET /api/packs/:slug/app-icon (public, like the tile: the installed pack's, else the bundled snapshot's, so a popular title nothing installed runs still has one) and GET /api/catalog/packs/:slug/app-icon (admin: installed, snapshot or feed). nosniff, sandbox CSP.
  • scripts/sync-bundled-packs.mjs copies app-icons/; api/bundled-packs re-synced (30 packs at x.y.2, so seedBundledPacks updates installed ones).
  • CS2 declares appIcon: '/games/counter-strike-2-app-icon.webp' (Steam client icon).
  • Every game summary has appIconUrl.

Pills

  • GameMark draws, in order: the app icon (object-fit: cover on a square source) → the IGDB cover cropped square from the top → the initials on a stable per-game tint from the palette tokens. Never a wide logo: a cover is only used when it is on images.igdb.com. Theme radius radii.sm.
  • Used by the picker (suggestions, picked chips, search options), profile "Your games", /welcome/games picked chips, and Home's games chips. GameThumb (portrait cover thumb) removed; the wide onboarding cards keep their logos.

IGDB re-resolve (games stored before IGDB was set up keep a Wikidata wordmark for good otherwise)

  • resolveStoredGamesAgainstIgdb(): rows with no IGDB id and source wikidata/builtin, players' picks first, 60 per pass, 350 ms apart. Matches by the same slug or the same normalised name only; takes IGDB id + cover, keeps id and slug. No match → igdb_checked_at, retried after 30 days.
  • Runs after startup enrichment (not awaited) and when IGDB credentials are saved. Off in CI/test like the enrichment.

Which image each game gets (fresh install, IGDB set)

  • App icon (31): CS2 + 30 packs — Steam client icons (AoE II, Apex, CoD, Deadlock, Dota 2, EA FC, GG Strive, Halo Infinite, Marvel Rivals, MK1, OW2, PUBG, R6, Rocket League, SF6, TF2, Tekken 8), App Store (Brawl Stars, Clash Royale, Fortnite, Hearthstone, Minecraft, MLBB, TFT), Nintendo eShop square (MK8 Deluxe, Splatoon 3, Smash Ultimate), Microsoft Store (Trackmania), Epic (Valorant), Commons (osu!). Sources per file in app-icons/SOURCES.md in the packs repo.
  • IGDB cover: League of Legends, StarCraft II, Battlefield 6, Chess once the re-resolve (or the built-in enrichment) finds their exact IGDB match, and any game picked through IGDB search.
  • Initials: those same four before an IGDB match, and anything from Wikidata search when IGDB is not set up.

Checks

  • eslint .: 0 errors. Typecheck ratchet: api 21, client 52 (unchanged). docs:api regenerated.
  • New tests/api/app-icons.spec.ts: valid PNG/WebP (all three WebP header kinds) accepted; oversized, GIF, SVG, non-square, empty refused; path rules; every bundled icon passes; upload → served as image/webp and named on /api/games/playable; oversized/wrong type refused over HTTP; built-ins carry appIconUrl (null for LoL); IGDB match picking.
  • Locally (own port + Postgres): app-icons, game-packs, bundled-packs, manual-report specs — 37 passed. DOM check of the "Your games" dialog: CS2 / Rocket League / Valorant / Dota 2 / Trackmania pills are 20×20 app icons; League of Legends and a Wikidata-only game show initials.

Game icons are trademarks of their owners and are used only to identify the game.

🤖 Generated with Claude Code

…iver

The 20 px game pills ("Popular here", "Your games") drew the game's
catalogue picture, which for anything that came from Wikidata is the wide
wordmark cropped to a sliver ("AGI GEN", "ROC LEA").

Packs can now carry the game's own square app icon (`appIcon`, a PNG or
WebP beside the pack). It is checked by its bytes on every way in —
upload, community index, catalog feed, bundled snapshot: PNG or WebP,
square, 32-512 px, at most 25 KB — and stored with the pack. It is served
on GET /api/packs/:slug/app-icon (the installed pack's, else the bundled
snapshot's, so popular titles nothing installed runs still have one) and
GET /api/catalog/packs/:slug/app-icon. CS2 ships its own with the module.
Every game summary carries `appIconUrl`.

GameMark now draws, in order: the app icon, the IGDB cover cropped square
from the top, or the initials on a per-game palette tint — never a wide
logo. The picker, profile "Your games", /welcome/games and Home chips use
it; GameThumb (the portrait cover thumb) is gone.

Games saved while search ran on Wikidata kept its wordmark for good once
IGDB was set up. They are now re-resolved against IGDB in the background
(at startup and when IGDB credentials are saved; rate-limited, same slug
or same normalised name only) and gain the IGDB id and cover, keeping
their id and slug.

Bundled packs synced from Auto-Tournament/packs#7 (30 packs at x.y.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 9fef6b7 into main Sep 25, 2026
11 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant