Replace credential auto-login with a Chrome-remote-debugging session capture flow
All checks were successful
Deploy / deploy (push) Successful in 38s
All checks were successful
Deploy / deploy (push) Successful in 38s
Live testing against the real site found MassPrice's login is behind hCaptcha, which silently blocks fully automated username/password login outright (the form never submits) — no selector fix could work around this. Also fixed the broken _is_logged_out() heuristic: an unauthenticated visit to /Rule/List redirects to the public marketing homepage, not a /login URL, so run_rule/list_rules were silently scraping the wrong page instead of erroring. New flow: a Session page lets the user start a headless-but-CDP-exposed Chromium instance, log in themselves via chrome://inspect (solving the captcha as a real click), then capture that browser's cookies into the same auth_state.json scheduled runs already use. Required a socat proxy in the container image, since Chromium's debug port binds loopback-only regardless of --remote-debugging-address (confirmed by live testing), unreachable through Docker's port mapping without it. All Playwright objects for this now live on one dedicated thread (automation.py's _debug_worker), since the sync API isn't safe to call across the arbitrary threads gunicorn's gthread workers hand out per request. Dashboard now shows a banner when the latest run failed specifically due to a missing/expired session, rather than just looking like an empty rule list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H6TgffxTvYhqxykAbVQg2v
This commit is contained in:
parent
cb027167a7
commit
bec399dd11
12 changed files with 473 additions and 121 deletions
82
CLAUDE.md
82
CLAUDE.md
|
|
@ -52,11 +52,12 @@ tool is confirmed working, so there's exactly one thing triggering runs.
|
|||
the real page rather than replaying a raw HTTP POST.
|
||||
- **Session persistence** — a Playwright `storage_state` (cookies +
|
||||
localStorage) is saved to `/data/auth_state.json` and reused across
|
||||
runs: the tool logs in **once**, then slides the session forward after
|
||||
every successful run, rather than hitting the login form on every
|
||||
scheduled click. A fresh login (via `MASSPRICE_USERNAME` /
|
||||
`MASSPRICE_PASSWORD`) only happens when the saved state is missing or
|
||||
has expired (detected by landing back on a login page).
|
||||
runs: scheduled runs never touch the login form at all, they just load
|
||||
this saved session. When it's missing or has expired (detected by an
|
||||
unauthenticated visit to `/Rule/List` silently redirecting to the
|
||||
public marketing homepage `/` — there's no `/login` URL to check
|
||||
against), the run fails with a clear "session expired or missing"
|
||||
error rather than attempting to log in — see below for why.
|
||||
- **`storage.py`** — tiny JSON read/write helpers (temp-file +
|
||||
`os.replace`, so a crash mid-write can't corrupt state) for
|
||||
everything under `/data`: `schedules.json`, `auth_state.json`,
|
||||
|
|
@ -66,24 +67,47 @@ tool is confirmed working, so there's exactly one thing triggering runs.
|
|||
teletext system — that design language is specific to that app, not a
|
||||
house style to reuse here.
|
||||
|
||||
## Known limitation — automated login
|
||||
## Login — why it's manual, and how it works
|
||||
|
||||
`MASSPRICE_USERNAME`/`MASSPRICE_PASSWORD` drive a fully automated login
|
||||
(the user's explicit choice over a manual hand-off flow). **If
|
||||
TCGplayer's login ever presents a CAPTCHA or a 2FA challenge, automated
|
||||
login will fail outright** — there is deliberately no fallback UI for
|
||||
this yet; a failed login just shows up as a `failure` entry in Activity
|
||||
with the error message. If this starts happening routinely, it needs a
|
||||
design change (e.g. a Chrome-remote-debugging-based manual re-auth flow),
|
||||
not a bigger retry loop.
|
||||
MassPrice's login (`store.tcgplayer.com/oauth/login`, TCGplayer's shared
|
||||
OAuth identity provider) is protected by **hCaptcha**. This was
|
||||
confirmed by live testing during development: filling in the real
|
||||
username/password and clicking Submit never navigates anywhere — the
|
||||
invisible captcha check silently never resolves for a headless/automated
|
||||
browser, and the form just refuses to submit. There is no selector fix
|
||||
for this; **fully automated login is not achievable here**, full stop.
|
||||
An earlier version of this app tried `MASSPRICE_USERNAME`/
|
||||
`MASSPRICE_PASSWORD` credential-based auto-login — removed once this was
|
||||
confirmed, since keeping dead code that can never succeed is worse than
|
||||
having none.
|
||||
|
||||
**The real logged-out MassPrice login form was never inspected** during
|
||||
development — the account was already signed in when the site was
|
||||
explored, and logging it out to look at the form would have disrupted
|
||||
the user's real, live MassPrice session. `automation._login()` uses
|
||||
resilient role/label/placeholder-based Playwright locators rather than
|
||||
brittle CSS, but **expect to need a one-time adjustment against the real
|
||||
form on first deploy** if the generic locators don't match.
|
||||
What actually works: a human only needs to pass the hCaptcha, not
|
||||
automate around it, and that's a one-time (well, periodic — however long
|
||||
the session cookie lasts) cost, not a per-run one. The **Session** page
|
||||
in the webui drives this:
|
||||
1. "Start Login Session" launches headless Chromium with its CDP debug
|
||||
port exposed (`DEBUG_PORT`, default `9222`).
|
||||
2. The user connects their own Chrome to it via `chrome://inspect`,
|
||||
which renders and accepts real input against that same running
|
||||
browser instance — no VNC/Xvfb needed, headless Chromium's debug
|
||||
protocol already supports this.
|
||||
3. The user logs in normally in that inspected tab, solving the hCaptcha
|
||||
themselves since it's a real click from a real browser.
|
||||
4. "Capture Session" saves that browser's cookies to
|
||||
`/data/auth_state.json` — the same file scheduled runs read.
|
||||
|
||||
**All of this lives on one dedicated thread** (`automation._debug_worker`),
|
||||
not arbitrary Flask request threads — Playwright's sync API isn't safe
|
||||
to call across threads, and gunicorn's `gthread` workers hand each
|
||||
request to a different one. Flask routes only ever put a command on
|
||||
`_debug_cmd_queue` and block for the result; they never touch the
|
||||
Playwright browser/context/page objects directly. An un-captured debug
|
||||
session auto-closes after 10 minutes so an open, unauthenticated CDP
|
||||
port doesn't sit exposed on the LAN indefinitely.
|
||||
|
||||
The Dashboard shows a red banner (`_session_warning()` in `app.py`) when
|
||||
the most recent run/refresh failed specifically because of a missing/
|
||||
expired session, so this doesn't just look like "no rules found."
|
||||
|
||||
## Local development
|
||||
|
||||
|
|
@ -93,13 +117,13 @@ playwright install chromium # only needed if testing automation.py directly
|
|||
python3 app.py
|
||||
```
|
||||
|
||||
Runs on port 5000 (`PORT` env to change). Without `MASSPRICE_USERNAME`/
|
||||
`MASSPRICE_PASSWORD` set and without Chromium installed, the Dashboard
|
||||
still boots — `_bootstrap()` in `app.py` best-effort-catches the initial
|
||||
rule sync, so local UI/schedule-editing work doesn't require Docker or
|
||||
real MassPrice credentials at all. `DATA_DIR` env var overrides where
|
||||
JSON state is read/written (defaults to `/data`, which won't exist
|
||||
locally — point it at a scratch directory).
|
||||
Runs on port 5000 (`PORT` env to change). Without Chromium installed and
|
||||
without a saved `auth_state.json`, the Dashboard still boots —
|
||||
`_bootstrap()` in `app.py` best-effort-catches the initial rule sync, so
|
||||
local UI/schedule-editing work doesn't require Docker or a real MassPrice
|
||||
session at all. `DATA_DIR` env var overrides where JSON state is
|
||||
read/written (defaults to `/data`, which won't exist locally — point it
|
||||
at a scratch directory).
|
||||
|
||||
**Every commit should be matched with a version bump** —
|
||||
`APP_VERSION`/`APP_VERSION_DATE` near the top of `app.py` — the same
|
||||
|
|
@ -112,6 +136,6 @@ pipeline and manual container restart steps.
|
|||
|
||||
**Never put real credentials in the repo.** `.env` and `data/` are
|
||||
gitignored and excluded from the deploy rsync precisely so
|
||||
`MASSPRICE_USERNAME`/`MASSPRICE_PASSWORD`/`LOGIN_PASSWORD` and the saved
|
||||
`LOGIN_PASSWORD`/`SECRET_KEY` and the saved
|
||||
MassPrice session (`data/auth_state.json`) have somewhere to live
|
||||
outside git.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue