Restyle login_page to the platform standard: brand (name + tagline)
above one centered card, "Sign in with OTM" as the full-width primary
button, and a muted Face ID/Touch ID/passkey helper line under it.
13ft has no local accounts, so there's no "Other ways to sign in"
step. The fail-closed "isn't wired to OTM yet" 503 copy and the tides
beacon are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
app/otm.py is the Python port of pickup's ticket verifier (HS256, mandatory
exp, audience check, alg only read to reject, never raises) plus a signed
session cookie and an in-memory single-use jti guard. portable.py gates every
route but PUBLIC_PATHS behind a session and fails CLOSED (503) with no secret
— it is a fetch proxy. /api/health reports the version, /changelog renders
CHANGELOG.md, the footer carries version/sign-out/Ideas, tides beacon from
TIDES_HOST/TIDES_SITE_ID. Dockerfile: OTM labels + HEALTHCHECK. 17 tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LCQmFvKshyAJsE8C3B5ET
freedium.cfd no longer resolves, and freedium-mirror.cfd has been
rewritten as a SvelteKit app that renders the post inside <main> rather
than the old <div id="main-content">. The content check therefore
rejected every mirror response, so Medium articles always fell through
to the archive fallbacks.
Accept either marker, and reject the mirror's error pages explicitly:
they return HTTP 200 with a streamed error payload and ship an empty
<article> skeleton, so <article> alone does not prove a post rendered.
Also fix an unrelated crash in process_html_document that blocked this
path (and every page with an inline script): get_text() has no "default"
keyword, so it raised TypeError.
Closes#49
Adds app/locales/ko.json with a natural Korean translation of all 25
UI strings, matching the exact key structure of en.json, and lists
ko under the Built-in locales section of the README.
Change the /status endpoint to accept the target URL via a query parameter instead of a path component. Update client-side EventSource to call /status?url=... and import urllib.parse.unquote to decode the incoming URL. Return a 400 error when the url param is missing. This avoids routing/encoding issues with encoded URLs.
Replaces the previous synchronous form-POST flow (which left users
staring at a white screen for the duration of the upstream fetch)
with an SSE-driven status page that shows real-time progress through
each stage of the bypass attempt.
Bypass pipeline:
- Direct fetch with Googlebot UA (unchanged for non-Medium URLs)
- Detect Cloudflare-style anti-bot challenge pages in the response
- Fallback 1: Freedium (freedium-mirror.cfd, then freedium.cfd) for
medium.com hosts — short-circuited before direct fetch since
Cloudflare consistently blocks the Googlebot UA from non-Google IPs
- Fallback 2: archive.org Wayback Machine snapshot
- Fallback 3: archive.today / archive.ph / archive.is / archive.li
mirrors (all four tried in sequence)
- Surface a clear error if every source fails
UI:
- Animated step list with spinner / checkmark per stage
- Progress bar and elapsed-time counter
- Honest error box with retry button instead of a blank page
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>