Replace `wait_for_url` with `wait_for_function` in both login and
submit `login_action` callbacks, racing URL change against `div.error`
appearing — same pattern as AtCoder and Codeforces. Also prefix
credential prompts with `[cp.nvim]:` for consistency.
Problem: Wrong credentials during login produced two bugs: scrapers
wrapped the `bad_credentials` error code in `"Login failed: ..."`,
causing double-prefixed messages in the UI; and `credentials.lua`
did not clear cached credentials before re-prompting or on failure,
leaving stale bad creds in the cache.
Solution: Standardize all scrapers to emit `"bad_credentials"` as
the raw error code. Add `LOGIN_ERRORS` map in `constants.lua` to
translate it to a human-readable string in both `credentials.lua`
and `submit.lua`. Fix `credentials.lua` to clear credentials on
failure in both the fresh-prompt and cached-creds-fail paths.
For AtCoder and Codeforces, replace `wait_for_url` with
`wait_for_function` to detect the login error element immediately
rather than waiting the full 10s navigation timeout. Also add
"Remember me" checkbox check on Codeforces login.
## Problem
Loading a problem whose source file already exists silently overwrites
user code. Submitting an empty file sends a blank submission to the
platform. Two ruff lint violations existed in the scrapers.
## Solution
- `setup.lua`: when the target source file exists on the filesystem
(`vim.uv.fs_stat`), show an inline `Overwrite? [y/N]:` prompt. Declining
keeps the existing file open and registers state normally. Skipped when
the file is already loaded in a buffer.
- `submit.lua`: resolve path to absolute, use `vim.uv.fs_stat` to verify
existence, abort with WARN if `stat.size == 0` ("Submit aborted: source
file has no content").
- `scrapers/atcoder.py`: remove unused `pathlib.Path` import (F401).
- `scrapers/base.py`: move local imports to top of file (E402).
Closes#364, #365.
## Problem
Scraper cookie handling was fragmented across per-platform files with no
shared access, httpx scrapers lacked `checking_login` fast paths on
login, and several re-auth edge cases (CodeChef submit, CF cookie guard,
AtCoder cookie persistence) caused unnecessary full re-logins or silent
failures.
## Solution
Centralize all cookie storage into a single `cookies.json` via helpers
in `base.py`. Add `checking_login` fast paths to `kattis.py` (using the
`x-username` response header as a session probe), `usaco.py`, and
`cses.py` login flows. Fix `kattis.py` submit to emit `checking_login`
only after loading cookies. Remove AtCoder cookie persistence from login
entirely — always do a fresh session. Harden CodeChef and CF reauth
with consistent status logging and cookie guard checks.
## Problem
On CSES, Kattis, and USACO, `:CP <platform> login` always prompted
for credentials and ran a full web login even when a valid session was
already cached. Submit also had weak stale-session detection.
## Solution
`credentials.lua` now tries cached credentials first before prompting,
delegating fast-path detection to each scraper. CSES `login()` checks
the cached API token and returns immediately if valid. USACO `login()`
and `submit()` call `_check_usaco_login()` upfront. Kattis `submit()`
emits `checking_login` consistently and also triggers re-auth on HTTP
400/403, not just on the `"Request validation failed"` text match.
The premature `Submitting...` log emitted by Lua before the scraper
started is removed — Python's own status events are sufficient.
## Problem
After the initial CodeChef implementation, submit silently swallowed
"contest not available for submission" errors, and the submit flow used
blind `wait_for_timeout` delays. AtCoder had an unnecessary 500ms settle
delay after file upload.
## Solution
Replace the initial page-load sleep in CodeChef submit with
`wait_for_selector`, replace the 3s post-click sleep with a proper
`wait_for_selector` on the result dialog, and extend the practice
fallback
check to catch both dialog variants. Remove AtCoder's
`BROWSER_SETTLE_DELAY`
and the constant from `timeouts.py`.
## Problem
CodeChef had no working login, submit, or contest list. The browser
selectors were wrong, the contest list was missing present/past
contests,
and problem/contest URLs were unset.
## Solution
Fix login and submit selectors for the Drupal-based site. Paginate
`/api/list/contests/past` to collect all 228 Starters, then expand each
parent contest into individual division entries (e.g. `START228 (Div.
4)`).
Add language IDs, correct `url`/`contest_url`/`standings_url` in
metadata,
and make `:CP <platform>` open the contest picker directly.
## Problem
Language version coverage was incomplete across all platforms, AtCoder
submit used a stale cookie fast-path that caused silent failures, and
raw
`vim.notify` calls throughout the codebase produced inconsistent or
missing `[cp.nvim]:` prefixes.
## Solution
Remove cookie persistence from AtCoder login/submit (always fresh
login),
increase the submit nav timeout to 40s, and switch to in-memory buffer
upload with the correct per-language extension from a full
`_LANGUAGE_ID_EXTENSION`
map covering all 116 AtCoder languages. Expand `LANGUAGE_VERSIONS` in
`constants.lua` with all AtCoder languages, 15 new CF languages with
full
version variants, and 50+ Kattis languages. Fix AtCoder `prolog` ID
(`6079`→`6081`, was Pony) and remove the non-existent `racket` entry.
Replace all raw `vim.notify` calls with `logger.log`. Simplify the
submit
language doc to point at `constants.lua` rather than maintaining a
static table.
## Problem
Codeforces language version IDs were wrong (pointed at old compiler
versions), `LANGUAGE_VERSIONS` was incomplete for most platforms, the
contest picker crashed when `supports_countdown` was stored as a
non-table entry, and `:CP stress` restored a broken IO view on exit.
## Solution
Correct CF `programTypeId` values and default to C++20. Add
`LANGUAGE_VERSIONS` entries for all six platforms. Guard
`get_contest_summaries` against non-table cache entries. Call
`ensure_io_view()` after stress session restore. Shorten the stress
terminal buffer name to a readable `term://stress.py` format.
## Problem
`LANGUAGE_VERSIONS` only covered cpp and python. Several platform IDs
were wrong — CodeChef used `C++ 17`/`Python 3` (correct: `C++`/`PYTH
3`), USACO listed nonexistent c++20/c++23 options, and CSES only had
C++17.
## Solution
Verify every platform's submit page and update all language ID tables.
Add java and rust entries where supported, fix incorrect CodeChef and
USACO IDs, and expand CSES `CSES_LANGUAGES` dict with
C++11/C++20/PyPy3/Java/Rust variants.
## Problem
Two gaps in the plugin: (1) contest pickers have no way to know whether
a contest supports countdown (race), so the UI can't surface that
affordance; (2) `:CP submit` hardcodes a single language ID per platform
with no way to choose C++ standard or override the platform ID.
## Solution
**Race countdown** (`4e709c8`): Add `supports_countdown` boolean to
`ContestListResult` and wire it through CSES/USACO scrapers, cache, race
module, and pickers.
**Language version selection** (`b90ac67`): Add `LANGUAGE_VERSIONS` and
`DEFAULT_VERSIONS` tables in `constants.lua`. Config gains `version` and
`submit_id` fields validated at setup time. `submit.lua` resolves the
effective config to a platform ID (priority: `submit_id` > `version` >
default). Helpdocs add `*cp-submit-language*` section. Tests cover
`LANGUAGE_IDS` completeness.
## Problem
`:CP <platform> login` short-circuited on cached cookies/tokens — if an
old session was still valid, the new credentials were never tested. The
user got "login successful" even with wrong input. USACO submit also
wasted a roundtrip on `_check_usaco_login` every time.
## Solution
Always validate credentials in the login path. Remove cookie/token
loading from `_login_headless` (AtCoder), `_login_headless_cf` (CF),
`_login_headless_codechef` (CodeChef), and `login` (CSES). For USACO
submit, replace the `_check_usaco_login` roundtrip with cookie trust +
retry-on-auth-failure (the Kattis pattern). Submit paths are unchanged —
cookie fast-paths remain for contest speed.
Closes#331
## Problem
Kattis and USACO problem tests never extracted float precision, so
epsilon problems got no tolerance. Kattis `scrape_contest_metadata`
omitted `contest_url` and `standings_url`, breaking `:CP open
contest/standings`. Kattis submit always returned success even when the
server responded with an error (e.g. "You need to join the contest").
## Solution
Call `extract_precision` on problem HTML in both scrapers and emit it in
the JSON payload. Set `contest_url` and `standings_url` on Kattis
metadata paths. After Kattis submit, check for `Submission ID:` in the
response and surface the error text if absent.
## Problem
`codeforces.py` used `curl_cffi` to bypass Cloudflare when fetching
contest problem HTML, making it unavailable in the nix python env and
requiring an extra dependency across `pyproject.toml` and `flake.nix`.
## Solution
Rewrite `_fetch_problems_html` to use scrapling `StealthySession` with
`solve_cloudflare=True`, matching the existing CF submit pattern. Extend
`needs_browser` in `scraper.lua` to route CF `metadata` and `tests`
through the FHS env on NixOS. Remove `curl-cffi` from `pyproject.toml`,
`flake.nix`, and test mocks.
## Problem
`scrape_contest_list` made O(N) requests — one per Starters number up to
~200 — to discover division sub-contests via `child_contests`. `run_one`
also fetched problem HTML via `curl_cffi` solely to extract the memory
limit, which is unavailable in the nix python env.
## Solution
Use `/api/list/contests/all` directly: filter to `^START\d+$` codes and
map to `ContestSummary` in a single request. Remove `_fetch_html_sync`,
`MEMORY_LIMIT_RE`, and `_extract_memory_limit`; hardcode `memory_mb =
256.0` and `precision = None` in `run_one`.
## Problem
`ContestSummary.display_name` defaults to `None`, which serializes to
JSON `null` → Lua `vim.NIL`. The contest picker displayed "vim.NIL" for
every entry. Additionally, `start_time` was always stored even when
null, because `vim.NIL` is truthy in Lua.
## Solution
Pass `display_name=name` explicitly in `_parse_contests_page` so JSON
never emits `null`. In `cache.lua` `set_contest_summaries`, coerce
`display_name` via a `~= vim.NIL` guard and apply the same guard before
storing `start_time`.
## Problem
Kattis and USACO login and submit were broken in multiple ways
discovered
during manual end-to-end testing. Neither platform could successfully
authenticate or submit through the plugin.
## Solution
**Kattis:** switch login from `POST /login/email` (requires CSRF fetch)
to
`POST /login` with `script=true` (200 = success, 403 = bad credentials);
remove `_check_kattis_login` entirely since Kattis blocks all GET
requests
from httpx; add submit retry on `"Request validation failed"` to handle
expired sessions; fix language ID `"C++17"` → `"C++"`.
**USACO:** fix login field `user` → `uname`; fix success check to
`code==1`;
fix submit endpoint to `submit-solution.php`, file field to
`sourcefile`,
hidden field extraction off-by-one (`group(2)` → `group(1)`); fix
`_pick_lang_option` loop order (keywords outer, options inner) so
specific
keywords like `"c++17"` match before broad ones like `"c++"`.
**`submit.lua`:** absolutize source file path via `fnamemodify(...,
':p')`
before passing to the scraper — Python is spawned with `cwd=plugin_path`
so relative paths silently fail with `FileNotFoundError`.
**Both platforms:** remove cookie fast path from `login` subcommand so
credentials are always validated, preventing stale cookies from masking
wrong credentials.
## Problem
Login and submit were stub-only for USACO, Kattis, and CodeChef, leaving
three platforms without a working solve loop.
## Solution
Three commits, one per platform:
**USACO** — httpx-based login via `login-session.php` with cookie cache.
Submit fetches the problem page and parses the form dynamically (action
URL, hidden fields, language select) before POSTing multipart with
`sub_file[]`.
**Kattis** — httpx-based login via `/login/email` (official Kattis CLI
API). Submit is a multipart POST to `/submit`; the `contest` field is
included only when `contest_id != problem_id`. `scrape_contest_list`
URL updated to filter
`kattis_original=on&kattis_recycled=off&user_created=off`.
**CodeChef** — StealthySession browser-based login and submit following
the AtCoder/CF pattern. Login checks `__NEXT_DATA__` for current user
and fills the email/password form. Submit navigates to
`/{contest_id}/submit/{problem_id}`, selects language (standard
`<select>` first, custom dropdown fallback), sets code via
Monaco/CodeMirror/textarea JS, and clicks submit. Retry-on-relogin
pattern matches existing CF behaviour.
Language IDs added to `language_ids.py` for all three platforms.
## Problem
There was no way to open a problem, contest, or standings page in the
browser from within the plugin.
## Solution
Add `contest_url` and `standings_url` to `MetadataResult` and persist
them in the cache. Add `cache.get_open_urls` to resolve all three URLs.
Wire up `:CP open [problem|contest|standings]` in `commands/init.lua`
to call `vim.ui.open`, warning when a URL is unavailable (e.g. CSES
has no standings). Closes#315.
## Problem
CSES `_web_login` used bare dict indexing on the API response, raising
an opaque `KeyError` on missing fields. `_check_token` swallowed all
exceptions as `False`, conflating transient network errors with invalid
tokens. CF persisted cookies unconditionally and silently swallowed
`_solve_turnstile` failures in `submit_action`.
## Solution
CSES API fields now use `.get()` with a descriptive `RuntimeError` on
absence. `_check_token` re-raises `httpx` network/timeout exceptions
so callers see real failures. CF cookie writes are guarded by an
`X-User-Handle` check (the CF auth cookie). `_solve_turnstile` errors
propagate to the outer error handler instead of being silenced.
## Problem
The submit path in `codeforces.py` guarded `cookie_cache.write_text` on
the presence of a `JSESSIONID` cookie. Codeforces does not use
`JSESSIONID`, so the cookie file was never written after submit,
breaking the fast-path on subsequent submits.
## Solution
Replace the name-specific guard with a non-empty check (`if
browser_cookies:`), matching the unconditional save already used in the
login path.
Closes#301.
## Problem
`:CP <platform> login` blindly caches username/password without
server-side
validation. Bad credentials are only discovered at submit time, which is
confusing and wastes a browser session.
## Solution
Wire `:CP <platform> login` through the scraper pipeline so each
platform
actually authenticates before persisting credentials. On failure, the
user
sees an error and nothing is cached.
- CSES: reuses `_check_token` (fast path) and `_web_login`; returns API
token
in `LoginResult.credentials` so subsequent submits skip re-auth.
- AtCoder/Codeforces: new `_login_headless` functions open a
StealthySession,
solve Turnstile/Cloudflare, fill the login form, and validate success by
checking for the logout link. Cookies only persist on confirmed login.
- CodeChef/Kattis/USACO: return "not yet implemented" errors.
- `scraper.lua`: generalizes submit-only guards (`needs_browser` flag)
to
cover both `submit` and `login` subcommands.
- `credentials.lua`: prompts for username/password, passes cached token
for
CSES fast path, shows ndjson status notifications, only caches on
success.
## Problem
After the initial submit hardening, two issues remained: source code was
read in Lua and piped as stdin to the scraper (unnecessary roundtrip
since
the file exists on disk), and CF's `page.fill()` timed out on the hidden
`textarea[name="source"]` because CodeMirror owns the editor state.
## Solution
Pass the source file path as a CLI arg instead — AtCoder calls
`page.set_input_files(file_path)` directly, CF reads it with
`Path(file_path).read_text()`. Fix CF source injection via
`page.evaluate()`
into the CodeMirror instance. Extract `BROWSER_SUBMIT_NAV_TIMEOUT` as a
per-platform `defaultdict` (CF defaults to 2× nav timeout). Save the
buffer
with `vim.cmd.update()` before submitting.
## Problem
AtCoder file upload always wrote a `.cpp` temp file regardless of
language. CF submit used `solve_cloudflare=True` on the submit page,
causing a spurious "No Cloudflare challenge found" error;
`_wait_for_gate_reload` in `login_action` was dead code. Stale cookies
caused silent auth failures with no recovery path. The `uv.spawn` ndjson
path for submit had no overall timeout.
## Solution
Replace AtCoder's temp file with `page.set_input_files` using an
in-memory buffer and correct extension via `_LANGUAGE_ID_EXTENSION`.
Replace CF's temp-file/fallback dance with a direct
`textarea[name="source"]` fill and set `solve_cloudflare=False` on the
submit fetch. Add a login fast-path that skips the homepage check when
cookies exist, with automatic stale-cookie recovery via `_retried` flag
on redirect-to-login detection. Remove `_wait_for_gate_reload`. Fix
`_ensure_browser` to propagate install errors. Add a 120s kill timer to
the ndjson `uv.spawn` submit path in `scraper.lua`.
## Problem
Codeforces submit was a stub. CSES submit re-ran the full login flow on
every invocation (~1.5s overhead).
## Solution
**Codeforces**: headless browser submit via StealthySession (same
pattern as AtCoder). Solves Cloudflare Turnstile on login, uploads
source via file input, caches cookies at
`~/.cache/cp-nvim/codeforces-cookies.json` so repeat submits skip login.
**CSES**: persist the API token in credentials via a `credentials`
ndjson event. Subsequent submits validate the cached token with a single
GET before falling back to full login.
Also includes a vimdoc table of contents.
## Problem
CSES submit was a stub returning "not yet implemented".
## Solution
Authenticate via web login + API token bridge (POST `/login` form, then
POST `/api/login` and confirm the auth page), submit source to
`/api/courses/problemset/submissions` with base64-encoded content, and
poll for verdict. Uses the same username/password credential model as
AtCoder — no browser dependencies needed. Tested end-to-end with a real
CSES account (verdict: `ACCEPTED`).
Also updates `scraper.lua` to pass the full ndjson event object to
`on_status` and handle `credentials` events for future platform use.
## Problem
`_submit_sync` was a 170-line nested closure with `_solve_turnstile` and
the browser-install block further nested inside it. Status events went
to
stderr, which `run_scraper()` silently discards, leaving the user with a
10–30s silent hang after credential entry. The NDJSON spawn path also
lacked stdin support, so submit had no streaming path at all.
## Solution
Extract `_TURNSTILE_JS`, `_solve_turnstile`, `_ensure_browser`, and
`_submit_headless` to module level in `atcoder.py`; status events
(`installing_browser`, `checking_login`, `logging_in`, `submitting`) now
print to stdout as NDJSON. Add stdin pipe support to the NDJSON spawn
path in `scraper.lua` and switch `M.submit` to streaming with an
`on_status` callback. Wire `on_status` in `submit.lua` to fire
`vim.notify` for each phase transition.
Problem: scrape_contest_list paginated the entire Kattis problem database
(3000+ problems) treating each as a "contest". scrape_contest_metadata
only handled single-problem access. stream_tests_for_category_async could
not fetch tests for multiple problems in a real contest.
Solution: replace the paginated problem loop with a single GET to
/contests that returns ~150 real timed contests. Add contest-aware path
to scrape_contest_metadata that fetches /contests/{id}/problems and
returns all problem slugs; fall back to single-problem path when the ID
is not a contest. Add _stream_single_problem helper and update
stream_tests_for_category_async to fan out concurrently over all contest
problem slugs before falling back to the single-problem path.
Problem: lua typecheck flagged missing start_time field on ContestSummary;
ty flagged BeautifulSoup Tag/NavigableString union on csrf_input.get(),
a 3-tuple unpack where _extract_problem_info now returns 4 values in
cses.py, and an untyped list assignment in usaco.py.
Solution: add start_time? to ContestSummary LuaDoc, guard csrf_input
with hasattr check and type: ignore, unpack precision from
_extract_problem_info in cses.py callers, and use cast() in usaco.py.
Add submit.lua that reads credentials from a local JSON store (prompting
via vim.ui.input/inputsecret on first use), reads the source file, and
delegates to scraper.submit(). Add language_ids.py with platform-to-
language-ID mappings for atcoder, codeforces, and cses.
Add KattisScraper and USACOScraper with contest list, metadata, and
test case fetching. Register kattis and usaco in PLATFORMS,
PLATFORM_DISPLAY_NAMES, and default platform configs.
Problem: problem pages contain floating-point precision requirements and
contest start timestamps that were not being extracted or stored. The
submit workflow also needed a foundation in the scraper layer.
Solution: add extract_precision() to base.py and propagate through all
scrapers into cache. Add start_time to ContestSummary and extract it
from AtCoder and Codeforces. Add SubmitResult model, abstract submit()
method, submit CLI case with get_language_id() resolution, stdin/env_extra
support in run_scraper, and a full AtCoder submit implementation; stub
the remaining platforms.