技大祭当日に使うCloudflare上のリアルタイム・ビンゴアプリです。完成後は原則freezeし、年1回だけ起動・deployします。
- React UIをViteの複数HTMLエントリーからbuildし、Workers Static Assetsから配信する。公開HTML、JavaScript、CSSは原則Workerを起動しない。
- same-origin WorkerがHTTP API、Cloudflare Access認可、Turnstile検証、景品画像R2、Durable Object routingを担当する。
- 固定名
gameのSQLiteGameStateDurable Object 1個が、番号、景品、当選状態、reach、survey、bounded audit logの正本になる。 ReactionHubDurable Objectが消失許容のstampを正本から分離する。- public stateはHibernation WebSocketで配信し、接続障害時は回数制限付きHTTP fallbackを使う。条件付きGETのETagは復元で巻き戻り得るrevisionだけでなく、状態内容から算出する。
- public reachはTurnstileをserver-side検証する。reachとstampは同じedge kill switchでWorker到達前に停止できる。
/admin*と/screen*は別Cloudflare Access applicationで保護し、WorkerはJWT issuer、AUD、署名、有効期限、email、subを検証する。人員membershipの正本は各Access policyとする。- 景品画像は5 MiB/type/signatureを検証し、content-hash keyで専用R2へ保存する。
- data recoveryはSQLite Durable Object PITRだけを使う。
GameDirectory、generation切替、logical snapshot、backup R2、daily Cronはない。
詳細と年次手順はCloudflare本番運用runbookを参照してください。
公開・会場画面の読み込み表示は既存モーダルの枠を再利用し、スピナーは背景装飾なしで表示します。10秒経っても準備できない場合は、通信エラーと再読み込みボタンを表示します。
productionは団体Cloudflare accountのnutfes-bingo Workerと、団体管理のapp/media custom domainを使います。通常deployに常設stagingや個人accountを使いません。DO/auth/bindingを再び変更する場合だけ、団体account内に一時的な検証環境を作ります。
cloudflare.production.envがAccess team/AUD、site/media URL、Turnstile sitekeyの公開正本です。production account ID、Worker名、binding構成はwrangler.jsoncを正本とします。credential、Access JWT、secretはGitへ保存しません。Admin/Screenの人員membershipは各Cloudflare Access policy、またはそのpolicyが参照するreusable groupで管理します。workers.dev、preview URL、R2 r2.devは無効です。
Node 26.2.0、pnpm 11.2.2、Docker Engine、miseを使用します。package managerはpnpmだけを使い、Vite buildとCloudflare開発runtimeはDocker内で実行します。ホストでpnpm devやpnpm buildを実行しないでください。
mise trust
mise install
mise run install
mise run cloudflare:devローカルURLはhttp://localhost:8787です。cloudflare:devはソースをマウントしてVite HMRを使い、mise run cloudflare:previewはbuild済みのclient/WorkerをWranglerで配信します。両方とも同じportを使うため同時には起動しません。WSLなどでファイル変更を検知しない場合はVITE_USE_POLLING=true mise run cloudflare:devを使います。
local runtimeはCloudflare公式dummy Turnstile key/secretを使います。明示的test modeはloopbackでだけ有効です。本番のTurnstile secretはWrangler secretだけで管理します。
mise run cloudflare:buildはDockerからdist/client/とdist/worker/を一緒にexportします。Wrangler dry-run/deployは生成済みのdist/worker/wrangler.jsonを使います。公開build設定はVITE_SITE_URL、VITE_MEDIA_ORIGIN、VITE_TURNSTILE_SITE_KEYで指定し、本番ではpreflightがcloudflare.production.envから設定します。
依存関係はmise run add <package>、mise run add -D <package>、mise run remove <package>で変更します。
pnpm fmt:check
pnpm lint
pnpm typecheck
pnpm test
pnpm doctor
pnpm knip
mise run cloudflare:checkpnpm testはWorkers Vitest runtimeでWorker、SQLite Durable Objects、R2、WebSocket、Access、Turnstileを検査します。mise run cloudflare:checkはDockerでのclient/Worker build、binding type freshness、Wrangler dry-run、Free plan bundle上限、Worker startup profileを確認します。
開発toolchainのHigh advisory対策はpnpm-workspace.yamlのoverrideとpnpm-lock.yamlで管理します。現在はfast-uri、undici v7、brace-expansionを修正版へ固定し、minimumReleaseAgeとtrustPolicy: no-downgradeは維持しています。release gateの監査を無効化したりseverity閾値を下げたりせず、依存の修正後に再実行してください。
# 初回・Playwright更新時。Linuxで共有ライブラリが不足する場合は --with-deps を付ける
pnpm exec playwright install chromium
pnpm test:e2e
pnpm perf
# E2EのHTMLレポートを開く
pnpm test:e2e:report両commandは既存のDocker production previewをhttp://localhost:8788で自動起動・終了します。port 8788を空け、E2Eと性能計測は順番に実行してください。既存サーバーや任意のremote URLは使いません。毎回新しいcontainerのtmpfsにDO/R2の状態を作り、通常開発の.wranglerや本番データには触れません。build用URLとTurnstile keyはテスト値に固定し、Admin/Screenは既存のloopback限定local bypassだけを使用します。Cloudflare Accessの実ログインはE2E対象外です。
playwright.config.ts/e2e/:Chromiumでモバイル幅のキーボード操作、番号の入力境界、管理画面での追加・削除→公開画面へのlive反映・reload後の永続化を検査します。Worker/APIをmockしません。失敗時のscreenshotとtraceはtest-results/、HTMLはplaywright-report/に保存します。lighthouserc.cjs:公開/と/prizes/をLighthouse標準のmobile条件で各3回計測します。PlaywrightのChromiumを再利用し、計測データは.lighthouseci/、HTML/JSONとmanifestはlighthouse-report/に保存します。外部のレポート公開serviceやAPI keyは使いません。- CIはPR・developへのpush・手動実行で両方を実行します。E2E失敗時もLighthouseを実行し、生成できたレポートを
browser-quality-reportsartifactとして7日間保持します。レポートはGit・Docker build contextに含めません。 - Ubuntu CIでは、インストールしたChromium実行ファイルだけにAppArmorのuser namespace許可を設定します。Lighthouseの起動に
--no-sandboxは使わず、OS全体の制限も無効化しません。Chromium公式の説明を参照してください。
公開ページの初期表示は、build時にentry CSSをHTMLへ埋め込み、ヘッダーで使うRajdhani 700を低い取得優先度のhint付きで先読みします。ホームの公開状態はHTMLのfetch preloadから開始します。景品一覧ではReactを含まない小さなbootstrapを先に実行し、初回の状態応答をアプリと共有して、先頭景品の画像を描画と同じsrcset・sizesで先読みします。初回GETを重複させず、状態の採用条件・WebSocket更新・ETagによる再検証は維持します。途中で画面が破棄された場合は本文読み込みまで中断し、その応答を後続の取得へ再利用しません。
ホームと景品一覧は通常のimportで初期Suspenseとpage chunkの取得待ちを避け、設定画面を含むモーダル・ヘルプは必要になってから読み込みます。CSS Modulesのクラス連結にはclsxを使い、初回表示から不要なTailwindのクラス解決処理を外しています。ローディングはシステムフォントの短い案内だけにし、10秒経過時の接続エラーと再読み込みボタンを残しています。
- CI artifactを展開し、
lighthouse-report/のHTMLをブラウザで開きます。manifest.jsonからURLと代表runを確認し、JSONと.lighthouseci/assertion-results.jsonで数値・assertionを確認できます。 - Performanceは総合スコアだけでなくLCP・CLS・TBTと、各auditの対象要素・resource・削減見込みを確認します。同じChrome・計測条件で複数runを比較してください。空のイベント状態でのlocal lab計測なので、当日の景品画像・人数・Cloudflare edge latencyや実ユーザーのINPを代表する値ではありません。
- Accessibilityは自動検査だけで合格とは判断せず、E2Eのキーボード操作と手動でのfocus・読み上げ確認を併用します。問題を隠すためにauditを無効化したり閾値を下げたりしないでください。
- Performance / Best Practices / SEOの90点未満はwarningとして分析対象にし、計測自体の失敗とAccessibilityが1回でも100点未満の場合はcommandを失敗させます。閾値は
lighthouserc.cjsを正本とします。計測の揺れをテストretryで隠さず、実行環境とaudit結果を確認してください。
PageSpeed Insightsへcloudflare.production.envのCLOUDFLARE_PRODUCTION_SITE_URLとその/prizesを入力し、mobile・desktopの両方を確認します。localhostやAccess保護下の管理・会場画面は対象にせず、認証を解除して測定しないでください。
Lighthouseのlab結果と、直近28日間のCrUX実利用データは別物です。実利用データがある場合は75パーセンタイルのLCP・CLS・INPを確認します。アクセスが少ない場合の「データなし」を合格扱いしません。PSI結果の共有URL、計測日時、対象deployのcommitをPRやIssueに記録して比較します。API clientや常設dashboardは追加せず、公式UIの分析機能を使います。
参考:Playwright CI、Lighthouse CI設定、PSIのlab/field data。
通常deployはdevelopのcleanかつpush済みHEADから次の3 commandだけを実行します。
mise run preflight
mise run deploy
mise run smokepreflightとdeployはGit HEAD、origin/develop、organization account、Worker、R2、Access座標、Turnstile sitekey/secretをfail closedで照合します。Admin/Screenの人員追加・通常削除はAccess policyだけを変更し、Git、deploy env、Worker deployは不要です。
1000 socket試験は通常release gateではありません。完成時またはrealtime/DO/capacityに影響する変更時だけ、local Workerに対してmise run capacity http://127.0.0.1:8787を実行します。
- 通常のcode/assets/config regression: DO class/schemaを変えていない場合だけ、直前のGit SHAとWorker version IDを確認してrollbackし、そのSHAを指定してsmokeする。
- DO class/schemaを変えたrelease: 古いversionへ戻さずfix-forwardする。
- data誤操作: まずAdminで逆操作し、紙master logを正とする。
- 30日以内のstate破損:
mise run recover -- prepare ...でplanを作り、二者確認後にPITRをscheduleする。commandはrestart前にundo bookmarkをmode600receiptへ保存する。 - Cloudflare全体または復旧長期化: optional reaction/reachを止め、紙master logと
offline/projector.htmlでイベントを継続する。
PITRはlocal runtimeで実行できません。イベント前にproduction相当のremote dummy stateでrestoreとundoを1回rehearseします。
通常500人、capacity確認1000 page instanceを想定します。Static Assets bypass、heartbeatなしHibernation WebSocket、単一GameStateにより、1000 page instanceの保守ケースは約16,000 Worker request / 16,000 DO requestです。
異常時は番号・景品・当選状態・survey・Admin更新を優先し、次の順にoptional trafficを止めます。
optional-public-mutationsedge ruleでstampとpublic reachをWorker到達前に同時停止する。- 会場進行を紙master logへ切り替える。reactionとpublic reachの停止をイベント停止理由にしない。
- Worker/Access障害はbypassせずfail closedにし、復旧見込みが短い場合だけrollback/PITRを行う。