Skip to content

Web dashboard at train.chasehuh.com: session-only Korail login, search, per-job reserve attempts, holds - #16

Open
chasehuh wants to merge 2 commits into
fable/railway-per-job-workersfrom
fable/web-dashboard
Open

chasehuh wants to merge 2 commits into
fable/railway-per-job-workersfrom
fable/web-dashboard

Conversation

@chasehuh

Copy link
Copy Markdown
Owner

Closes #15. Stacked on #14 (base branch fable/railway-per-job-workers); merge #14 first, then this.

What

Web dashboard, Korail-app flow, one Railway service. train/server/web.py + train/server/static/ served by the same FastAPI app as the bearer /jobs API. Mobile-first vanilla HTML/JS/CSS, Korean UI: 로그인 → 출발/도착/가는날/출발시각 조회 → 열차 목록 (특실/일반실 예약가능/매진) → 예약 시도 sheet (좌석 등급, A/B/C/D 좌석 위치, 최대 시도 시간, 조회 간격, 알림만) → 예약 시도 탭 (status, sandbox id, egress IP, log tail, 취소) → 예약 내역 탭 (미결제 holds, 결제기한, 예약 취소).

Routes: POST /web/login, GET /web/me, POST /web/logout, POST /web/search, POST|GET|DELETE /web/jobs[/:id], GET /web/holds, DELETE /web/holds/:rsv_id. Search, holds list and hold cancel are single Korail calls from the session's client; reserve attempts never run in the web process — POST /web/jobs reuses the #14 JobRunner, injecting the session credentials as create-time sandbox variables (JobRunner.create(spec, worker_env=…, owner=masked id)).

Where the password lives, and for how long (train/server/session.py)

  1. Browser form until submit (field cleared on success).
  2. Control-plane process memory only, inside the session's Korail client: idle TTL 30 min, absolute TTL 8 h, or POST /web/logout (calls Korail logout and nulls the password). Never on disk, DB, logs, job records (owner is the masked id), or the cookie (random id only).
  3. Create-time variables of each per-job sandbox VM for that sandbox's life (max_minutes + 5 min grace at most, then destroyed).

Browser security: HttpOnly; Secure; SameSite=Strict session cookie (server-side store); X-CSRF-Token required on every mutating /web/* route (token handed out at login / GET /web/me, never in a cookie); login is JSON-only; login rate limit 5 failures per 15 min per client IP and per Korail id (Korail locks memberships after 5 bad passwords; the limiter fails closed before the 5th reaches Korail); CSP default-src 'self', X-Frame-Options: DENY, Referrer-Policy: no-referrer, Cache-Control: no-store on API responses, OpenAPI/docs endpoints disabled; Korail error codes surfaced (502 Korail <code>: <msg>, 401 on expired Korail session).

Hosting: new Railway service web in project train_chasehuh_com (id f1293bf8…), Dockerfile build, PORT=8000, healthcheck /healthz. Variables documented in README. Custom domain train.chasehuh.com attached (target port 8000).

Also fixed here: ZoneInfo("Asia/Seoul") from #14 crashes on the sandbox image (no tzdata) — replaced with a fixed UTC+9 offset. The sessions= injection bug (empty store was falsy) is fixed too.

Verified (2026-09-17)

  • 38 unit tests: mocked e2e login → search → job (sandbox variables = session creds, not in the record) → worker exit 0 → reserved + destroyed → holds → cancel hold → logout (creds dropped), bad-password 401 with Korail code, rate limit 429 + Retry-After, CSRF 403, session idle expiry, cookie flags, static page + headers, bearer /jobs unchanged.
  • Preview deploy https://web-production-5c952.up.railway.app: /healthz, page served with CSP/DENY headers, /web/me 401, /jobs 401, /docs 404. One bearer monitor_only job from the deployed service booted from checkpoint train-web-preview, egress IP 152.55.176.61, polled the real timetable, hit its 2-min deadline → idle_timeout → sandbox destroyed. Uvicorn access logs show paths only.
  • All test sandboxes destroyed; the checkpoint train-web-preview is kept because the preview service points at it.

Needs verification

  • Browser login with real credentials (I never had Chase's password; web login/search/holds against live Korail was exercised only with mocks). First real login should be on the phone at the preview URL.
  • DNS: train.chasehuh.com still CNAMEs to Vercel. In Cloudflare set CNAME train → 7jqnbgvz.up.railway.app and TXT _railway-verify.train → railway-verify=c8a0241b6e4db491621a5c1682990c685495cc340f76154416c27b89e5cb7747; Railway then verifies and issues TLS (my Cloudflare tokens here are expired).
  • RAILWAY_API_TOKEN on the web service is my CLI session token, which expires ~04:27 UTC today. projectTokenCreate / apiTokenCreate return Not Authorized from the CLI session. Create a project token in the dashboard (project train_chasehuh_com → Settings → Tokens) and set it as RAILWAY_TOKEN (then delete RAILWAY_API_TOKEN).
  • Docker build locally (daemon down); the Railway build of the same Dockerfile succeeded.
  • Real reserve from the dashboard: only on a Chase-named trip.

🤖 Generated with Claude Code

dev-dooi and others added 2 commits September 17, 2026 12:27
…empts, holds

Korail-app-style mobile page served by the same FastAPI service. Credentials
live only in process memory for the session (30 min idle / 8 h absolute /
logout) and as create-time variables of each per-job sandbox. httpOnly
SameSite=Strict cookie, CSRF header on mutations, login rate limit, CSP.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

@dev-dooi must be a member of the chasehuh team on Vercel to deploy.
- Click here to add @dev-dooi to the team.
- If you initiated this build, request access.

Learn more about collaboration on Vercel and other options here.

This branch had an error being deployed

2 failed deployments
Production — 4f9fa883 Deployed Oct 1, 2026 by vercel[bot]
Preview — 4f9fa883 Deployed Sep 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants