Added
- Release update check (
NOTIFY_UPDATES) – nothing signaled when a newer release was published, so a container started once could run an outdated image indefinitely. The bot now queries GitHub's latest release at startup and at most once per day, notifying once per new version (state kept indata/update_check.json) with the release-notes link and the update command (docker compose pull && docker compose up -d). Silent when already current or running ahead of the latest release (e.g. the:devimage); a failed request never blocks the run. Enabled by default; disable withNOTIFY_UPDATES=false. NOTIFY_ALREADY_CLAIMED– the run summary reports only status changes, so a store with nothing new to report disappeared from the notification entirely (e.g. AliExpress, on days the daily check-in was already collected). The new setting, off by default likeNOTIFY_CLAIM_FAILS, restoresalready claimed/already in library/skippedentries to the summary for every store.- Unit test suite (
tests/) – covers logic with no browser or network dependency: AliExpress coin/check-in payload parsing, Epic mobile offer detection, redirect host checks, the notification summary filter, and.envparsing. Two regressions in this category were caught by these tests during development. Runs viapython -m pytest tests/ -qin under a second. - Epic mobile free-game claiming (
src/stores/epic_mobile.py, #23) – Epic's weekly Android/iOS giveaway does not appear in thefreeGamesPromotionsAPI the bot polls, so it was never claimed. A new module queries Epic's mobile storefront listing (egs-platform-service…/discover/home?platform=android|ios) and passes the URLs to the existing Epic claimer, which claims them on the same store pages used for PC titles, requiring no additional login, browser profile, or captcha exposure. Only offers with an active freeClaimentry are taken (the same catalog item also carries a full-price entry for after the promotion ends). Android and iOS are distinct SKUs and both are claimed by default; controlled byEG_MOBILEandEG_MOBILE_PLATFORMS. Since both platforms share a title, claims are labelledForetales (Android)/Foretales (iOS)in logs, the summary notification, and the database.
Fixed
lateston the GitHub container registry served v1.1 for four releases –docker-ghcr.ymlonly published on a release event, and releases here are created bygithub-actions[bot], which GitHub does not let trigger another workflow.v1.2tov1.4never reached GHCR, so the defaultdocker-compose.ymlkept pulling the June image. The tag push now triggers the build directly:v1.5publishesv1.5,latestandmain,v1.5dpublishesv1.5danddev. Three related fixes in the same workflow: a push tomainno longer rewrites thedevtag, GHCR images are built forlinux/arm64as well (Raspberry Pi), and a per-commit concurrency group stops the same code being built twice.DRYRUN=truedid not prevent GamerPower from claiming giveaways –gamerpower.pyhad no dry-run check, unlike every other store: Fanatical, Itch.io, and IndieGala giveaways were claimed for real, and Alienware ones were silently marked "already notified" and never surfaced again. All four claim paths now stop before the irreversible action, reportavailable (dry run), and leave the database untouched.SCHEDULER_HOURS=24or higher crashed the bot on startup (#22) – documented as fixed in1.4, butmain.pystill built the interval job as a cron expression (*/24), which APScheduler rejects (ValueError: the step value is higher than the total range). Replaced withIntervalTrigger(hours=…), matching the behavior already documented in the README and.env.example: the timer counts N hours from startup for any positive value. Fixed daily times (SCHEDULER_FIXED_TIMES) are unaffected and continue to support exact minutes, e.g.14:15,21:30.- Epic continued running after a failed login –
run()invoked the login step and proceeded regardless of the result, so a session that never established still visited pages and clicked into empty forms, producingfailedentries. WithNOTIFY_CLAIM_FAILSnow off by default, this made a broken login indistinguishable from a normal run._ensure_logged_in()now returns a boolean andrun()aborts with a logged error and notification on failure. It returns false only when sign-in is confirmed to have failed (automation exhausted and the manual VNC window elapsed), never on an inconclusive check. Pre-release:devimages briefly had the opposite symptom (#29): the successful automated sign-in returned nothing instead of true, so🔓 Signed in as: …was followed byAborting Epic Games flow due to login failure, on the first run of a fresh profile only. A newtests/test_style.pycatches that class of mistake. - Chrome processes leaked during GamerPower's Steam/Epic delegation – both paths closed the borrowed browser with
browser.stop(), which terminates the parent Chrome process but leaves its children running, the same leak1.4fixed elsewhere for "Failed to connect to browser" (#19). Both now useclose_browser(), which terminates the full process tree. The Epic delegation also aborts if login fails, instead of proceeding logged out. - Prime Gaming reported "Signed in as: unknown" while logged out (#13) –
1.4widenedprime.py's_is_signed_in()to accept Amazon's account-header dropdown (amazon-dropdown-header-interactable/amazon-dropdown-header-message) as proof of a session, but that header renders on Luna in the logged-out state as well. Login was skipped entirely,🔓 Signed in as: unknownwas logged, "no games available" was reported, and the bot then attempted to claim without a session._is_signed_in()now checks only the one signal that exists solely for an authenticated account, the non-empty first name inuser-dropdown-first-name-text, so a logged-out page correctly triggers the normal login flow (verified live: the logged-out page with the header dropdown returns not-signed-in; a page with a first name returns signed-in with the name). - AliExpress check-in notification omitted the day streak and tomorrow's reward – the notification reported only
claimed 50 🪙 (985 total).aliexpress.pyparsed a single response shape (the{name, value}list used bymtop.aliexpress.coin.execute, which is why the wallet balance worked), so the actual check-in responses,coin.channel.sign.listandcoin.channel.sign.execute, which carry the streak and next-day reward, were captured but logged asfields={}and discarded. A new_flatten_payload()reads both response shapes (including JSON-encoded string values);_extract_checkin_info_from_api()extracts the streak and tomorrow values, preferring the response from the collect action itself. Page-scraping is now a fallback for whatever the API does not provide, with additional Polish-language patterns added since the page renders these numbers as animated digits. Every captured check-in response is also written todata/ae_coin_api.jsonfor diagnostics. - SteamDB "Play For Free" games (e.g. ICARUS) were treated as free-to-keep –
steam.py's_parse_steamdb_html()classified a game as keepable by searching each card's HTML for the textFree to Keep. The last card on the page also captures the page's trailing FAQ ("What is Free to Keep on Steam?") in its slice, so free-weekend/free-to-play titles matched the text, were "claimed", and reported as failures. The parser now matches SteamDB's actual green badge classcat-free-to-keepinstead of matching free text (verified against a live page dump: keep games are retained; ICARUS and everycat-play-for-freecard are excluded).
Changed
- Log output overhauled: quieter by default,
DEBUG=truelimited to the bot, library internals moved toDEBUG_LIBS– a normal run logged detail that matters only while debugging: AliExpress alone emitted roughly 40🔬 Coin APIlines plus🔎 Anti-bot diagnostics,🚧 Blocked app schemes, and a running account of each login keystroke; Epic narrated every checkout click; GOG logged its login wait loop; GamerPower logged a processing and a routing line per giveaway; Prime logged internal session-recovery steps; Discord logged every message chunk sent. All of this moved todebuglevel, so the default log now reports only which store is running, sign-in status, games found, claim/skip outcomes, and warnings/errors.DEBUG=trueitself was further diluted by library internals, every WebSocket frame exchanged with Chrome, HTTP handshakes with full response headers, SQLite calls, and SQLAlchemy printing eachBEGIN/PRAGMA/COMMITtwice (its ownechohandler ran alongside the app's), a single run could reach tens of thousands of lines.main.pynow keeps those loggers at warning level unless the newDEBUG_LIBS=trueis set, soDEBUG=true(on by default) reports only the bot's own actions: browser launch parameters, database inserts/hits, which SteamDB cards were skipped and why, Prime's offer-container diagnostics (previously dead code in a comment), GamerPower's resolved redirect target, AliExpress' check-in widget polling, Epic's mobile detection, and a credential-free settings dump at startup. Also filtered: a spuriousUnknown child process pid …, will report returncode 255warning, raised by Python's asyncio child watcher noticing a Chrome process already reaped byclose_browser()'s cleanup (added in1.4). - AliExpress check-in notification format – all values were packed into a single bracket, so
claimed 50 🪙 (+12 tomorrow · 1,040 total)read as tomorrow bringing 12 additional coins rather than a total. Each value is now labelled individually:claimed 50 🪙, streak 5 days, tomorrow 12 🪙, balance 1,040 🪙; values the page does not report are omitted rather than estimated. - AliExpress streak and next-day reward now read from the check-in calendar – both values were guessed by matching field names in the captured responses, which reported the wrong number: a live capture showed the notification claiming "tomorrow 13 🪙" when the actual next-day reward was 50, the 13 having come from a day counter picked up by the page-text fallback.
coin.channel.sign.listin fact returns adailySignNodeListwith one entry per day, keyed bycalendarDayDistance(0 = today, 1 = tomorrow), each carrying its own prize and asequenceNumbercounting the streak day._extract_checkin_calendar()reads those exact fields, so the notification now reportsstreak 15 days, tomorrow 50 🪙for the same run. The streak counter is discarded if any visible past day was not signed (it is then not a streak), name-based matching is kept only as a fallback with stricter patterns, and the page-text fallback no longer accepts a bare number near the word "tomorrow" unless it is explicitly labelled as coins. - Shared title-matching helper; SteamDB dump path fixed – the loose title-comparison helper used for ownership detection existed as identical copies in
steam.pyandgamerpower.py; it now lives once inBaseClaimer. The SteamDB page dump moved from a hardcoded Linux path (/fgc/data/steamdb_dump.html, a silent no-op outside Docker) to the configured data directory, and is written only whenDEBUG=true. - Dependency updates - bumped
psutilto>=7.2.2,appriseto>=1.12.0, andtzdatato>=2026.3(Dependabot #26, #27, #25), verified with a--no-cachecontainer rebuild and a full claiming run. - GitHub Actions updates - upgraded
actions/setup-pythonusage tov7in the dependency-check workflow (Dependabot #24). NOTIFY_CLAIM_FAILSnow covers every store and defaults tofalse– previously guarded only two ad-hoc alerts (AliExpress, Epic), while unclaimable games (e.g. a free Steam DLC requiring an unowned base game, such as "The Mound: Omen of Cthulhu, Lost Explorers' Swords Pack") always appeared in the run summary regardless of store. The summary filter inmain.pynow hides everyfailed…entry from any store unlessNOTIFY_CLAIM_FAILS=trueis set.