Two independent defects in web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js, both affecting the My Games snapshot. Filed together because they live in the same file. Observed on master @ 643c8f8 (1.0.9.4).
1. The Steam client_icon CDN is never preferred
AGENTS.md:20 states the rule:
installed-apps-sync.js must prefer Wand's own client icon CDN shape https://api-cdn.wemod.com/steam_community/<steamAppId>/client_icon/96.webp whenever the matched title/game/version metadata contains a Steam AppID, regardless of install platform. […] If metadata still does not expose the icon, fall back to the rendered Wand sidebar DOM.
The code does the opposite. pickImageUrl returns the first non-null candidate in argument order (artwork.js:21-30), and in pickImageUrlForTitle the client icon sits at argument 7:
return pickImageUrl(
title?.imageUrl, // 1 ← wins whenever present
title?.iconUrl, // 2
title?.coverUrl, // 3
title?.thumbnailUrl, // 4
title?.logoUrl, // 5
title?.headerImageUrl, // 6
getSteamClientIconUrl( // 7 ← only reached if 1-6 are all empty
findSteamAppId(...steamRoots),
getInstalledAppSteamAppId(preferredApp.platform, preferredApp.sku)
),
sidebarClientIconUrl, // 8 ← ahead of game.* metadata
...
game.imageUrl, // 11
...
)
Trigger: any Steam-installed game whose catalog title carries a flat image field. The snapshot's imageUrl becomes title.imageUrl and the client icon is never selected, even though the AppID is available.
Secondary: sidebarClientIconUrl is at argument 8, ahead of game.imageUrl and friends. AGENTS.md places the scraped sidebar DOM last, but today it overrides the game's own metadata.
Suggested fix: move the getSteamClientIconUrl(...) argument to the front — it returns null when no AppID resolves, so non-Steam titles fall through unchanged — and move sidebarClientIconUrl to last.
2. An empty-but-present service slice bypasses the store fallback
// :25
if (isRecord(state.installedAppsService?.installedApps)) {
return { rawInstalledApps: state.installedAppsService.installedApps, ..., source: "service" }
}
// :48 — never reached when the service slice is {}
if (isRecord(storeState?.installedApps)) { ... source: "store" }
isRecord({}) is true. When the service instance exists but installedApps is still {} — its state before refreshApps populates it — the service branch is taken with rawInstalledApps = {}, and the store branch is skipped. Unlike catalog and installedGameVersions, rawInstalledApps has no fallback inside that branch.
Trigger: store hydrated from cache with a populated installedApps slice while the freshly-resolved service is still empty → My Games builds from an empty map and renders empty.
Impact: transient — the refreshApps hook / 15s poll heals it — so the symptom is an empty My Games on open rather than a permanent failure. Note the existing warn text at buildSnapshot ("Service resolved but installedApps is empty/undefined. Store fallback also unavailable.") already anticipates this situation.
Suggested fix: require a non-empty record for the service branch: isRecord(x) && Object.keys(x).length > 0.
Verification
Both reproduce as unit tests driven through the public buildSnapshot / resolveInstalledData contracts (no internals exported), and both fail against unfixed master:
× prefers the Steam client icon over the title's own artwork fields
Expected: "https://api-cdn.wemod.com/steam_community/1245620/client_icon/96.webp"
Received: "https://cdn.example/subnautica-cover.webp"
× reads installed apps from the store while the service slice is still empty
Expected: "store"
Received: "service"
A fix is attached; it takes the suite from 34 to 37 tests, with pnpm lint --max-warnings=0 and pnpm typecheck clean.
Two independent defects in
web-panel/bridge/scripts/default/installed-apps-sync/installed-data.js, both affecting theMy Gamessnapshot. Filed together because they live in the same file. Observed onmaster@643c8f8(1.0.9.4).1. The Steam
client_iconCDN is never preferredAGENTS.md:20states the rule:The code does the opposite.
pickImageUrlreturns the first non-null candidate in argument order (artwork.js:21-30), and inpickImageUrlForTitlethe client icon sits at argument 7:Trigger: any Steam-installed game whose catalog
titlecarries a flat image field. The snapshot'simageUrlbecomestitle.imageUrland the client icon is never selected, even though the AppID is available.Secondary:
sidebarClientIconUrlis at argument 8, ahead ofgame.imageUrland friends. AGENTS.md places the scraped sidebar DOM last, but today it overrides the game's own metadata.Suggested fix: move the
getSteamClientIconUrl(...)argument to the front — it returnsnullwhen no AppID resolves, so non-Steam titles fall through unchanged — and movesidebarClientIconUrlto last.2. An empty-but-present service slice bypasses the store fallback
isRecord({})istrue. When the service instance exists butinstalledAppsis still{}— its state beforerefreshAppspopulates it — the service branch is taken withrawInstalledApps = {}, and the store branch is skipped. UnlikecatalogandinstalledGameVersions,rawInstalledAppshas no fallback inside that branch.Trigger: store hydrated from cache with a populated
installedAppsslice while the freshly-resolved service is still empty →My Gamesbuilds from an empty map and renders empty.Impact: transient — the
refreshAppshook / 15s poll heals it — so the symptom is an emptyMy Gameson open rather than a permanent failure. Note the existing warn text atbuildSnapshot("Service resolved but installedApps is empty/undefined. Store fallback also unavailable.") already anticipates this situation.Suggested fix: require a non-empty record for the service branch:
isRecord(x) && Object.keys(x).length > 0.Verification
Both reproduce as unit tests driven through the public
buildSnapshot/resolveInstalledDatacontracts (no internals exported), and both fail against unfixedmaster:A fix is attached; it takes the suite from 34 to 37 tests, with
pnpm lint --max-warnings=0andpnpm typecheckclean.