Add spaced-repetition study mode on Cloudflare Pages, D1 and Access - #79
Merged
Conversation
- Terraform で D1 / Cloudflare Access / Pages プロジェクトを定義する。Access の AUD タグは read-only 属性なので、Pages の環境変数へ Terraform から直接渡せる。 ダッシュボードから手で控えて貼る作業と、その転記ミスがなくなる。 - ホスティングを GitHub Pages から Cloudflare Pages へ移す。ルートで配信されるので basePath の分岐は畳んだ。サイトのデプロイは Pages の Git 連携に任せ、 CI が受け持つのは D1 の更新だけにする。 - D1 のテーブルは Terraform に持たせない。スキーマ変更の履歴はマイグレーションが 持つべきもので、宣言的な差分検出とは相性が悪いため。 - wrangler.d1.jsonc をあえてその名前にしていないのは、Pages がルートの wrangler.jsonc を自動検出して Terraform 管理のバインディングと二重管理に なるのを避けるため。 sync-d1.yml が参照するマイグレーションと同期スクリプトは次のコミットで入る。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
出題スケジューラは FSRS-6 (ts-fsrs)。Anki 23.10 以降の既定アルゴリズムで、 左スワイプ = Again、右スワイプ = Good の 2 択をそのまま評価入力に写像する。 設計上の勘所: - review_logs を原本、card_states をそこから作り直せる派生データとして扱う。 この前提のおかげで、オフライン同期の順序ずれ・Undo・パラメータの再最適化が すべて rebuildCardState() ひとつに帰着する。 - 冪等性は (user_id, client_event_id) の一意索引で担保する。既知の ID は先に 弾き、それでもすり抜けた重複は D1 の batch ごと失敗させる。batch は トランザクションなので、ログだけ入って状態が二重に進むことはない。 - タイムゾーンとロールオーバー(午前 4 時)の計算はアプリ側で行い、結果を study_day として保存する。SQLite にタイムゾーン付きの時刻型がないためだが、 結果として日次の集計が索引一発になり、無料プランの CPU 10ms にも優しい。 - 認証は Cloudflare Access の JWT を検証する。Access を有効にしていても Functions に直接届く経路は残りうるので、署名のない Cf-Access-Authenticated-User-Email は使わない。 DDL の正は migrations/*.sql で、functions/_lib/schema.ts はそれを写した型定義。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/study はタイトルだけを見て中身を思い出し、答え合わせをしてから左右に スワイプする画面。答えを見るまではスワイプさせない(見ずに評価しても 記録の意味がないため)。 - 未送信のレビューは IndexedDB のアウトボックスに溜め、オンラインに戻った ときにまとめて送る。冪等キーを持たせてあるので、再送で二重に反映される ことはない。IndexedDB が使えない環境ではメモリだけで動く。 - Cloudflare Access のセッション切れは、redirect: "manual" で投げて opaqueredirect を掴まえて検知する。素の fetch ではクロスオリジンの リダイレクトがネットワークエラーに見え、オフラインと区別できないため。 ただしリダイレクトの原因が Access とは限らない(開発サーバの末尾スラッシュ リダイレクトなど)ので、リロードにクールダウンを設けて無限ループを避ける。 - Service Worker が /api/ をキャッシュしないようにする。古いキューを掴むと 採点済みのカードがもう一度出てくるし、Access のリダイレクトを保存すると 以後ずっと壊れた応答を返し続けることになる。 暗記度は安定度 S を対数で 0〜100 に写像して表示する。S をそのまま出すと 40 日と 200 日の差が伝わりにくいため。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
実際に terraform apply を通す過程で見つかった 4 点を修正する。
- Access の self-hosted アプリケーションは自分のアカウントのゾーンに属する
ホスト名にしか掛けられない。*.pages.dev は Cloudflare 共有のドメインで
ゾーンではないため対象にできず、独自ドメインが必須になる
("domain does not belong to zone")。app_hostname 変数を足し、
cloudflare_pages_domain で Pages に繋ぐ形にした。設計時に
「ドメインを買わずに済む」と書いていたのは誤りだった。
- read_replication を明示する。省略すると更新時にプロバイダが null を送り、
API に 400 で弾かれる ("Expected object, received null")。
- deployment_configs を locals に切り出して production と preview の両方へ渡す。
Pages の API は fail_open を両環境で揃えることを要求する。
- self_hosted_domains と deployments_enabled が非推奨になっていたので、
destinations と production_deployments_enabled に移行する。あわせて
preview_deployment_setting を none にした。プレビューの URL は Access の
対象パスに含まれず保護の外に出るため。
wrangler.d1.jsonc には terraform output の D1 ID を入れた(秘密ではない)。
migrations_dir はトップレベルではなく D1 のエントリ内が正しい位置だった。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
スワイプするだけだったカードデッキを、Anki のような間隔反復(SRS)で出題する暗記アプリにする。ホスティングを GitHub Pages から Cloudflare Pages へ移し、学習状態を D1 に、認証を Cloudflare Access に置く。ランニングコストは月額 $0(すべて無料枠内)。
何が変わるか
/studyはタイトルだけを見て中身を思い出し、答え合わせをしてから左右にスワイプする画面。スケジューラは Anki 23.10 以降の既定アルゴリズムである FSRS-6(
ts-fsrs)。カード閲覧のページは公開のまま残し、/studyと/apiだけを Access で自分ひとりに絞っている。コミット
e6f70be3dd0549db80fc61 つ目は単体では完結していない。
sync-d1.ymlが参照するマイグレーションと同期スクリプトは 2 つ目で入る。逆順にするとpages.ymlを消した時点で他のワークフローが存在しない workflow を dispatch する状態になるため、こちらの壊れ方を選んだ。設計上の勘所
review_logsを原本、card_statesを派生として扱う。オフライン同期の順序ずれ・Undo・FSRS パラメータの再最適化が、すべてrebuildCardState()ひとつに帰着する(user_id, client_event_id)の一意索引。すり抜けた重複は D1 の batch ごと失敗させ、状態が二重に進まないようにしているstudy_dayとして保存する。SQLite にタイムゾーン付きの時刻型がないためだが、結果として日次集計が索引一発になり、無料プランの CPU 10ms にも優しい動作確認
wrangler pages dev+ ローカル D1 で通した。notes/cardsに同期new→learning、安定度 2.307、次回出題は 10 分後clientEventIdを再送 →applied: 0, skipped: 1(冪等)newに戻り、review_logsも消えるlint / typecheck(アプリと Functions 両方)/ テスト 23 件 /
next buildすべて通っている。マージ前に必要な手作業
このブランチだけでは動かない。 Cloudflare 側の初期設定が先に要る。手順は
docs/setup-cloudflare.mdに書いた。terraform applyを手元で実行し、出力された D1 のデータベース ID をwrangler.d1.jsoncに貼る課金は発生しないが、支払い方法の登録は 1 回必要になる。Pages / Functions / D1 だけならカードなしで使えるが、Zero Trust は無料プランでも支払い方法を求められる。
terraform applyの前にplanを確認してほしい。cloudflare_pages_projectは更新が作り直しを引き起こす不具合が報告されていた(5.1 系、5.14 で修正)ので、forces replacementが出ていないか一度見ておきたい。やっていないこと
/api/study/statsと/api/settingsは動くが画面がない。1 日の枚数調整は当面 API 経由🤖 Generated with Claude Code