Feature/qr caption scan count bulk csv - #4
Merged
Conversation
必須2項目(名前・媒体)それぞれにチェックボックスを設け、どちらも既定はオフに する。既定を「入れない」にしたのは、ポスターのように自前のデザインを持つものに とっては下の文字が切り落とす手間でしかないため。チェックボックスには実際に印字 される文字列を並記する。「媒体」とだけ書いてあっても、それが場所を伴って 「ポスター · 本館1F」と出ることは伝わらないため。 場所は任意項目なので独立したトグルを設けず、媒体と同じ行のまま媒体のチェックに 連動させる。媒体抜きで場所だけが印字されると紙の上で意味の取れない断片になる。 太字の判定を行の位置ではなく項目に紐づける。従来は1行目を太字にしていたため、 名前をオフにすると媒体が太字になって見出しのように見えていた。 ダウンロードするPNGのファイル名から媒体と場所を落とし、識別名_QR.png にする。 この2つはコード間で重複しがちで、フォルダ内で実際に探す手がかりになる名前が 埋もれていた。'QR' は文字列連結ではなく slugForFilename の引数として渡し、 名前が空でも _QR.png という壊れた見た目にならないようにする。 選択は safeStorage に保存して次回以降も引き継ぐ。QRコードは1枚ずつダウンロード するが印刷はまとめて行うので、20枚分チェックし直すのが作業の大半になるため。 初回は QR_CAPTION_DEFAULTS のままで、キャプションは付かない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
X のアカウントが x.com/nut_fes で、キーワードにアンダーバーを使えないと ソーシャルのハンドルをそのまま設定できなかった。弾いていたのは入力検証だけで、 実行時に解決する fallback.ts はもともとキーの文字種を制限していない。 アンダーバーはQRコードのサイズに影響しない。RFC 3986 の unreserved 文字なので encodeURIComponent でパーセントエンコードされず、ペイロードは既にバイトモード (小文字はQRの英数字モードに無い)なので他の文字と同じ8ビットで済む。 文字クラスはハイフンを最後に置く。[a-z0-9-_] と書くと 9-_ が範囲として解釈され、 大文字と記号まで通ってしまう。この点は useFieldErrors.test.ts で固定した。 FALLBACK_DESTINATIONS には x と nut_fes の2つのキーを登録する。前者は deriveFallbackKey がホスト x.com から導出する値で、キーワード欄を空にした プロジェクトのQRに入る。後者は手で名付けるときに選ぶであろうハンドル名。 キーが一致しないと静的フォールバックに落ちてしまい、それはこの仕組みが 防ごうとしている失敗そのものなので、両方登録しておく。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
イベント中にこの画面を開く理由そのものなので、下のメタ情報の行ではなく名前と 同じ行に置く。 集計は表示中のページのIDに限定した GROUP BY 1本で、既存の idx_access_logs_qr_id が効く。絞らないと10行を描画するために最大のテーブルを 丸ごと走査することになる。プロジェクト一覧が同じ修正を経ているので規則を揃えた。 QRCodes に scan_count 列を持たせてスキャンごとに加算すれば表示は無料になるが、 1スキャンあたりのD1書き込みが1回から2回に増える。Freeプランでは書き込みが 100,000行/日で律速する一方、読み取りは5,000,000行/日で余っている。カウンタ列は 50倍余っている資源を節約するために唯一の制約を半分にする取引になるので採らない。 読み取りコストは(表示中のQRに属するログ数 × 表示回数)で伸びる。2万件のログが すべて同一ページに属する最悪ケースで1回20,000行、1日あたり約250回の表示に相当 する。管理画面としては十分だが、そのためポーリングや入力イベントには繋げない。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
プロジェクト一覧にチェックボックスを追加し、選択分を1本のCSVとして出力する エンドポイント GET /projects/access-logs/csv?projectIds=a,b,c を新設する。 zip ではなく単一ファイルにしたのは、依存の追加が要らず、複数同時ダウンロードが ブラウザにブロックされないため。どの行がどのプロジェクトかを示すために プロジェクト列を先頭に足す(単一プロジェクト版には不要な列)。 行数上限は選択の合計に対してかける。プロジェクトごとにすると10件チェックで 10倍の行数を1クリックで許可することになり、それはFreeプランの1日の読み取り クォータの10%にあたる。 並び順はプロジェクト順→各プロジェクト内で時刻降順とし、キーセットを (project_id, accessed_at, id) の3列にする。全体を時刻順にすると idx_access_logs_project_accessed_at が順序付けに使えず、最大50,000行を Worker内でソートすることになってCPU 10ms 制限に当たる。表計算で開いたときの 見やすさもこちらが勝る。 全選択は表示中のページのみを対象にする。他ページまで黙って含めると、見えて いない量のCSVを1クリックで要求できてしまう。 指定したIDが1つでも存在しなければ404にする。黙って除外すると、ユーザーが チェックしたプロジェクトが欠けたまま完全に見えるファイルができる。 CORS に exposeHeaders: Content-Disposition を追加する。このヘッダはCORSの セーフリスト外なので、これが無いと管理画面はサーバーが組んだ日本語ファイル名を 読めず、クライアントが推測した名前で保存されていた。ファイル名は選択順の先頭を 使う。IN(...) の返却順(SQLiteのrowid順)で引くと、選択の先頭ではなく最も古い プロジェクトの名前が付いてしまう。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
haruto-kamijo
force-pushed
the
feature/qr-caption-scan-count-bulk-csv
branch
from
July 29, 2026 04:48
d9b5858 to
1565cd9
Compare
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.
概要
本番リリースに向けて、実際にポスターを刷って運用する際に足りなかった4点を入れました。
本番へのデプロイは既に完了しています(挙動はこのブランチの内容と一致)。
9c51141識別名_QRにする86afa2343a41fdd9b585811ファイル / +1,024 −102。テストは 42 → 65 件(web +23)。
変更内容
QRのキャプションとファイル名
必須2項目(名前・媒体)それぞれにチェックボックスを設け、どちらも既定はオフにしました。
ポスターのように自前のデザインを持つものにとって、下の文字は切り落とす手間でしかないためです。
チェックボックスには実際に印字される文字列を並記しています。「媒体」とだけ書いてあっても、
それが場所を伴って
ポスター · 本館1Fと出ることは伝わりません。場所は任意項目なので独立したトグルを設けず、媒体のチェックに連動させています。媒体抜きで場所
だけが印字されると、紙の上で意味の取れない断片になります。
ファイル名は
造形大ポスター_ポスター_1F掲示板.png→造形大ポスター_QR.png。媒体と場所はコード間で重複しがちで、フォルダ内で実際に探す手がかりになる名前が埋もれていました。
選択は次回以降も引き継ぎます。QRは1枚ずつ落としますが印刷はまとめて行うので、20枚分チェック
し直すのが作業の大半になるためです。初回は必ずオフです。
キーワードのアンダーバー許容とXの転送先
X のアカウントが
x.com/nut_fesで、アンダーバーが使えませんでした。弾いていたのは入力検証だけで、実行時に解決する
fallback.tsはもともとキーの文字種を制限していません。アンダーバーはQRコードのサイズに影響しません。RFC 3986 の unreserved 文字なので
encodeURIComponentでパーセントエンコードされず、ペイロードは既にバイトモード(小文字はQRの英数字モードに無い)なので他の文字と同じ8ビットで済みます。
FALLBACK_DESTINATIONSにはxとnut_fesの2つを登録しました。前者はderiveFallbackKeyがホストから導出する値(キーワード欄を空にしたプロジェクトのQRに入る)、後者は手で名付ける
ときに選ぶハンドル名です。キーが一致しないと静的フォールバックに落ちてしまい、それはこの
仕組みが防ごうとしている失敗そのものなので両方入れています。運用が固まったら片方を消せます。
QRごとのアクセス数
一覧の各行で、名前と同じ行の右端にバッジで表示します。イベント中にこの画面を開く理由そのもの
なので、下のメタ情報の行ではなく名前の横に置きました。
CSV一括出力
プロジェクト一覧にチェックボックスを追加し、
GET /projects/access-logs/csv?projectIds=a,b,cで1本のCSVとして出力します。zip ではなく単一ファイルにしたのは、依存の追加が要らず、
複数同時ダウンロードがブラウザにブロックされないためです。どの行がどのプロジェクトかを示す
ために先頭に
プロジェクト列を追加しています(単一プロジェクト版には不要な列)。設計判断(diffから読み取りにくい部分)
アクセス数をカウンタ列にしなかった理由 ← 一番見てほしい点です
QRCodes.scan_countを持たせて加算すれば表示は無料になりますが、1スキャンあたりのD1書き込みが1回から2回に増えます。
負荷テストのレポートのとおり、このシステムの実質的な上限は書き込みです。カウンタ列は50倍
余っている資源を節約するために唯一の制約を半分(1日10万→5万スキャン)にする取引になります。
代わりに表示中ページのIDに限定した
GROUP BY1本で数えています(idx_access_logs_qr_idが効きます)。絞らないと10行を描画するために最大のテーブルを丸ごと走査します。プロジェクト
一覧が同じ修正を経ていたので規則を揃えました。
読み取りコストは(表示中QRのログ数 × 表示回数)で伸びます。2万件が全て同一ページに属する最悪
ケースで1回20,000行 = 1日約250回の表示相当。そのためポーリングや入力イベントには繋げて
いません。自動更新にすると前提が変わります。
その他
(project_id, accessed_at, id)の3列。全体を時刻順にすると
idx_access_logs_project_accessed_atが順序付けに使えず、最大5万行をWorker内でソートして CPU 10ms 制限(1102) に当たります。表計算で開いたときの見やすさも
こちらが勝ります。
それは1日の読み取りクォータの10%です。
要求できてしまいます。
まま完全に見えるファイルができます。
[a-z0-9-_]は9-_が範囲として解釈され、大文字と記号まで通ります。この点は
useFieldErrors.test.tsで固定しました。媒体が太字になって見出しのように見えていました。
検証で見つけて直したもの
Content-Dispositionが読めていなかった — セーフリスト外のヘッダなのでexposeHeadersが無いと、サーバーが組んだ日本語ファイル名を管理画面が読めず、クライアントが推測した名前で保存されていました。
IN (...)の返却順(SQLiteのrowid順)で先頭を取っていたため、先頭に選んだプロジェクトではなく最も古いものの名前が付いていました。
検証結果
ローカルの17プロジェクト・10,769行で実測しました。
nut_fesとxの両方が X に転送同一QR・同一ミリ秒のログ2件が同内容の行として出ましたが、DB上は別レコード(id 32696 / 32697)
で総行数が完全一致するため、キーセット3列目
idのタイブレークが機能した証拠です。i18n.tsxなど5ファイルは4つの変更が同じハンクに混在しており、ファイル単位では分割できずハンク単位で振り分けました。そのため個別に確認しています。
相談したい点
xとnut_fesの二重登録(wrangler.jsonc)— 使う方が決まっていれば片方に絞ります。残作業(このPRの範囲外)
CSV_EXPORT_ENABLEDが本番で"false"のため、CSV出力は本番では未稼働です。有効化前に本番で一度 S4 を回すことを
docs/load-test-report-2026-07-27.md§5.7 に記載しています。GIT_SHA未設定のため/healthzが"version":"dev"を返します(同レポート §5.4)。&p=が付いて 57×57 になっています)。