-
Notifications
You must be signed in to change notification settings - Fork 0
Steam Library and Drafts
Everything a signed-in user gets from the issue #199 bundle: Steam library sync, verified-owner attestation, cloud report drafts, replaced-appid detection, and the surrounding admin surface.
This section is the plain-English answer to "does Steam sign-in transfer my library?" -- referenced from the site's privacy and about pages.
- Sign-in does not transfer library or wishlist. Steam OpenID 2.0 gives us only a Steam ID, display name, and avatar URL. See Steam-Auth-Flow.
- Separate pull, kicked off on first use. Library syncs the first time a signed-in user opens the profile page (no cached row -> pull). Wishlist syncs the first time the "On wishlist" browse chip is activated. Subsequent refreshes are manual (Refresh button on the profile card).
-
Server-side call using our Steam Web API key. The pull is done via
IPlayerService/GetOwnedGamesand/orIWishlistService/GetWishlistwith?key=STEAM_API_KEY&steamid=<user's steam id>. The key is a repo secret; the browser never sees it. - Public profile visibility is required and is the only opt-out. Steam only returns data when the user's Steam profile visibility is Public. Friends-only or Private returns nothing; we cache nothing and the browse chips fall back to "All". Users who want the site to hold no library/wishlist data flip their Steam profile visibility away from Public and clear the cached row on the profile page.
-
Cached shape. Appid list + count + sync timestamp only. No purchase history, no session playtime, no wishlist prices. Rows live in
user_steam_libraryanduser_steam_wishlistunder owner-only RLS. - Removal. Manual: Clear button on the profile Library / Wishlist card. GDPR: admin_erase_user_anonymize wipes both tables.
Details of what each table holds and where the appids are used follow below.
When a user signs in with Steam, we cache their owned-games list so the browse and profile pages don't need per-view Steam Web API calls.
Table: public.user_steam_library
-
user_id(uuid, PK, referencesauth.users) -
steam_id(text) -
game_count(int) -
appids(jsonb array of numeric appids) -
synced_at(timestamptz) - RLS: owner-only select/insert/update. Anon has no access.
Sync: edge function sync-steam-library at POST /functions/v1/sync-steam-library.
- Reads the caller's JWT for
steam_id. - Calls
IPlayerService/GetOwnedGameswithinclude_appinfo=false,include_played_free_games=true,include_free_sub=true,skip_unvetted_apps=false. The last two are the difference between our count and ProtonDB's (they were previously N-4). - Upserts into
user_steam_library. - Returns
{ ok, game_count, appid_count, synced_at }.
When it runs:
- Auto-syncs on first visit to
profile.htmlwhen no cached row exists. - Manual: the profile Library card's Refresh button.
-
Profile Library card: nickname, game count, last-synced timestamp, Refresh button, and a "View my games" link that opens
app.html?filter=mineprefiltered. -
Home page bar chart (signed-in only): horizontal breakdown of tier counts across the user's owned games. Cross-references
appidswithsearch-index.jsoncolumn 2 (tier). -
Browse filter panel: new "Library" pill group with "All" and "My games". Selecting "My games" intersects the visible list with
libraryAppIdsand is included in the save/restore payload and the active-filter badge count. -
Wishlist filter chip (#266 Phase 1): a sibling "Wishlist" pill group with "All" and "On wishlist" filters the browse list to games in the user's synced Steam wishlist. Backed by the
user_steam_wishlisttable (owner-only RLS, PK onuser_id) and thesync-steam-wishlistedge function, which callsIWishlistService/GetWishlist/v1/and stores appids sorted by user priority. Loaded lazily on first chip activation viajs/app/lib/user-wishlist.js. -
Deck Verified filter chip (#266 Phase 2): pill group with Verified / Playable / Unsupported / Unknown reads the same
deck-status.jsonmap the game page uses. No new backend, lazy-loads the full map vialoadDeckStatusMap()on first non-"all" chip. Non-Steam ids (GOG, Epic) always pass through since Valve does not rate them. -
Verified owner badge: at report submit time we look up
user_steam_library.appids. If it contains the report'sapp_id, we setowner_verified=trueon the row. Report cards render a small green checkmark + "Verified owner" in the author column when true. - Submit form pill: same lookup happens before the form renders, so users see a "Verified owner - this report will be flagged as owner-verified" pill at the top of the form.
user_configs.owner_verified (boolean, default false).
Distinct from game_owned, which was unconditionally set to true for every web submission (basically a "reporter used the web form" marker, not an ownership attestation). owner_verified is only true when the library check confirms ownership.
Backfill: a single UPDATE joining user_configs with user_steam_library flips existing rows to owner_verified=true for confirmed matches. Run this whenever a user syncs their library:
UPDATE public.user_configs uc
SET owner_verified = true
FROM public.user_steam_library usl
WHERE uc.proton_pulse_user_id = usl.user_id
AND uc.owner_verified = false
AND usl.appids @> to_jsonb(uc.app_id::int);Users can save partial reports and finish them later on any signed-in device.
Table: public.user_report_drafts (composite PK on user_id, app_id).
-
form_data(jsonb): snapshot fromsnapshotFormData()injs/shared/drafts.js. Captures every named input value plus the progressive-question_formState(canInstall / canStart / canPlay / verdict / faults / tinkeringMethods). - Owner-only RLS.
Client: js/shared/drafts.js exposes getDraft, upsertDraft, deleteDraft, snapshotFormData. Submit page (js/submit/main.js) wires them:
- On page load, if a draft exists, show a "You have a saved draft for this game (from N minutes ago). Restore draft / Discard" prompt above the form.
- Save Draft button next to Submit sends
snapshotFormData(form)toupsertDraft. - On successful submit, fire-and-forget
deleteDraftso the row doesn't linger.
When Steam retires an appid and remaps to a newer one (e.g. 5488 -> 45700 for Devil May Cry 4, or the Hitman 1/2 -> World of Assassination chain), we detect it during the box-art probe and surface it across the site.
Detection: scripts/pipeline/game_images.py::_extract_replaced_by. Runs only in the delisted branch (appdetails success:false + store page redirect). Fetches https://store.steampowered.com/app/{id}/ with redirects followed. If the final URL matches /app/{newid}/ with newid != id, records replaced_by: <newid> on the game-images-cache.json entry alongside status: 'delisted'.
Search-index enrichment: enrich_search_index_with_delisted padds rows to 11 columns and writes replaced_by into column 10. Column layout is now [appId, title, tier, protondbCount, pulseCount, appType, releaseYear, delisted, adult, trend, replaced_by].
Box art inheritance: when writing game-images.json, an old appid with no own header URL falls back to the new appid's URL (or standard CDN) so the old cards keep box art.
Frontend:
- Game page renders a yellow banner: "This app has been replaced. Steam now sells this title as
<newTitle>. Old app id:<old>, new app id:<new>. New reports go to the new app automatically. Old reports on this page still apply if you're playing the original build." - REPLACED pill next to the game title (yellow, alongside DELISTED / DEMO).
- Submit Report button rewires to the new appid; a muted "Old build?" link falls back to the original.
- Browse cards get a yellow REPLACED corner tag on the thumbnail.
- Submit page renders a yellow warning banner: "Heads up: This app was replaced. Steam now sells this title as
<newTitle>. Old app id:<old>, new app id:<new>. Submitting here files a report against the original build. If you're playing the current version, submit against the new appid instead."
Box Art Manager (admin.html?tab=boxart):
- Loader fetches
game-images-cache.jsonto build aknownMissingSteamSet from entries withstatus: 'missing'or'delisted'._deriveStatusflips Steam entries in that set from the optimisticdefault_cdntomissingso the filter shows the same games users see broken (~87 in prod). - Also fetches
image_load_errorsand partitions rows bygog:/epic:prefix intoknownMissingSteam/knownMissingNonSteam._deriveStatustreats client-reported non-Steam 404s as missing for GOG/Epic (which otherwise always look "cached" because the pipeline records URLs unconditionally). - Detail page header shows
Box Art: <title> - <store> - App <appid>so admins can copy the appid without scrolling. - Refetch/probe errors render multi-line: source, HTTP status, error, attempted URL,
store redirected to(when Steam redirected the appid, e.g. 5488 -> 45700), upstream body preview.
Client-side image error reporting:
-
public.image_load_errors(app_id PK, store_type, hit_count, first_seen, last_seen). Anon read + upsert-only RLS. -
steam-img.jsfires a one-per-tab-per-appid POST after the fallback chain exhausts. Session Set gates so the same tab does not POST twice per appid.
Store API Explorer (admin.html?tab=api-explorer):
- New Steam endpoints:
steam_store_redirect,steam_current_players,steam_global_achievements,steam_news,steam_reviews,steam_community_search,steam_featured,steam_featured_categories. - Raw response toggle: swap between the parsed
datafield (default) and the full edge-function envelope (endpoint, arg, url, status, data). - Auto follow-up: when
steam_appdetailsreturnssuccess:false, an inlinesteam_store_redirectprobe runs automatically so admins see the replaced_by target without switching endpoints.
.github/workflows/retry-pages-build.yml in both mdeguzis/proton-pulse-web and mdeguzis/proton-pulse-web-staging. Fires on workflow_run completed for pages-build-deployment; if conclusion==failure and run_attempt==1 it kicks gh run rerun --failed. Caps at one retry so a genuinely broken deploy is not hammered.
The staging deploy-target repo has every other workflow disabled (they lack the secrets they need and were spam-failing every scheduled run). Only pages-build-deployment and this watcher are active there.
-
tests/steamLibraryApi.test.js- REST helpers foruser_steam_library. -
tests/homeLibraryChart.test.js-computeLibraryTierCountspure function. -
tests/authorVerifiedOwner.test.js-renderAuthorBlockbadge rendering. -
tests/draftsApi.test.js- draft REST helpers +snapshotFormData. -
tests/importResolution.test.js- static import validator (catchesimport { X } from './y.js'wherey.jsdoes not exportX). -
tests/adminBoxart.test.js- regression pins for_deriveStatussignature,game-images-cache.jsonfetch, andimage_load_errorsmerging. -
tests/test_game_images_helpers.py- four cases for_extract_replaced_by(happy redirect, same appid, homepage redirect, transport error).