AIに読ませたい方へ:右上の「📥 Markdown版」から
RIKURECO_MANUAL.md をダウンロードし、ChatGPTやClaudeなどに読み込ませると、この説明書の内容にもとづいて質問に答えてもらえます。
りくレコ 取扱説明書
陸上競技会の運営・エントリー受付・結果速報を行うWebアプリ「りくレコ」の完全な取扱説明書です。
利用者向けの操作手順と、開発・保守のための技術仕様の両方を含みます。
- 対象読者: 大会運営者 / 選手・観客 / 開発者・保守担当 / コードを読むAIエージェント
- 最終更新: 2026-08-23
- 本体URL:
https://rikutomo.tabui-tomo.com/rikureco.html
第1部 概要
1. りくレコとは
記録会・競技会の運営をブラウザだけで完結させるアプリです。インストール不要、アカウント登録不要、無料で使えます。
1.1 3つの画面
| 画面 | ファイル | 用途 |
|---|---|---|
| 🛠 運営 | rikureco.html | 競技の作成、組分け、記録入力、入金確認 |
| 📺 観戦 | rikureco_view.html | 選手のエントリー申込、結果閲覧(読み取り専用) |
| 🔍 さがす | rikureco_search.html | 公開設定された大会の検索 |
3画面はヘッダー右上のタブで相互に移動できます。運営→観戦に移るときは、設定済みの大会IDが自動で引き継がれます。
1.2 3種類の利用者
- 運営者 … 管理キーを持つ人。データの書き換えができる。
- 選手 … エントリーを申し込み、自分の申請状況を確認する人。
- 観客 … 結果を見るだけの人。
1.3 データの置き場所(端末とサーバー)
データは2か所にあります。この違いを理解すると操作を誤りません。
| 置き場所 | 何が入るか | いつ書かれるか |
|---|---|---|
| 端末内(localStorage) | 編集中の大会データすべて | 操作のたび自動 |
| サーバー | 共有用のコピー | 「⬆️ サーバーへ保存」を押したとき |
サーバーへ保存を押すまで、選手や観客の画面は変わりません。 記録を入力したら保存を押してください。
(例外:入金チェックと受理のボタンは、押した直後に自動でサーバーへ反映されます)
1.4 料金と保管期間
- 利用はすべて無料です。
- サーバー上のデータは、大会IDを登録した日から5ヶ月で自動的に削除されます。
- 削除の対象は、その大会のすべてのファイル(競技・選手・記録・オンライン申請・照会用データ)です。
- 延長はできません。 残したい記録は「保存 (JSON)」で手元にバックアップしてください。
- 5ヶ月を待たずに消したい場合は「🗑 サーバーから今すぐ削除」が使えます。
2. 用語集
| 用語 | 意味 |
|---|---|
| 大会ID | 大会を識別する半角英数字3〜32文字。共有URLの一部になる。例: sakura-hs-2026 |
| 管理キー | 大会データを書き換えるための合言葉。4文字以上。再発行できない。 |
| 確認用パスワード | 選手が申請時に決める合言葉。後日エントリー状況を照会するのに使う。 |
| 競技(グループ) | 「男子100m」などの種目名。同じ名前の競技が複数の組を持つ。 |
| ラウンド | 「予選1組」「決勝」など、競技の中の1レース。 |
| 公開範囲 | 公開 / 非公開 / 未公開 の3段階。3.3参照。 |
| 共有URL | rikureco_view.html?id=大会ID。選手・観客に配るアドレス。 |
第2部 運営者向け操作手順
3. はじめの設定
運営ページ(rikureco.html)の下部ナビ「⚙ 設定」から行います。
3.1 大会IDと管理キーを登録する
- 「🌐 サーバー保存」の大会IDに、他と重ならない名前を入れます(半角英数字・ハイフン・アンダースコア、3〜32文字)。
- 管理キーを4文字以上で決めます。
- 「🆕 大会IDを登録」を押します。
登録すると、その大会は管理キーを知っている人だけが書き換えられるようになります。
⚠️ 管理キーは再発行できません。 忘れると、その大会IDのデータを二度と更新・削除できなくなります。必ず控えてください。
登録の確認画面には、登録日と自動削除日(5ヶ月後)が表示されます。
URLで大会IDを引き継ぐ — rikureco.html?id=大会ID を開くと、大会IDが自動で入ります。運営端末を増やすときは設定画面の「🛠 運営用URLをコピー」で配れます(管理キーは含まれないので別途伝えてください)。選手向けは「🔗 共有URLをコピー」です。
3.2 大会情報(開催日・場所・対象)を入力する
「🏟 大会情報」欄で設定します。公開設定にしたとき、検索ページに表示される情報です。
開催日 — 連日でも飛び飛びでも登録できます。
- 1日だけ … 左の日付を選んで「日程を追加」
- 連日 … 左に開始日、右に終了日を入れて「日程を追加」(間の日がまとめて入ります)
- 飛び飛び … 上の操作を繰り返します
- 登録済みの日はチップで表示され、
×で1日ずつ削除できます - 上限は60日です
都道府県 — 47都道府県から選択します。
競技場名 — 自由入力です(60文字まで)。
対象 — 小学生/中学生/高校生/大学生/一般/その他から複数選べます。
3.3 公開範囲を選ぶ
3段階から選びます。いつでも変更できます。
| 設定 | 検索ページ | 共有URLを知る人 | 管理キーを知る人 | 選手のエントリー |
|---|---|---|---|---|
| 🌍 公開 | 載る | 見られる | 見られる | 受付可 |
| 🔗 非公開 | 載らない | 見られる | 見られる | 受付可 |
| 🔒 未公開(準備中) | 載らない | 見られない | 見られる | 受付しない |
- 準備中は「未公開」にしておき、内容が固まったら「公開」または「非公開」に切り替える使い方を想定しています。
- 変更は「保存 (保護)」→「⬆️ サーバーへ保存」で反映されます。
3.4 エントリー時の注意事項を書く
「📣 エントリー時の注意事項」に書いた内容は、選手のエントリー申請画面の一番上に表示されます(1000文字まで)。改行もそのまま表示されます。
記入例:
・申込期限は8月31日(日) 23:59まで
・参加費 1種目 800円 / ◯◯銀行 ◯◯支店 普通 0000000
・入金確認後に「受理」となります
・当日は競技開始の60分前までに受付をお願いします
空欄にすると何も表示されません。
4. 競技を作る
4.1 競技を1つずつ追加する
「一覧」画面の右下のオレンジ色の + ボタンから作ります。
| 項目 | 説明 |
|---|---|
| 種目名(グループ) | 「男子100m」など。同じ名前でまとめて表示されます |
| ラウンド | 「予選1組」「決勝」など |
| 上がり条件 | 「3着+2」のように書くと、着順に応じて Q マークが自動で付きます |
| 大会記録(GR) / 日本記録(NR) / 世界記録(WR) | 記録を入力するとその値を上回ったとき自動でマークが付きます |
| 開始時刻 | タイムテーブルの並び順に使われます |
| 1次招集 / 2次招集 | 任意。入力するとタイムテーブルに 📣 1次 09:30 / 2次 09:45 と小さく表示されます。片方だけでも構いません |
| タイプ | トラック(小さい方が上位)/ フィールド・得点(大きい方が上位)/ リレー |
4.2 複数の組をまとめて作る
「作成数 (一括)」に数字を入れると、予選|1組 予選|2組 … のように連番で作られます。
4.3 手動で一括登録する
「手動一括」ボタンから、表形式で選手をまとめて入力し、自動で組分けできます。同じゼッケン番号があると警告が出ます。
5. エントリーを受け付ける
5.1 選手に共有URLを伝える
設定画面の「🔗 共有URLをコピー」を押すと、rikureco_view.html?id=大会ID がコピーされます。このURLを配ってください。
選手は大会IDを手入力することもできます。
5.2 申請を取り込む
- 「一覧」画面の「申請取込」ボタンを押します。
- 「⬇️ 新着のオンライン申請を取得」を押すと、サーバーに届いた申請が取り込まれます。
- 取り込み後、「サーバー側の申請データを削除しますか?」と聞かれます。削除しても選手のエントリー状況確認は引き続き使えます。
オフラインの場合は、選手からJSONファイルを受け取って「ファイルから読み込む」で取り込めます。
5.3 入金チェックと受理
同じモーダル内の「💰 入金チェックと受理」で行います。
- 参加費の入金を確認したら 「入金」 を押す
- 申請内容に不備がなければ 「受理」 を押す
押した内容は自動でサーバーに反映され、選手の画面に表示されます。(反映されると「✔ サーバーに反映しました」と出ます)
「表示中をすべて入金済に」「表示中をすべて受理に」で一括処理もできます。この「表示中」は、次で説明する絞り込みの結果に対して働きます。
選手側に表示されるバッジは次のとおりです。
| バッジ | 意味 |
|---|---|
| 運営に届いています(確認待ち) | 送信済み・運営がまだ取り込んでいない |
| 入金確認待ち → 入金確認済み | 運営が入金を確認した |
| 受理待ち → 申請受理 | 不備がなく受理された |
| 組分け完了 | 組・レーンが決まった |
5.4 申請を検索・絞り込む
- 競技セレクト … 競技ごとに絞り込みます。ここで選んだ競技が、そのまま「承認してレースを作成」の対象になります。
- 検索欄 … 氏名・所属・ゼッケン番号・生まれ年の一部で絞り込みます。
- 絞り込み解除 … すべて表示に戻します。
件数は 該当 3件(未承認 全12件中) / 入金確認 2件 / 受理 1件 のように表示されます。
5.5 組分けしてレースを作る
- 上の競技セレクトで対象の競技を選びます(「すべての競技」のままでは作成できません)。
- 「1組の人数」と「並べ方」を決めます。
- シード … 申込記録の速い順に、中央レーンから振り分けます
- 順 … 申込順に並べます
- ランダム … 無作為に並べます
- 「受理」にした申請だけを組分けする … 既定でオンです。オフにすると未受理の申請も対象になります。
- 「承認してレースを作成」を押します。
作成された申請は「組分け済み」になり、二重に登録されることはありません。申請の備考は選手のコメント欄に引き継がれます。
6. 大会当日の操作
6.1 招集(1次・2次)とDNS
選手行の「1次」「2次」バッジを押すと、招集済みの色に変わります。棄権は「DNS」を押します。
競技に招集時刻を登録しておくと、運営・観戦の両方のタイムテーブルに時刻が小さく表示されます。選手への案内に使えます。登録していない競技には何も出ません。
6.2 記録を入力して順位を出す
各選手の記録欄に数字を入れ、下部の「順位計算」を押すと、着順・Qマーク・記録マーク(GR/NR/WR)が自動で付きます。
- トラック競技は小さい記録が上位、フィールド・得点は大きい方が上位です。
- DNS の選手は順位計算から除外されます。
6.3 風速と完了フラグ
風速は競技名の下の「風速」欄に入れ、「一時保存」を押した時点で確定します。
「未完了 / 完了済」のボタンで、その組が終わったかを切り替えます。完了にするとスコアボードが OFFICIAL 表示になります。
6.4 次ラウンドを作る
競技グループの「次R」ボタンから、上位進出者を集めて次のラウンドを自動生成できます。進出人数と並べ方を指定できます。
6.5 スコアボードを映す
下部ナビ「速報」を押すと全画面のスコアボードになります。プロジェクターや大型モニタでの表示を想定しています。「前の組 / 次の組」で切り替えます。
6.6 印刷する
- 競技グループの「一括印刷」 … そのグループの全組をまとめて印刷
- エントリー画面の「印刷」 … その組だけ印刷
7. データの保存・共有・バックアップ
7.1 サーバーへ保存する
設定画面の 「⬆️ サーバーへ保存」 を押します。押すまで選手・観客の画面は更新されません。
7.2 サーバーから復元する
「⬇️ サーバーから復元」 で、サーバーの内容をこの端末に取り込みます。この端末の未保存の変更は失われます。
起動時にも自動で読み込みますが、サーバー側が新しいときだけ取り込みます(オフラインで編集した内容が起動のたびに巻き戻らないようにするため)。
7.3 JSONバックアップ
「バックアップ」の 保存 (JSON) で全データをファイルに出力できます。復元 (JSON) で戻せます。
💡 サーバーのデータは5ヶ月で消えます。大会が終わったら必ずJSONバックアップを取ってください。
7.4 CSV入出力
競技グループ単位、または1レース単位でCSV/JSONを出力できます。CSVの列は次のとおりです。
Round,Time,Type,Status,Wind,Lane,No,Name,Team,Comment,Record,Rank
7.5 サーバーから削除する
「🗑 サーバーから今すぐ削除」 で、5ヶ月を待たずに削除できます。端末内のデータは残ります。元に戻せません。
8. 誤操作を防ぐロック
設定画面で管理者パスワードを設定すると、「ロック機能」が使えます。ロック中は編集操作にパスワードが必要になり、大会中の誤操作を防げます。
このパスワードは端末内だけのもので、サーバーには保存されません。管理キーとは別物です。
第3部 選手・観客向け操作手順
9. 大会を見つける
[🔍 さがす] タブ(rikureco_search.html)で、公開されている大会を検索できます。
- キーワード(大会名・競技場名)
- 都道府県
- 対象(小学生〜一般)
- 開催期間
- 「これから開催される大会のみ表示」(既定でオン)
大会カードを押すと、その大会の観戦ページが開きます。
運営から共有URLや大会IDを直接もらっている場合は、観戦ページの「🌐 IDで読込」に入力しても開けます。
10. エントリーを申し込む
観戦ページの下部ナビ「📝 申請」から申し込みます。
| 項目 | 必須 | 説明 |
|---|---|---|
| 対象の競技名 | ✅ | 登録済みの競技から選びます(自由入力はできません) |
| ゼッケン/No | 分かる場合のみ | |
| 氏名 | ✅ | |
| 所属 | 学校名・チーム名 | |
| 申込記録(Seed) | 組分けの参考になります | |
| 生まれ年 | 西暦4桁(月日は不要) | |
| 確認用パスワード | ✅ | 4文字以上。後日の状況確認に使います |
| 備考 | 300文字まで自由記入 |
⚠️ 確認用パスワードは再発行できません。控えておいてください。
競技の選択肢が空の場合は、まだ運営が競技を登録していないか、大会データが読み込まれていません。
11. エントリー状況を確認する
同じ画面の 「🔎 エントリー状況を確認する」 に、申請時の氏名と確認用パスワードを入れます。
機種変更後でも、アプリを消したあとでも、別の端末からでも確認できます。表示される内容は5.3のバッジと同じです。
12. 結果を見る
- 一覧 … 競技とラウンドの一覧、タイムテーブル
- 速報 … 全画面のスコアボード
- 選手 … 登録選手の一覧(検索可)
記録は約1分ごとに自動更新されます。別のタブから戻ったときも最新に更新されます。
第4部 技術仕様
13. ファイル構成
rikureco.html 運営画面(単一HTML、JSインライン)
rikureco_view.html 観戦・エントリー画面
rikureco_search.html 大会検索ページ
rikureco_api.php サーバーAPI(唯一のサーバー処理)
data/ データ置き場
.htaccess 直接アクセス禁止+ディレクトリ一覧禁止
- フレームワークは使っていません(Vanilla JS + PHP)。
- データベースは使いません(ファイルベースのJSON)。
- 3つのHTMLはそれぞれ独立していて、共通のJSファイルはありません。同じロジックを直すときは各ファイルを個別に直す必要があります。
14. サーバー側のデータ構造
14.1 ディレクトリ配置
data/
<大会ID>/
meta.json 登録情報・公開設定・大会プロフィール
data.json 大会データ本体(競技・選手・記録)
applications.json オンライン申請の受け取り箱(運営が消せる)
lookup.json 選手の照会用インデックス(消えない)
.rikureco_public.json 公開大会の検索索引
.rikureco_rate.json IPごとのレート制限カウンタ
.rikureco_pwfail.json 管理キー失敗回数
.rikureco_gc 自動削除を1日1回に間引くためのスタンプ
.htaccess 直接アクセス禁止
data/ 配下は .htaccess で .json を拒否しているため、ブラウザから直接読むことはできません。必ずAPI経由になります。
14.2 meta.json
{
"app": "rikureco",
"id": "sakura-hs-2026",
"name": "第1回さくら記録会",
"title": "第1回さくら記録会",
"key_hash": "$2y$10$...",
"created_at": 1787464735,
"expires_at": 1800683935,
"updated_at": 1787464900,
"legacy": false,
"visibility": "public",
"public": true,
"dates": ["2026-09-05", "2026-09-06"],
"pref": "東京都",
"venue": "駒沢陸上競技場",
"categories": ["中学生", "高校生"]
}
app: "rikureco"が自動削除の対象マーカーです。この印がないディレクトリには一切触れません(BBSや突破ノートのデータを守るため)。key_hashがnullの大会は「未クレーム」状態で、申請一覧の閲覧・削除ができません。title/venueは実体参照を戻した素のテキストです(検索索引に使うため)。
14.3 data.json
{
"meta": { "name": "...", "comment": "...", "entryNotice": "...", "visibility": "public",
"dates": [], "pref": "", "venue": "", "categories": [] },
"races": [ { "id": 1787464735000, "name": "男子100m", "round": "予選1組", "time": "10:00",
"type": "time", "status": "pending", "wind": "+1.2",
"comment": "", "condition": "3着+2", "GR": "", "NR": "", "WR": "" } ],
"entries": { "1787464735000": [ { "lane": 1, "no": "101", "name": "...", "team": "...",
"comment": "", "record": "11.20", "rank": 1, "kana": "", "members": "",
"call1": false, "call2": false, "mark": "Q", "recordMark": "" } ] },
"applications": [ ... ]
}
entriesのキーは競技ID(Date.now()由来の数値の文字列)です。- 文字列はHTMLエスケープ済みで保存されます(17.1)。
meta.adminPass(端末ロック用ハッシュ)はサーバー側で必ず削除されます。
14.4 applications.json
選手からの申請の受け取り箱です。素のテキストで保存されます。
[ { "id": 1787465870867, "raceName": "男子100m", "no": "12", "name": "山本",
"team": "F高", "seed": "11.80", "birthYear": 2008, "note": "備考",
"pass": "online", "status": "pending", "paid": false, "accepted": false } ]
pass は常に "online" です(平文パスワードは保存しません)。運営が clear_apps で削除できます。
14.5 lookup.json
選手の本人確認専用インデックスです。clear_apps でも消えません。
[ { "n": "山本", "r": "男子100m", "h": "$2y$10$...", "t": 1787465870 } ]
n=氏名, r=競技名, h=パスワードハッシュ, t=登録時刻。
なぜ分けているか: 本人確認のハッシュを applications.json に置くと、運営が取り込み後にその箱を空にした時点で選手が照会できなくなるためです。
14.6 公開索引と運用ファイル
.rikureco_public.json は公開大会だけの一覧です。検索のたびに全ディレクトリを開かずに済ませるためのキャッシュで、save_state のたびに同期されます。壊れていても検索は止まりません(空として扱います)。実体のないIDが残っていた場合は、検索時に自動で掃除されます。
15. API仕様
15.1 共通ルール
- エンドポイントは
rikureco_api.phpのみです。 - POSTは
Content-Type: application/json必須です(それ以外は415)。 loadinfosearchは GET も使えます。- レスポンスは常にJSON(
Content-Type: application/json; charset=utf-8)です。 - リクエスト本文の上限は 3MB です。
- すべてのパラメータは型ガード(
rr_str())を通ります。配列を送っても例外にはならず、空文字として扱われます。 - 大会IDは
^[A-Za-z0-9_-]{3,32}$かつ予約語(tipsbbsadminなど)以外である必要があります。
15.2 register
大会IDを登録し、管理キーを設定します。
{ "action": "register", "id": "sakura-hs-2026", "key": "ひみつの合言葉", "name": "第1回さくら記録会" }
- 既に管理キーが設定されているIDは
409です。 - 管理キーは4〜64文字。
- レート制限: 10回/時。
15.3 info
登録状況と保管期限だけを返します。管理キーは任意(未公開の大会を見るときのみ必要)。
GET rikureco_api.php?action=info&id=sakura-hs-2026
15.4 load
大会データを返します。
- 一般(管理キーなし):
applicationsはraceNamenamenopaidacceptedstatusのみに絞られます。備考・生まれ年・所属は返しません。 - 運営(管理キー一致): すべて返します。
owner: trueが付きます。 - 未公開(draft)の大会は管理キーがないと
403。 - ETag による
304に対応しています。ETagは運営用と一般用で別になります。
15.5 save_state
大会データを保存します。管理キー必須。
{ "action": "save_state", "id": "...", "key": "...", "payload": { "meta": {...}, "races": [...], "entries": {...}, "applications": [...] } }
保存時にサーバー側で行うこと:
meta.adminPassを削除applications[].passを"online"に置換、passHashを削除paid/acceptedを真偽値に正規化、birthYearを西暦4桁に検証datesprefcategoriesをホワイトリスト・実在日で検証- 公開索引を同期
- 競技1000件、1競技400人、申請3000件の上限を適用
15.6 submit_app
選手が申請します。管理キー不要。
{ "action": "submit_app", "id": "...", "app": { "raceName": "男子100m", "no": "12", "name": "山本",
"team": "F高", "seed": "11.80", "birthYear": 2008, "note": "備考", "pass": "確認用パスワード" } }
- 登録済みの大会にのみ受け付けます(未登録IDでフォルダが増えるのを防ぐため)。
- 未公開(draft)の大会は
403。 - 重複判定: 同じ競技で、Noがある場合はNoで、Noが空の場合は氏名で判定します。
passはpassword_hashしてlookup.jsonに保存します(平文は保存しません)。- レート制限: 30回/時。
15.7 check_app
選手が氏名+パスワードで自分の状況を照会します。
{ "action": "check_app", "id": "...", "name": "山本", "pass": "確認用パスワード" }
- 本人確認は
lookup.jsonで行います。 - 状態は
data.json(運営が取り込み済みの場合)、詳細はapplications.jsonから合成します。 - 失敗すると
403+ 総当たりカウント。5回失敗で10分ロック。 - レート制限: 60回/時。
15.8 apps / clear_apps
申請の一覧取得・削除。管理キー必須、かつ管理キーが設定済みの大会のみ(未クレームの大会は 403)。
clear_apps は applications.json だけを削除し、lookup.json は残します。
15.9 delete
大会データを即時削除します。管理キー必須、かつ管理キーが設定済みの大会のみ。公開索引からも削除されます。
15.10 search
公開大会を検索します。大会ID不要、GETで呼べます。
GET rikureco_api.php?action=search&q=さくら&pref=東京都&category=高校生&from=2026-09-01&to=2026-09-30&upcoming=1
レスポンスには meets(最大200件)、prefs(47都道府県)、categories(対象区分)が含まれます。並び順は「開催予定が先 → 開催日の早い順」です。
15.11 HTTPステータス一覧
| コード | 意味 |
|---|---|
| 200 | 成功 |
| 304 | 更新なし(load のみ) |
| 400 | パラメータ不正・ID書式違反・JSON壊れ |
| 403 | 管理キー違い / 未公開の大会 / 未クレームの大会 |
| 404 | 大会が存在しない・データ未保存 |
| 405 | メソッド違反 |
| 409 | ID重複・申請重複 |
| 410 | 保管期限切れ(削除済み) |
| 413 | 本文が3MB超 |
| 415 | Content-Type が application/json でない |
| 429 | レート制限・総当たりロック |
| 500 | ファイル書き込み失敗 |
| 507 | 申請件数が上限 |
16. クライアント側の状態(app.state)
app.state = {
meta: { name, comment, entryNotice, adminPass, isLocked,
visibility, dates: [], pref, venue, categories: [] },
races: [ ... ],
entries: { "競技ID": [ ... ] },
applications: [ ... ]
}
localStorage のキー:
| キー | 内容 |
|---|---|
rikureco_saved_state | 運営端末の全データ |
rikureco_auto_id / rikureco_cloud_key | 大会IDと管理キー |
rikureco_last_sync | 最後にサーバーと同期した時刻(起動時の巻き戻し防止) |
rikureco_tt_closed / rikureco_tt_folded | タイムテーブルの開閉状態 |
rikureco_viewer_id / rikureco_viewer_state | 観戦端末の大会IDと自分の申請履歴 |
17. セキュリティ設計
17.1 エスケープの規約
このアプリで最も重要な規約です。
運営画面(rikureco.html)と観戦画面のstateは「HTMLエスケープ済みの文字列」を保持し、表示するときはそのままinnerHTMLに出す。
したがって:
- 入力欄から
stateに書き戻すときは必ずescapeHTML()を通す。 素通しすると、そのままinnerHTMLに載って XSS になります。 - 表示時に再度
escapeHTML()を呼ばない。 二重エスケープになり「S&D陸上部」が「S&D陸上部」と表示されます。 innerTextやvalueに出すときはunescapeHTML()で戻す。 そうしないと実体参照がそのまま見えます。- 外部から取り込むデータは
app.security.normalize()を通す。unescapeHTML → escapeHTMLの順で掛けるので、エスケープ済みでも素のままでも「ちょうど1回」に揃います(冪等)。
サーバー側は素のテキストで保存し、エスケープは表示する側の責任とします。applications.json が素なのはこのためです。
観戦画面の「自分の申請履歴」だけは端末内の素データなので、表示時に escapeHTML() を掛けます。
17.2 認証と権限
| 操作 | 必要なもの |
|---|---|
| 大会を見る(公開・非公開) | 大会ID |
| 大会を見る(未公開) | 大会ID+管理キー |
| エントリー申請 | 大会ID(未公開の大会は不可) |
| 自分の申請状況の照会 | 氏名+確認用パスワード |
| データの書き換え | 管理キー |
| 申請一覧の閲覧・削除・大会の削除 | 管理キー(かつ登録済みの大会であること) |
管理キーは password_hash() で保存し、password_verify() で照合します。平文はサーバーに残りません。
17.3 総当たり・レート制限
- 管理キーを同一IPで15分以内に5回間違えると、10分間ロックされます(
data/.rikureco_pwfail.json)。失敗時は1秒待たせます。 - 未公開の大会を「管理キーなしで」開こうとしただけの人は失敗回数に数えません(一般の閲覧者がロックを誘発しないため)。
- IPごとのレート制限:
register10回/時、submit_app30回/時、check_app60回/時、save_state600回/時。
17.4 個人情報の露出範囲
| 情報 | 誰が見られるか |
|---|---|
| 氏名・所属・記録(スタートリスト相当) | 大会IDを知っている人 |
| 申請の備考・生まれ年 | 管理キーを持つ運営のみ |
| 確認用パスワード | 誰も見られない(ハッシュのみ保存) |
| 端末ロック用パスワード | サーバーに送られない |
17.5 CSRF・パストラバーサル対策
- POSTは
application/json限定です。他サイトのフォームからは「単純リクエスト」として送れず、プリフライトが必要になります。CORSヘッダを出していないためプリフライトは失敗します。 - 大会IDは正規表現で厳格に検証し、
../などは受け付けません(全アクションで検証済み)。 - 自動削除は
app: "rikureco"マーカーのあるディレクトリのみを対象とします。
17.6 実施済みの攻撃テスト
次の攻撃を実際に仕掛けて、すべて防げることを確認しています(2026-08-23)。
| 分類 | 試した内容 | 結果 | |
|---|---|---|---|
| パストラバーサル | 9アクション × 22パターン(../、URLエンコード、二重エンコード、ヌルバイト、絶対パス、Windowsパス、予約デバイス名など)計198通り | 全て400で拒否。data/ の外にファイルは作られない | |
| DoS | 5MBペイロード / 深さ10万のネストJSON / 1万件のレース・選手 / 5万文字の入力 / 40並列アクセス | 413・400で拒否、上限で切り詰め、5万文字でも0.09秒、40並列0.16秒 | |
| DoS(ディスク) | 大会を25回連続で作成 | 10個で打ち止め(10回/時) | |
| ブルートフォース | 管理キー・選手パスワード・未公開大会のキーを連続試行 | 5回で10分ロック。1回ごとに1秒待たされる。ロック中は正しいキーも通さない | |
| ブルートフォース(回避) | ロックを解除しながら70回試行 | 時間あたりの上限(60回/時)が別途作動 | |
| SQLインジェクション | — | データベースを使っていないため構造上不可能 | |
| コマンドインジェクション | ; `\ | && ` $()` を含む入力 | exec系の関数を1つも使っていないため実行されない |
| ヘッダーインジェクション | 改行を含むIDをボディ・URLの両方から | 注入されたヘッダ0件 | |
| PHPオブジェクトインジェクション | — | unserialize を使っていないため不可能 | |
| JSONインジェクション | 引用符でレスポンス構造を壊す入力 | 正しいJSONを維持、余計なキーは混入しない | |
| XSS | 全入力欄に <script> <img onerror> <svg onload> を投入し全画面を巡回 | 発火0件、要素の注入0件 | |
| プロトタイプ汚染 | __proto__ constructor prototype をキーに持つデータ | サーバー・クライアントの両方で除去 | |
| CSRF | text/plain など4種のContent-Typeで書き込み | すべて415で拒否 |
18. 自動削除の仕組み
meta.jsonのcreated_atから5ヶ月後がexpires_atです。rr_gc()が期限切れの大会を削除します。全リクエストで走ると重いので、data/.rikureco_gcのタイムスタンプで1日1回に間引いています。- GCを待たずに、期限切れの大会へアクセスがあった時点でも削除されます(
rr_lookup())。 - 削除対象は
app: "rikureco"を持つディレクトリのみです。BBSや突破ノートのファイルには構造上触れません。 meta.jsonを持たない古いデータは、初回アクセス時にその日を登録日として取り込みます(いきなり消さないため)。
19. サーバー負荷への配慮
- 観戦画面の自動更新は 45〜75秒のランダム間隔です。全員が同じ秒に集中して負荷の山ができるのを避けています。
- 画面が隠れている間(別タブ・画面オフ)は取得しません。
- 別タブから戻ったときは、前回取得から30秒以上経っていれば即座に更新します。
loadは ETag を返すので、更新がなければ304(本文0バイト)で済みます。- 入金・受理のトグルは700msのデバウンスでまとめてから送信します。
- 検索は全ディレクトリ走査ではなく、公開索引1ファイルを読みます。
第5部 保守
20. 設定値の変更方法
rikureco_api.php の冒頭の定数を変えます。
| 定数 | 既定値 | 意味 |
|---|---|---|
RIKURECO_RETENTION_MONTHS | 5 | 保管期間(月) |
RIKURECO_MAX_PAYLOAD | 3MB | リクエスト本文の上限 |
RIKURECO_MAX_RACES | 1000 | 1大会の競技数 |
RIKURECO_MAX_ENTRIES | 400 | 1競技の選手数 |
RIKURECO_MAX_APPS | 3000 | 申請の蓄積上限 |
RIKURECO_MAX_NOTE | 300 | 備考の文字数 |
RIKURECO_MAX_DATES | 60 | 開催日の登録数 |
⚠️ 保管期間を変えたら、画面の告知文も直してください。告知はrikureco.htmlの設定パネル、rikureco_view.htmlの#viewer-notice、rikureco_search.htmlの注記、privacy_policy.phpの第5章にあります。
都道府県・対象区分を変えるときは、PHPの $RIKURECO_PREFS / $RIKURECO_CATEGORIES と、rikureco.html の RR_PREFS / RR_CATS の両方を同じ並びで直します。
21. 開発時の検証手順
# PHP構文
php -l rikureco_api.php
# JS構文(インラインscriptを抜き出してチェック)
node -e "
const fs=require('fs');
for(const f of ['rikureco.html','rikureco_view.html','rikureco_search.html']){
const s=fs.readFileSync(f,'utf8');
const sc=[...s.matchAll(/<script(?![^>]*src=)[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]).join('\n;\n');
fs.writeFileSync('/tmp/'+f+'.js',sc);
}"
for f in rikureco.html rikureco_view.html rikureco_search.html; do node --check /tmp/$f.js; done
# ローカルサーバー
php -S localhost:8321
APIをcurlで叩くときは -H 'Content-Type: application/json' が必須です。日本語をGETパラメータに渡すときは --data-urlencode を使ってください(生バイトのURLは受け付けられません)。
22. 実装時にはまりやすい点
- メディアクエリはCSSの末尾に置く。 詳細度を上げないため、前方に書くと後続の通常ルール(
.modal-content{width:90%}など)に負けます。 calcRank()は最後にrenderEntry()を呼ぶ。 入力欄の値を読むときは calcRank より先に読まないと、再描画で消えます。transition: widthがあるので、クラス付与直後にgetBoundingClientRect()を測ると元の幅が返る。selectはmin-width: 0を入れないとflex内で縮まない。100vwは縦スクロールバーの分はみ出す。left:0; right:0を使います。- 申請の一覧は id ではなく配列インデックスで操作する。 idは取込元により小数や重複がありえます。
- 同じロジックが3つのHTMLに重複している。 片方だけ直すとずれます。
- クライアントとサーバーで選択肢の配列が二重管理。 片方だけ増やすと検証で落ちます。
23. 既知の制限
- 大会データは端末が主で、サーバーは共有用のコピーです。複数端末から同時に編集すると、後から保存した方で上書きされます。
- 検索索引は1ファイルなので、公開大会が1万件を超えると読み込みが重くなります(その規模になったらSQLite化が必要)。
- 同姓同名で確認用パスワードも同じ場合、互いの申請状況が見えます。
- 開催日は未入力でも登録できます(検索では「日程未定」と表示)。
- 管理キー・確認用パスワードは再発行できません。
:has()を使ったCSSがあるため、古いブラウザでは選択状態の強調が弱くなります(機能自体は動きます)。
24. トラブルシューティング
| 症状 | 原因と対処 |
|---|---|
| 「この大会IDは未登録です」 | 先に「🆕 大会IDを登録」を押します |
| 「管理キーが違います」 | キーの打ち間違い。5回失敗すると10分ロックされます |
| 「約10分間ロックされています」 | 10分待ちます。正しいキーでもロック中は通りません |
| 選手の画面に反映されない | 「⬆️ サーバーへ保存」を押していない可能性。入金・受理は自動反映されます |
| 検索ページに大会が出ない | 公開範囲が「公開」になっているか、保存済みか確認します |
| 選手が競技を選べない | 運営がまだ競技を登録していないか、大会データが読み込まれていません |
| 「保管期限を過ぎたため削除されました」 | 5ヶ月経過。新しいIDで登録し直します |
| 風速が保存されない | 「一時保存」を押してください(押した時点で確定します) |
文字が & と表示される | 取り込み経路が normalize() を通っていません(17.1) |
| 申請の取り込みで403 | 管理キー未設定の大会です。「🆕 大会IDを登録」でキーを設定します |
*この説明書は RIKURECO_MANUAL.md として本体と同じディレクトリに置かれています。仕様を変更したときは、この文書も合わせて更新してください。*