A Codeforces client with a polished web UI: contest/problem browser, server-rendered LaTeX (KaTeX), inline AI translation with persistent annotations, optional auto-translate (full / section / paragraph) with rate limits, statement export and image copy, a syntax-highlighted code editor with autosave, native standings and submission-history pages, an interactive stats dashboard, per-role fonts (including custom uploads), color themes and translation-annotation palettes, and an embedded Codeforces verification flow for real submissions.
- Contest start times on the homepage: Settings now offers
off,upcoming(default), andall. Upcoming contests show their local start date and time;allalso shows the start date for contests that have begun or finished. - Upcoming contests stay together: future contests are placed before historical contests and sorted by the nearest start time, while historical contests remain newest-first.
- Compact local dates: contest times use the local timezone and omit the year and leading zeroes, for example
8-17 22:35; historical contests use a compact date such as8-14. - Cleaner native submit editor: the non-interactive syntax-highlight layer no longer paints a duplicate scrollbar; editing and synchronized scrolling are unchanged.
- Reliable contest submissions: all submit flows now open Codeforces' contest-scoped
/contest/{id}/submitendpoint, which works consistently during active contests as well as after they finish. The selected problem is preserved for both native API mode and the persistent webview. - No premature My submissions jump: the app no longer treats any generic status URL as proof of success. A native submission completes only after the current attempt has filled the form, dispatched the real Codeforces submit click, and reached that contest's exact My submissions page.
- Safer verdict tracking: submission watches start at the real click time, are isolated by attempt ID, survive closing the verifier after dispatch, and ignore delayed events from canceled or superseded attempts.
- Submission URL hardening: strict parsed URL checks reject wrong contests, status pages, lookalike hosts, and query/fragment bait. Focused Bun tests cover direct Codeforces and same-origin
/cf/proxy URLs.
- Hierarchical navigation: the topbar Back『‹』button steps up the page structure level by level (problem statement → problem list → contest list), and the Home『⌂』button jumps straight to the contest list from any depth. When you open Settings or Stats from a problem, Back returns you to that problem — never to a Standings / Submit / My-subs tab you merely passed through. The bottom "Main" tab still pops the most recent content page from history.
- Statement sharing: copy a rendered statement directly to the image clipboard, save it as PNG, or open a zoomable PDF preview before saving. Image export keeps the active light/dark theme and translation-annotation palette, expands collapsed annotations, and locks page scrolling while rendering without imposing a total-pixel cap.
- Persistent zoom: the app window and each embedded Codeforces tab remember their own zoom level across restarts.
Ctrl+0,Ctrl++,Ctrl+-, andCtrl+wheelcontinue to work. - VSCode problem import: optionally send the open problem and samples to the Codeforces Judge / Competitive Companion listener from the statement toolbar. Enable it and configure the localhost port in Settings.
- More resilient sessions: login state is tied to Codeforces account cookies instead of anonymous session cookies. Temporary network or Cloudflare validation failures no longer make a valid account appear logged out.
Three runtime modes, all backed by the same Bun HTTP server (src/server.ts in dev, src/server-prod.ts in packaged builds):
| Mode | Command | What it is |
|---|---|---|
| Electron app (recommended) | bun start |
Frameless desktop window, and the only mode that can submit solutions. Embeds real CF pages via <webview> under a persistent persist:cf partition — that's where cf_clearance lives, auto-refreshed by a background Turnstile solver. |
| Launcher | bash ./bin/cfapp |
Picks an installed Chromium, opens it in --app mode with an isolated profile. No Electron dependency — but cannot submit (see below). |
| Dev | bun run dev |
Bun's HMR server on http://localhost:3000, open in any browser. Cannot submit (see below); use this for UI work. |
Submitting requires the Electron app. Codeforces guards its submit form with Cloudflare Turnstile, and a Turnstile sitekey only validates on the domains its owner registered —
codeforces.com. The Launcher and Dev modes serve CF pages fromlocalhostthrough the/cf/reverse proxy, so the widget refuses to issue a token no matter what. Both modes therefore replace the submit form with an explanation. Browsing, statements, translation, standings, and My submissions all work fine everywhere; only the submit step is affected.
bun start spawns the Electron binary directly (scripts/start-electron.ts) so it
works on a machine that has Bun but no Node. electron . still works too, but it
goes through a #!/usr/bin/env node shim and needs Node installed.
bun install
bun run dev # terminal prints the URL — browsing/translation onlyFor real CF login and to submit anything, use the Electron app:
bun startThe first time, click Login in the top bar — a CF webview opens, you log in normally, and cookies sync back to the app config dir (mode 0600 on Unix) so the server-side API calls share the session.
In Settings → Submit / standings / my submissions, choose one of two bottom-tab modes:
| Mode | Behaviour |
|---|---|
| Codeforces web pages | Embeds the original CF Submit / Standings / My submissions pages. |
| Native / API | Uses native cached pages for standings and submission history. Submission stays in the native editor, then opens only the real CF Turnstile widget in a modal. Completing verification submits automatically and opens My submissions, polling every 3 seconds until the new verdict is final. |
The selected mode is persisted in both config.json and browser localStorage. Native submit drafts, standings, and submission history survive tab switches; standings and history also have persistent cache entries for instant display after restart.
Runtime data is stored in the OS-native application directories (see src/paths.ts):
| Linux | macOS | Windows | |
|---|---|---|---|
| Config (settings, cookies, custom fonts, drafts) | ~/.config/cfapp/ |
~/Library/Application Support/cfapp/ |
%APPDATA%\cfapp\ |
| Cache (API, bundled fonts) | ~/.cache/cfapp/ |
~/Library/Caches/cfapp/ |
%LOCALAPPDATA%\cfapp\Cache\ |
Cross-platform behaviour that is already wired:
- User-Agent matches the host OS (Linux / macOS / Windows) so Cloudflare
cf_clearancematches the Electron webview + curl replay. - Frameless window — the main window is
frame: falseon all three platforms and draws its own top bar; drag that bar to move the window. There is deliberately no titlebar and no traffic-light / minimize-maximize-close buttons, so quit withCmd+Q(macOS) orAlt+F4(Linux / Windows), or use your compositor's own window controls. On macOS,Cmd+Wonly closes the window — the process stays alive and reopens a window on reactivate. - Built-in fonts (including the open “Georgia” = Gelasio under
font-family: Georgia) download into the cache dir on first run. Mirrors include jsDelivr and npmmirror for better reach in China; once cached, loads are local (/fonts/…). - Custom fonts — upload TTF / OTF / WOFF / WOFF2 in Settings → 字体; stored under
<configDir>/custom-fonts/. Body / statement / display roles can each pick a custom family. - Microsoft Georgia (real TTF, optional): auto-imported into the custom-font library when present — Windows
Fonts\georgia.ttf, macOS Supplemental, Linux msttcorefonts / Wine / Proton. Distinct from the bundled Gelasio “Georgia”. - Legacy migration — if you previously used
~/.config/cfappon macOS/Windows, data is copied into the OS-native config dir when the new dir is empty. - Packaging:
bun run build:linux/build:mac/build:win. Ship a matching Bun binary under.bun-packagedfor the target OS/arch before packaging (cross-building needs that target’s Bun from bun releases).
Dev on any platform:
bun install
bun start # Electron + Bun server — needs no Node, and can submit
# or
bun run dev # browser-only UI work — cannot submit<configDir>/config.json (see table above; on Linux that is ~/.config/cfapp/config.json):
Sensitive fields are stored in plaintext; the file is written with mode 0600. Native submission uses the logged-in Codeforces webview session and the real CF form; the official Codeforces API does not provide a solution-submission endpoint.
AI Base URL and model default to empty — fill them (or use Settings → AI 翻译) for whichever OpenAI-compatible provider you use. No vendor endpoint is pre-filled.
Settings UI fields autosave (debounced); there is no separate Save button.
Two different "languages":
| Setting | Where | Stored in | Meaning |
|---|---|---|---|
| App language | Settings → 语言 | localStorage (cfapp:lang) |
UI chrome: English / 中文 / bilingual |
| AI target language | Settings → AI 翻译 | config.json → ai.targetLang |
Language the model translates into |
| CF bottom-tab mode | Settings → Submit / standings / my submissions | config.json → cfTabsMode and localStorage (cfapp:cf-tabs-mode) |
Original CF web pages, or native/API-backed pages |
Client-only appearance (also localStorage):
| Setting | Key | Options |
|---|---|---|
| Color theme | cfapp:color-theme |
Leather Book (default), Cool Gray, Forest, Rose Gold, Violet, Obsidian |
| Translation annotation style | cfapp:tr-theme |
Amber 赭黄 (default), Ink 青墨, Indigo 靛蓝, Cinnabar 朱砂, Plum 紫藤 |
| Per-role fonts | cfapp:font-{body,statement,display} |
Built-in stacks, custom family, or explicit default |
| Homepage contest times | cfapp:contest-time-display |
off, upcoming (default), or all; local timezone, no year or leading zeroes |
| App zoom | cfapp:zoom:host |
Persisted host-window zoom factor |
| Embedded CF zoom | cfapp:zoom:frame:<tab> |
Persisted independently for each Codeforces tab |
codeforces.com sits behind Cloudflare, which fingerprints the TLS handshake (JA3). Bun's fetch — like Node's and Deno's — has a recognizable non-Chrome JA3 and gets the "Just a moment" challenge even with a valid cf_clearance cookie. curl's JA3 happens to pass.
So in src/api/cookie.ts, every request to *.codeforces.com is routed through a curl subprocess (jarFetchViaCurl) that preserves real-Chrome header casing and replays the session cookie jar. Everything else (AI translate, font downloads) goes through plain fetch.
The Electron main process (electron/main.cjs) additionally:
- pins the
persist:cfpartition's User-Agent to a Chrome major that matches the bundled Chromium, socf_clearanceis issued under a UA the server can replay; - treats only a top-level Codeforces navigation 403 as an expired edge-verification token, then purges
cf_clearanceand opens a small background window so Turnstile can re-issue it; subresource failures do not log the user out; - syncs the partition's cookies to disk so the Bun server's curl calls see the same session.
The curl/JA3 path above solves reading Codeforces from anywhere. Submitting is a separate problem, and it has no equivalent workaround.
The submit form carries a Cloudflare Turnstile widget. A Turnstile sitekey is
registered against a hostname allowlist by whoever owns it — for codeforces.com,
that's Codeforces. When the widget runs on any other origin it declines to issue a
token, and without a token the form is rejected server-side. This is the anti-
automation property Turnstile exists to provide, so there is no legitimate way
around it and this project does not attempt one.
That leaves a clean split:
- Electron loads
codeforces.comfor real inside a<webview>. The widget runs on its own domain, verification succeeds, and the app auto-submits afterwards. - Any plain browser (Launcher mode,
bun run dev, or the packaged web bundle) reaches CF through the same-origin/cf/reverse proxy onlocalhost. The origin is wrong, so verification can never complete. The Submit tab detects this (isElectronAppinsrc/web/shared.ts) and shows an explanation instead of a form that would silently fail.
Standings and My submissions are unaffected in every mode — they are read-only and go through the server-side cookie jar rather than Turnstile.
src/
server.ts Bun HTTP server (dev / HMR): REST API, fonts, translation
server-prod.ts Production entry used by packaged Electron builds
server-lib/ Shared server helpers (JSON body, config sanitize, drafts)
api.ts re-export shim (backward compat)
api/ CF + AI backend modules
html.ts barrel for statement HTML parsing
html-parts/ math (KaTeX / CF $$$$), statement, text, page-detect
translate-prompt.ts language-parameterized, injection-resistant system prompt
translate-stream.ts SSE streaming translation (throttled KaTeX, stall salvage)
ai-probe.ts list models / chat test / rate-limit probe for Settings
cookie.ts curl JA3 path + cookie jar
avatar-cache.ts proxy + disk cache for CF avatars
…
paths.ts OS-native config/cache dirs + path allowlists
custom-fonts.ts user-uploaded font library under <configDir>/custom-fonts/
config.ts config.json load/save + migrations
ac-store.ts per-handle contest AC verdict persistence
fonts.ts multi-mirror font cache under <cacheDir>/fonts/
web/
app.tsx root router + layout chrome
auto-translate.ts problem-page auto-translate orchestration
rate-limiter.ts dual limiter: request interval + rolling 60s RPM
themes.ts color themes, tr-annotation palettes, font-role metadata
chrome.tsx Topbar / BottomBar barrel
chrome/ AuthIndicator, PersistentCfFrame
pages/
SettingsPage.tsx account, language, AI, theme, fonts
settings/ AI / auto-translate / model / font / custom-font UI
ProblemPage.tsx statement + editor + translation/export UI
NativeCfTabs.tsx native submit editor, standings, cached submissions + verdict polling
problem/ selection toolbar, translation, export, VSCode import
StatsPage.tsx dashboard shell
stats/ charts, heatmap, avatar, cards
ContestsPage.tsx / ProblemsPage.tsx
styles/ base, problem, native CF tabs, stats, settings, themes
i18n.ts app UI language (en / zh / mix) + dictionary
shared.ts Route, UserMe, AppConfig, …
zoom.ts persistent host and embedded-tab zoom helpers
electron/
main.cjs app shell, CF cookie sync, Turnstile solver
preload.cjs logout IPC bridge
bin/cfapp standalone launcher (Chromium --app mode)
scripts/
auth-codeforces.ts headless CF login helper
build-web.ts bundle web UI into dist-web/
electron-path.ts locate the Electron binary without needing Node
start-electron.ts `bun start` entry — spawns Electron directly
seed-config.ts write a starter config.json (dev convenience)
bun run typecheck # tsc --noEmit
bun run test # bun test
bun run build:web # produce dist-web/ for Electron packaging
bun run build # web bundle + electron-builder (Linux)- Contest & Problem browser — paginated contest list with per-contest "x/y solved" badges synced from your full submission history in one pull; problem statements with server-rendered LaTeX (KaTeX), including Codeforces
$$$…$$$math - Inline AI translation — OpenAI-compatible endpoint translates problem text with persistent annotations; streams token-by-token with LaTeX and inline code preserved. Target language is configurable (中文 / English / 日本語 / 한국어 / … or custom); the system prompt is fully editable — the built-in one is injection-resistant so problem imperatives like "determine" or "output YES" are translated as text instead of obeyed. Base URL and model start empty until you configure them.
- Auto-translate — optional full / section / paragraph modes; manual trigger or start on open; dual rate limit (min gap between starts, and after a finish before the next start under concurrency) + rolling RPM; full-mode cards can default collapsed
- Rate-limit probe — Settings → auto-translate can fire a short burst with the current concurrency / interval / RPM; if the provider returns 429, that knobs set is too aggressive (manual check, not auto-tuning)
- Bilingual UI — English, 中文, or mixed bilingual chrome, instantly and per-client (
localStorage) - Offline-tolerant & instant — contest lists, problem statements, native standings, submission history, stats, and avatars are persisted locally and painted immediately on launch (show-cached-then-refresh), so the app stays usable when the network / VPN drops; avatars are proxied through the app's network path and byte-cached to disk
- Native CF tabs — optional native standings and My submissions pages; pages remain mounted across tab switches, use persistent caches after restart, and show standard verdict abbreviations (
AC,WA,TLE,MLE,RE,CE, …) - Code editor & submission — syntax-highlighted autosaving editor. Native mode opens the correct
/contest/{id}/submitform, extracts only Turnstile into a centered modal, injects the selected problem/language/source, auto-submits after verification, then polls the new submission every 3 seconds through queue/testing to the final verdict - Statement export — copy a statement as an image without saving, save a full-resolution PNG, or preview a vector PDF with adjustable zoom. Exports preserve the current light/dark theme and translation-annotation colors.
- VSCode integration — send the current problem, limits, URL, and samples to a local Codeforces Judge / Competitive Companion-compatible extension from the problem page.
- Persistent zoom — host UI and embedded Codeforces tabs retain independent zoom levels between launches.
- Stats dashboard — interactive rating history (tier bands, hover tooltips, drag-to-zoom + time-range presets), submission heatmap, verdict/language distribution, pure CSS + inline SVG (zero chart deps). Solve data is scoped per handle.
- Fonts & themes — body / statement / display roles from a multi-mirror local font cache; optional custom font library; six color themes; five translation-annotation palettes (赭黄 / 青墨 / 靛蓝 / 朱砂 / 紫藤) via
data-*flips - Cloudflare/session handling — curl-based JA3 path for server requests, persistent Electron
persist:cfcookies, login-cookie fingerprinting that survives transient verification failures, and an in-app real Turnstile verification modal for submissions - Cross-platform — Linux / macOS / Windows data dirs, UA, packaging targets, and optional Microsoft Georgia import
- Modular codebase — large surfaces split into focused modules under
api/,web/pages/,server-lib/, with thin barrels for stable import paths
{ "handle": "your_handle", // for rankings / my-status "apiKey": "...", // CF API key (settings → API) "apiSecret": "...", "password": "...", // only needed for web-form submit fallback "proxy": "http://127.0.0.1:7890", // optional, applies to CF + AI calls "verifySsl": true, "cfTabsMode": "webview", // webview | api (native standings/submissions + verification modal) "vscodeJudge": { // optional Competitive Companion-compatible listener "enabled": false, "port": 27121 // Codeforces Judge / CPH default port }, "ai": { // OpenAI-compatible endpoint for translation "baseUrl": "", // e.g. https://api.example.com/v1 (empty by default) "apiKey": "", "model": "", // empty until you pick one in Settings "targetLang": "中文", // language AI translation renders into (preset or custom) "promptTemplate": "", // system prompt; {lang}/{source_text} substituted (blank = built-in) "stream": true, // stream translation token-by-token // Auto-translate (problem page). "off" keeps manual selection-only behaviour. "autoMode": "off", // off | full | section | paragraph "autoTrigger": "manual", // manual button | onopen (start when the statement loads) "rpm": 5, // rolling 60s start budget; 0 = unlimited "concurrency": 2, // max in-flight translation requests "requestIntervalMs": 200, // min gap between request *starts*; also enforced after a finish before the next start "autoCollapse": false // full-mode: collapse translation cards by default } }