AtCoder List の構成・設計・運用の詳細です。利用者向けの説明は README と 使い方ページ を見てください。
AtCoder の「解きたい問題」「復習したい問題」をストックしておける問題管理ツール。
| 役割 | 技術 | 備考 |
|---|---|---|
| ホスティング | Cloudflare Workers(Static Assets) | 無料枠: 10 万リクエスト/日。スリープしない |
| DB | Cloudflare D1(SQLite) | 無料枠: 5GB / 500 万行読み取り/日。放置しても停止されない |
| API | Hono | worker/ |
| フロント | React 19 + Vite | src/。Worker と同じオリジンから配信 |
| 認証 | ユーザー名 + パスワード(PBKDF2 + ペッパー)、セッションは D1 | worker/auth.ts, worker/password.ts |
worker/ Hono API(/api/*, /auth/*)
src/ React SPA
shared/ フロント・Worker 共通の型 / 定数 / バリデーション
migrations/ D1 スキーマ
- 無料かつ放置しても止まらないことを最優先。Supabase / Neon は無料枠でも非アクティブ時に停止・休止があり、Render 無料枠はスリープする。Cloudflare は Workers + D1 ともに常時無料で動く。
- Workers 無料枠は CPU 時間 10ms/リクエスト。bcrypt はネイティブ実装が使えず重すぎるので、WebCrypto の PBKDF2-SHA256 を使う。
反復回数は 2 万回に抑えている(5 万回は M 系 Mac で約 6ms だったが、本番の Workers では CPU 11〜28ms と上限を超えた)。その分を以下で補っている:
- ペッパー: Worker の Secret
PASSWORD_PEPPERを HMAC 鍵としてパスワードに混ぜてから PBKDF2 にかける。DB だけ漏れても総当たりできない - ログイン試行制限: 5 回連続で失敗するとそのユーザーを 15 分ロック
- 反復回数はハッシュ文字列に埋め込んでいるので、将来上げても既存ユーザーはそのままログインできる
- ペッパー: Worker の Secret
- フロントと API を 1 つの Worker にまとめているので CORS 不要・デプロイ 1 コマンド。
users ─┬─< sessions
└─< problems ─< problem_tags
-
users: ユーザー名は大文字小文字を区別せず一意(COLLATE NOCASE)。メールアドレスは持たない(パスワードの復旧は復旧コードで行う)。 -
sessions: Cookie にはランダムトークン、DB にはその SHA-256 のみ保存(DB が漏れてもセッションを乗っ取れない)。期限 30 日、毎日 Cron で掃除。 -
problems: difficulty / status は色ではなく列挙値で保存。UNIQUE(user_id, url)で同じ問題の二重登録を防止。 -
problem_tags: 問題とタグの多対多。 -
problems.score: 配点。AtCoder から自動取得するか手入力(古いコンテストなど配点がない問題は NULL)。 -
atcoder_contests/atcoder_tasks: AtCoder から取得した問題一覧・配点のキャッシュ。
詳細は migrations/(0001 は GitHub OAuth 版の初期スキーマ、0002 でパスワード認証に、0003 で AtCoder 連携を追加)。
追加フォームの「AtCoder から入力」でコンテスト名や ID を打つと候補が出る(ABC/ARC/AGC/AHC/企業コンなど、アーカイブにある全コンテスト)。選んで問題を押すと URL・問題名・配点が入る。
- AtCoder に公式 API はないため、公開ページ(コンテストアーカイブ・問題一覧・各問題ページ)の HTML を読んでいる。robots.txt で禁止されていないページのみ
- AtCoder は Cloudflare からのアクセスを 403 で拒否するため、Worker からは取得しない。手元の Mac で
scripts/sync-atcoder.tsを実行して本番 D1 に書き込み、Worker はそれを返すだけ - Mac の launchd で毎朝 7:00 に自動実行する。1 回あたり最大 300 アクセス(3 秒間隔、約 15 分)で、次の順に進める
- アーカイブ 1 ページ目(直近約 50 回)の問題一覧・配点
- アーカイブ 2 ページ目以降のコンテスト一覧(約 1,450 回。初回のみ)
- 過去回の問題一覧(新しい順)
- 過去回の配点(新しい順)
- 過去回の問題一覧はおよそ 1 週間、配点はおよそ 1 ヶ月でそろう見込み。進捗は実行ログの最後に出る
- 問題一覧がまだのコンテストは候補に「問題取り込み待ち」と出る。急ぐときは手動で取り込む
- 問題文は AtCoder の著作物なので取得・保存しない
- Difficulty は AtCoder Problems 独自の推定値で AtCoder 公式には存在しないため、自動入力しない
npm run atcoder:sync # 毎朝の取り込みと同じ
npm run atcoder:sync -- --all # 上限なしで最後まで取り込む(5 秒間隔。過去分の一括取り込み用)
npm run atcoder:sync abc400 arc190 # 指定したコンテストの問題一覧と配点を取り込む(ID は大文字小文字を区別)# インストール
cp scripts/launchd/com.ryusuke920.atcoder-list-sync.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ryusuke920.atcoder-list-sync.plist
# 今すぐ 1 回実行 / ログ確認
launchctl kickstart gui/$(id -u)/com.ryusuke920.atcoder-list-sync
cat ~/Library/Logs/atcoder-list-sync.log
# アンインストール
launchctl bootout gui/$(id -u)/com.ryusuke920.atcoder-list-sync
rm ~/Library/LaunchAgents/com.ryusuke920.atcoder-list-sync.plist- 初回実行時に macOS が「node が"書類"フォルダへのアクセスを求めています」と聞いてくるので許可する
- Mac がスリープ中だった場合は起きたときに実行される。電源オフだった日はスキップ
wrangler loginの認証を使うので、ログインが切れていると失敗する(ログに出る)
「設定」で AtCoder ID を登録すると、登録した問題の状態を提出結果から更新する。
- 提出データは非公式の AtCoder Problems の API(
/atcoder-api/v3/user/submissions)から取得する。AtCoder 公式の提出ページは robots.txt で禁止されているため読まない - API は CORS を許可しているのでブラウザから直接読み、問題ごとの集計(AC したか・最後の結果)だけを Worker に送る(
src/submissionSync.ts→POST /api/sync/results) - API の注意書きに従い、アクセス間隔は 1 秒以上(タブをまたいでも localStorage で守る)。前回の続き(
users.submissions_cursor)から読み、1 回の同期は 20 ページ(1 万件)まで。ジャッジ中の提出があればそこから読み直す - 状態は手で選べない。一度でも AC していれば AC、なければ最後の提出の結果、提出がなければ未提出。同期のたびに全問題を決め直す
- 問題を追加したとき、問題一覧を開いたとき(10 分に 1 回まで)、「同期」ボタンで実行
- パスワードを忘れたときは復旧コードで再設定する(メールアドレスは預からない)。登録時と設定画面からの再発行時に一度だけ表示し、DB には HMAC のみ保存。使ったコードは無効になり新しいコードを発行する
- ログイン・復旧・パスワード確認は、5 回続けて失敗すると 15 分ロック
- 設定画面からアカウントを削除できる(問題・タグ・提出結果は外部キーの CASCADE で消える)
npm install
cp .dev.vars.example .dev.vars # ローカル用の PASSWORD_PEPPER
npm run db:migrate:local
npm run dev # http://localhost:5173 → 新規登録- Cloudflare にログイン:
npx wrangler login - D1 を作成し、
database_idをwrangler.jsoncに書く:npx wrangler d1 create atcoder-list - ペッパーを Secret に登録(値は誰も知らなくてよい。一度決めたら変えない。変えると全員ログインできなくなる)
openssl rand -base64 32 | npx wrangler secret put PASSWORD_PEPPER - スキーマ適用とデプロイ
npm run db:migrate:remote npm run deploy
以降は npm run deploy だけ。スキーマを変えたら migrations/ に SQL を追加して npm run db:migrate:remote。