# Monaka — 開発者向けメモ

**最終更新: 2026-08-03**

## ⚠️ 作業ルール（最優先）

**修正を実施する前に、修正内容に間違いが無いかユーザーに確認をする。**

**UTAGE取り込み関連の作業を行う前に [`docs/Convert_utage_to_monaka.md`](docs/Convert_utage_to_monaka.md) を必ず全て読むこと。**

**🚨 UTAGE取り込み修正は「全UTAGEページに汎用対応」で作ること。特定URLや特定ページだけに効く実装は絶対禁止。詳細は [`docs/Convert_utage_to_monaka.md`](docs/Convert_utage_to_monaka.md) の冒頭を参照。**

## ドキュメント

- **引き継ぎ（作業サイクル・検査の現在地・すぐ再開する手順・落とし穴・宿題）: [`docs/HANDOFF-audit-20260804.md`](docs/HANDOFF-audit-20260804.md)** —— 新しく入る人はまずこれ
- **修正の判断基準（何を直し、何を直さないか）: [`docs/degradation-policy.md`](docs/degradation-policy.md)**

- 索引: ルート [`SPEC.md`](SPEC.md)、詳細なファイル説明 [`docs/README.md`](docs/README.md)
- ファネル・管理機能の技術仕様: [`docs/funnel-spec.md`](docs/funnel-spec.md)
- 運用手順: [`docs/manual.md`](docs/manual.md)

## 環境

- Laravel。環境変数の名前は [`.env.example`](.env.example) を参照。**値はリポジトリに書かない。**
- DB 型・カラムは推測せずマイグレーションおよび Eloquent モデルで確認する。

## Recent Implementation Log

日付は作業日（追記は新しいほど上）。

### 2026-08-12

- **マージ後監査（PR#81）: 公開側の送信経路を棚卸しし、受講者ログインに総当たりの上限が無かったのを修正**: 引き継ぎ資料が次に挙げていた**公開側の送信経路**に移った。**認証の要らない送信（POST/PUT/PATCH/DELETE）を全部並べて上限の有無を見る**形にしたところ、**最初の走査は291本**出て、`contract/*` など明らかに認証必須のものが「公開」に混ざっていた —— **私の走査のバグ**で、`route:list --json` の middleware は**別名ではなくクラス名**で返る（`auth` → `App\Http\Middleware\Authenticate`、`throttle:20,1` → `Illuminate\Routing\Middleware\ThrottleRequests:20,1`）。2026-08-02 に `gatherMiddleware()` で**逆方向**（別名で返るのにクラス名で照合）を踏んでおり、同じ場所で2度転んだ形。直すと **17本**、うち上限が無いのは3本だった。**LINE webhook 2本は対象外と判断** —— `X-Line-Signature` を検証してから動くので勝手に走らせられず、上限を付けると提供元の再送を落とす。**残る1本が不具合** —— **受講者ログイン（`member/{slug}/login`）だけ総当たりの上限が無かった**。管理画面は `hasTooManyLoginAttempts`、パートナーは `throttle:10,1` で止めているのに受講者は何も無く、**誤ったパスワードを60回続けても弾かれず、直後に正解でログインできた**（実測・`last_login_at` が入ることまで確認して素通りでないことを確かめた）。見ているのは利用者ではなく**その顧客**で、破られると**お金を払っていない人が有料の教材を見られる**。**止め方はアカウント単位を主にした**（`サイトのスラッグ + メール` で10回/分。相手が回線を変えても効く）。**IP 単位は緩く**した（60回/分）—— ここが今回の要点で、**パートナーと同じ `throttle:10,1`（IP だけで数える形）にすると、社内の共有回線から大勢が入る場面で30人中20人が締め出される**（実測）。**上限を足すときは「止めたい形」と「巻き添えになる形」を両方測る**。**ここでも自分の測定が一度空回りした** —— 巻き添えを見るプローブが**セッションに CSRF トークンを入れておらず30件すべて 419**で、コントローラにすら届いていなかった。「429 でない＝通った」と数えていたため**上限を外しても付けても同じ結果**に見えた。**成立した印**（`last_login_at`）で数え直して確定させた。**自分のテストの誤りも1件** —— `SiteMember` に `status` を渡していたが、`membership_site_members` に**その列は存在しない**（`$fillable` と実列は一致しており形4の穴ではない。受講者の閲覧可否は `member_bundle_grants` だけで決まる）。テスト中のみ有効な `preventSilentlyDiscardingAttributes` が捕まえた。**変異2件で検知を確認**（上限を丸ごと外す＝修正前の形／アカウント単位をやめ IP の緩い上限だけ残す）。**素通り防止**として「同じ回線から30人が全員ログインできること」を同時に見る（上の2件が「全部弾かれているから緑」になるのを防ぐ）。検証: **MySQL 8.0 で 650 passed**（時刻を3パターンに固定しても同数）。

### 2026-08-11

- **マージ後監査（PR#80）: 形6「二重に走る」を再棚卸しし、二重送信を3件修正**: 引き継ぎ資料が次に挙げていた**形6の再棚卸し**に移った。2026-08-02 は**定期実行だけ**を見ており、**キューのジョブ**と**シナリオリマインダ**が抜けていた。**3件とも実測で確定させた**。**①メール一斉送信** —— `processNextChunk()` が**チャンクを送り切ってから** `dispatch_cursor` を進めていた。ジョブは `tries = 3` で再試行するので、途中で worker が落ちると位置が戻ったまま拾い直され、**チャンクまるごと再送**になる（実測: 12人・チャンク5 で **合計15通・2回届いた宛先5件**）。**1件送るごとに位置を進める**ようにした（送った直後・記録前に落ちる隙間は残るが、そこは**1人ぶんが上限**）。**②LINE一斉送信も同じ形**だったので同時に直した（別サービスなので**検査も独立に**用意した。LINE は同じ通知が2回鳴るぶん、届き方の印象はメールより悪い）。**③シナリオリマインダ** —— 対象の絞り込みは「まだ送っていないもの」を**読んで**選ぶが、送信済みの印が付くのは**送った後**。実行が重なると両方が「まだ送っていない」と読んで**同じ人へ2通届く**（実測: **5人全員が2通**）。**`reserved` は絞り込みに使えない** —— `syncScenarioReservations()` が**未来ぶんの予約も** `reserved` で書くので、除外すると通常の配信が丸ごと止まる。ステップ配信と同じ**対象ごとの `Cache::lock`** で止め、**ロックの中でもう一度**決着済みかを読み直す形にした。**`withoutOverlapping()` は答えにならない**と分かったのが今回の要点 —— 守るのは**スケジューラの経路だけ**で、1回の実行が有効期限を超えればロックが切れるし、手元から直に流せば効かない。**ロックだけでも足りない** —— 非ブロッキングで取るため後発は**別の対象**ならロックを取れてしまい、**再確認を外す変異では4人が2通**受け取った（＝ロック内の読み直しは飾りではない）。**自分のテストの誤りも1件見つけて直した** —— 一斉送信の再送テストで、**チャンクを全部送り切ってから**位置を巻き戻していたため、落ちた後の読者まで「届いた」と数えて**常に3件重複**していた。`sendOne()` は例外を自分で捕まえて**失敗として記録する**ので送信イベントから例外を投げても「落ちた」にはならない、と確かめたうえで、**届いたのは落ちた時点まで**とみなす形に直した。**変異5件で検知を確認**（メールの位置更新をまとめる／LINE の位置更新をまとめる／リマインダのロックを外す／ロック内の再確認を外す、＋素通り防止として重なりが無ければ全員に1通ずつ届くこと）。検証: **MySQL 8.0 で 647 passed**（時刻を4パターンに固定しても同数）。

- **マージ後監査（PR#79）: 前サイクルで直したのと同じ形が、会員サイトにも残っていた**: 引き継ぎ資料が次に挙げていた**会員サイトの量**に移った。前回**公開ページのコメント**を直したので、**同じ形（訪問者・受講者が増やすコメント）が他の画面に無いか**を探したところ、**レッスンのコメント**（`MembershipSiteLessonCommentService::visibleForLesson`）が同じく**上限なしで全件読んでいた**。実測（1件300文字）: **5,000件=応答 6.7MB**、**20,000件=応答 26.7MB・リクエストのメモリ 61.5MB**。見ているのは**受講者**（利用者の顧客）なので影響は公開ページと同じ性質。公開ページと同じ方針で修正 —— **ブロック（レッスン）ごとに最新200件**、**新しい方を残す**、**表示は古い順**、**件数を画面に出す**。結果、**件数によらず 277KB**。**上限に達していないときは総数を数えない**ようにして、通常のレッスンで問い合わせを増やさない（検査で固定）。**ここでも自分の測定が一度空回りした** —— 最初の実測は件数を増やしても 9.1KB のまま平坦で、原因は `course_settings`（cast が `array`）に**JSON 文字列を渡していた**ため二重エンコードされ、`is_array()` が false になって**コメント機能が無効のまま**測っていたこと。配列で渡し直して確定させた（この落とし穴も資料に追記）。**変異3件で検知を確認**（上限を外す／古い方を残す／コメント機能を止める＝素通り防止）。検証: **MySQL 8.0 で 642 passed**（時刻を固定しても同数）。

- **マージ後監査（PR#78）: 公開ページの量的な上限を走査し、2件修正（うち1件は公開ページが 500 になる）**: 引き継ぎ資料が次に挙げていた**公開ページの量**に移った。管理画面は 2026-08-04 に実測済みだが、公開側は未測定で、**見ているのが利用者ではなくその顧客**なので影響が大きい。コメントブロックを 0〜20,000件で実測したところ**2件見つかった**。**①`layout` が無いと公開ページが 500** —— 判定が `in_array($el['layout'] ?? 'list', [...]) ? $el['layout'] : 'list'` と書かれており、**キーが無いと `in_array` は true になるのに ternary の側で `$el['layout']` を読んで** `Undefined array key` で落ちていた。**防御のつもりの `??` が防御になっていない**形で、周りは全部 `?? 'list'` 形式で書かれているので意図は「欠けても耐える」だった。先に取り出してから判定するよう修正。**②コメントに上限が無く青天井** —— 1件300文字で **5,000件=応答 8.8MB・ピーク 93MB**、**20,000件=応答 35MB・ピーク 265MB**（`memory_limit` 128M を超えて全訪問者に 500）。コメントは**訪問者が増やす**ので、ページの持ち主には量を抑えられないのが管理画面との違い。ブロックごとに**最新200件**へ制限し、**黙って打ち切らず**「全〇件のうち最新200件を表示しています」と出す。**新しい方を残す**（古い方を残すと投稿した本人が自分のコメントを見つけられない）が、**並びは古い順**に戻す（会話の流れ）。**ブロックごとに引く** —— ページ全体で1回引いて上限をかけると、投稿の多いブロックが枠を食い潰して**静かなブロックのコメントが丸ごと消える**（この落とし穴も検査で固定した）。結果、**件数によらず応答 374KB・7問い合わせ・リクエストのメモリ増分ほぼ0**。**測り方の誤りも自分で見つけた** —— 最初ピークメモリが件数とともに増えて見えたが、**プローブ自身が2万件の配列を組んでいた**ぶんだった。リクエスト前後の `memory_get_usage()` の差で測り直して確定させた。**変異3件で検知を確認**（`layout` の修正を戻す／上限を外す／古い方を残す）。**あわせて前回の LINE プレビューを実ブラウザで確認**（画像のサムネイルが描画される・Flex の見出し/本文/ボタン文字と代替テキストが出る・横にはみ出さない・JSエラーなし）。検証: **MySQL 8.0 で 638 passed**（時刻を固定しても同数）。

- **マージ後監査（PR#77）: 送信側に足した画像・動画・Flex が、一括プレビューでは「空の吹き出し」に見えていた**: 引き継ぎ資料が次に挙げていた **LINE 配信の中身**に移った。**3点を実測** —— ①**テキストの差し込みは効いていた**（`%name%` → `山田太郎`。実際に送って送信JSONを読んで確認）②**Flex の代替テキスト・JSON の中身は置き換わらない**（送信側が `message_text` にしか `StepMailMergeService` を通していない）③**一括プレビューが画像・動画・Flex を一切出さない** —— 画像URLも代替テキストも Flex の中身も、種別バッジすら無く、**空の吹き出しが並ぶだけ**だった。**③は自分の取り残し** —— 2026-08-05（PR#74）で送信側に種別を足したのに、**プレビュー側の更新を忘れていた**。docblock も「画像・動画・Flex は送信側に機能が無いのでここでも出さない」と書いたままで、**嘘になっていた**。パネルの約束は「実際に届く形」なので、内容を確認できない状態だった。種別ごとに描き分けるよう修正（画像・動画は**サムネイル＋元URL**、Flex は**中の文字＋代替テキスト**）。Flex は入れ子の JSON をそのまま見せても何が届くか分からないので、`LineMessagePayload::flexPreviewTexts()` で**読者の目に入る文字だけ**を抜き出す（`text` / `label` / `altText` を上から順に。**20行で頭打ち**にしてパネルが開かなくなるのを防ぐ）。**②は対象外と判断** —— 画面の置き換え文字チップは**本文の textarea にしか挿入しない**ので、Flex での差し込みは**画面が約束していない**。後から善意で広げると Flex JSON の構造を壊す差し込みが起き得るため、理由を仕様書に明記して据え置いた。**変異2件で検知を確認**（プレビューを修正前の形＝本文だけ描くに戻す／Flex の文字抽出が常に空を返す）。検証: **MySQL 8.0 で 634 passed**（時刻を固定しても同数）。

- **マージ後監査（PR#76）: 未検査だった「届くメールの中身」を実際に送って突き合わせた（不具合0件）**: まず**前回の修正で CI が緑に戻り、止まっていた本番デプロイが流れたこと**を GitHub Actions の実ランで確認した。今回入ったのは自分のコミットのみだったので、引き継ぎ資料が次に挙げていた**配信の「中身」**に移った。落ちないこと（例外・cron 停止）は走査済みだが、**送った中身が正しいか**は一度も見ていなかった。この repo は「保存はされるが送信処理が読んでいない」（形4）が繰り返し起きている領域（LINE一斉送信のカスタム送信者・シナリオのデフォルト件名/ヘッダー/フッター）なので、`MailDeliveryContentTest` で**送信を実行して送られたメッセージそのものを読む**形にした。**走査の結果は不具合0件** —— ①差し込み文字が**件名と本文の両方**で置き換わる（片方だけだと「件名に %name% がそのまま出る」メールが届く）②テキスト形式でも差し込みは効き、**HTML の共通ヘッダーは付かない**③**読者名に HTML が入っていても本文には生で入らない**（HTML本文は `e()` を通す）が、**件名はエスケープしない**（`&lt;` が文字として見えるため）④差出人は**ステップ → シナリオ既定 → アカウント**の順⑤**一斉送信でも同じ**（ステップ配信とは別サービスなので独立に確認）。**LINE 側の非対称も確認したが問題なし** —— `pushMessage()` は `pushText()` と違って `$sender` 引数を持たないので画像・動画・Flex でカスタム送信者が落ちるのではと疑ったが、`LineMessagePayload::build()` が**全種別に** `sender` を載せていた（疑ってから読む、の実例）。**変異4件で検知を確認**（件名の差し込みを掛けない／HTML本文をエスケープせず差し込む／差出人の優先順位を崩す／一斉送信が composer を通らない）。**新しい検査が時刻依存でないことも確認**（`FAKE_NOW` で 03:00・23:58 に固定しても同数）。**製品コードの変更なし**。検証: **MySQL 8.0 で 629 passed**。

- **CI が赤く本番デプロイが止まっていた。原因は自分が入れた「時刻依存の検査」だった（マージ後監査）**: 基準線を取った時点で **main が赤**（`LineMediaMessageDeliveryTest` の3件）。**GitHub Actions の実ランを確認したところ、PR#75 のマージで走った 08:54 JST の実行が failure**、それ以前（16:31 JST 等）は success —— つまり**実行した時刻で結果が変わっていた**。**原因は自分が PR#74 で書いた fixture** で、`schedule_mode => 'immediately'`・`existing_reader_policy => 'send_all'` という**ドメインに実在しない値**を使っていた（正しくは `registration_immediate` / `schedule_all_readers`）。実在しない値は `ScenarioLineStepDispatchService::scheduledSendAt()` の `match` の **`default` に落ち**、`delay_time` が空なので**「登録日の 9:00」**として扱われる。その結果、**09:00 JST より前に走らせると1通も送られず**落ちる。**時刻を振って実測**して確定させた（JST 08:30→0通・09:30→2通・13:00→1通・23:30→1通）。**値を直すだけでは「いま通る」ことしか示せない**ので、**時刻を6つ振って送られること**を見る検査（`test_step_delivery_does_not_depend_on_the_time_of_day`）を足した。**変異2件で検知を確認**（実在しない値に戻す／送信処理を止める）。**他に時限爆弾が無いかも全件で走査** —— `FAKE_NOW` で時刻を固定して流せるようにし（環境変数が無ければ何もしない）、**00:05 / 03:00 / 08:59 / 23:58 / 年末 23:50 の5つで 624 passed** を確認。**別担当の3コミット（v0.4.53〜v0.4.55）も監査して問題なし** —— ①フォント設定に `!important` を付ける変更は**ブロックで明示的にフォントを選んだときだけ**に限定されており、`Noto Serif JP` は公開ページ・エディタの両方で**実際に読み込まれている**ことを確認（読み込んでいなければ効かない変更になる） ②Gemini の `responseModalities` 追加は応答の**読み取り側を変えていない**ので既存の検査は有効なまま ③失敗理由に**提供元のテキストを画面へ出す**ようになったが、エディタ・アカウント設定とも `textContent` で描いており、マークアップは注入されない。検証: **MySQL 8.0 で 624 passed**（時刻4パターンでも同数）。

### 2026-08-10

- **別担当が入れた AI 画像編集を監査し、課金の発生する経路の回数制限漏れを1件修正＋未記載だった仕様を文書化**: 前回のセッションから間が空き、その間に **PR#68〜#74 と別担当の v0.4.33〜v0.4.52**（初期設定の説明改善・**アカウント設定画面と AI 画像編集**）が入っていた。まず**現状把握を実測からやり直した** —— 手元の sqlite では3件落ちるが、これは**引き継ぎ資料 §4-0 のとおり環境の問題**（新しい `ai_provider_credentials` 等が古い sqlite に無い）で、**MySQL では全件緑**であることを確認してから監査に入った。**AI 画像編集は総じて堅く作られていた** — APIキーは `EncryptedSecret` で暗号化・`text` 列・`(user_id, provider)` unique・FK なし（手動作成テーブル混在の本番に配慮）、外部呼び出しに timeout、元画像は自サーバの `/storage` 配下のみ、テストも2ファイルあり、既存の自動走査（画面186・送信303）が新ルートを自動で拾っていた。**見つけた1件** — **接続テストだけ回数制限が無かった**。この接続テストは「無料枠ではキーは通るが画像編集は使えない」を見抜くために**実際に画像を1枚生成する**ので、**1クリックが利用者の請求**になる。画像編集側には `throttle:20,1` があるのに確認側は無制限という非対称で、連打や JS の繰り返しで請求が膨らむ。`throttle:10,1` を入れた。**対象外と判断したもの（理由つき）** — ①**他契約の画像を `source` に渡せる**が、`storage/app/public` は**元から誰でも URL で取得できる**領域なので新たな開示は起きない（渡すには相手のランダムなファイル名を知っている必要がある）。所有者チェックを足すとメディア以外の経路で置かれた画像の編集を巻き添えで塞ぐおそれがある ②**提供元の応答サイズに上限が無い**が、`BoundedResponseBody` は**利用者が貼り付けた URL** 用の仕組みで、ここは固定の信頼できるホストしか叩かない（実測でも応答8MB相当でピーク増分は約4MB）。**ここでも二重防御で変異が検知されなかった（3度目）** — `/storage` 配下の判定を丸ごと外しても、`../` を含む書き方は **Flysystem 側でも弾かれる**ため検知できなかった。**その層だけが効く入力**（ディスク上に実在するが `/storage/` で始まらないパス）を渡す形を足して、判定が単独で効いていることを確かめた。**変異4件で検知を確認**（接続テストの上限を外す／画像編集の上限を外す／`/storage` 判定を外す／キーの暗号化を外す）。**あわせてドキュメントの陳腐化を6件是正** — アカウント設定・AI画像編集が**仕様書にもマニュアルにも無かった**ので `funnel-spec.md` §15 と `manual.md` §6 を新設（対象外と判断した2件の理由も明記）、引き継ぎ資料の数字（609→621・141→142件）と直近サイクル表・落とし穴・次の宿題を更新、`docs/README.md` の「6つの形」を実態（7つ＋別枠）に是正。検証: **MySQL 8.0 で 621 passed**。

### 2026-08-05

- **LINEの画像・動画・Flex 配信を実装（利用者の必須要望・機能追加）**: 前日の調査で「LINE送信は全経路テキストのみ（`LineMessagingPushService` の公開メソッドが `pushText` の1つだけ・DB にも画像/動画/Flex の列が無い）」と確定していたため、**LINEステップ配信**と**LINE一斉送信**に画像・動画・Flex を追加した。**組み立てと検証の正本は `App\Support\Line\LineMessagePayload` の1クラス**にまとめ、ステップ配信・一斉送信・画面がすべてこれを通る形にした（別々に組むと「プレビューには出るのに届かない」という新しいずれを作るため）。**DB**: `2026_08_05_100000` で両テーブルに `message_type`（既定 `text`）・`media_original_url`・`media_preview_url`・`flex_alt_text`・`flex_json` を追加。**既存行は既定 `text` で従来どおり**動き、本文列（NOT NULL）には触れていない。**送信側**: `LineMessagingPushService::pushMessage()` を新設（`pushText` は互換のため残し、内部で `pushMessage` を呼ぶ）。**URLは https のみ**（LINE は http を受け付けない）で、メディアから選んだ `/storage/...` は **`APP_URL` 基準の絶対URLへ自動変換**して保存する（相対のままだと LINE から取得できない）。**Flex JSON は `bubble`/`carousel` でも、Flex Simulator が書き出す `{"type":"flex",...}` のメッセージ全体でも受け付ける**（貼り方が2通りあるため）。**保存時に送れる形かを検証**し、満たさなければ画面でエラーにする（保存できたのに届かない、を防ぐ）。送信時にも再確認し、送らずに理由を `last_error` に残す。**画面**は共通 partial `_line_message_fields` を2画面で共有（片方だけ直したときのずれを防ぐ）。**検査は「実際に LINE へ送られる JSON を読む」形**にした（DB や画面の値だけ見ても形4「保存はされるが送信処理が読んでいない」を拾えない）—— `Http::fake()` で捕まえて `messages[0]` を突合。**変異2件で検知を確認**（`build()` が常にテキストを返す＝機能が繋がっていない状態／https 検証を外す）。**未対応**: LINEチャット（手動送信）と LINE自動応答は従来どおりテキストのみ（別テーブルのため今回は対象外・仕様書に明記）。検証: 空DBから **migrate 142件完走**、全体 **608 passed**。

### 2026-08-04

- **配信まわりの全経路調査（利用者の依頼: フレックス・動画・画像などLINE特有の配信を含む）＋定期実行コマンドの異常系（不具合0件）**: 「ステップ配信・シナリオ配信・メール配信・LINE配信を全部調べて」というご依頼を、ソース・DB・マイグレーション・資料の事実で確認した。**LINEの配信形式は全経路テキストのみ**と確定 —— `LineMessagingPushService` の公開メソッドは **`pushText` の1つだけ**で、組み立てる messages は `'type' => 'text'` のみ。DB にも画像・動画・Flex の列は無い（`scenario_line_steps.message_text` / `scenario_line_broadcasts.message_text` / `line_chat_outbound_messages.body_text` / `scenario_line_auto_replies.message_body`。画像があるのは `scenario_line_rich_menus.image_url` だけで、これはリッチメニューの別機能）。**画面もそれらを提供していない**ので「出荷したが動かない」（形2）ではなく**そもそも未実装**であり、コード側にも明記がある（`ScenarioLineStepController::preview()` の docblock「LINE ステップはテキストのみ（送信側が `pushText` しか持たない）なので、画像・動画・Flex は出さない」）。自動応答の**「テンプレートを送信」は LINE のテンプレート/Flex ではなく**、画面に「暫定: テンプレ本文をテキスト返信」と明記されていて実装もそのとおり。資料（`funnel-spec` / `manual`）も画像・動画・Flex 配信を謳っていないため**文書と実装の食い違いは無し**。**機能フラグの漏れも0件**（Kernel が参照する7つの `config('putage.*')` がすべて `.env.example` にある）。あわせて**定期実行の配信コマンド5本を壊れた状態で実際に動かす** `DeliveryCommandRobustnessTest` を新設（何もデータが無い／アカウントがアーカイブ済み／LINEトークンが空／送信用アカウントを消した孤児シナリオ）。**不具合0件**。**ここでも自分のテストが空回りしていた** —— `putage:event-reminders` は設定 `event_reminders_enabled` が既定 false で**配信ロジックを一度も通らずに SUCCESS を返す**（しかもこのコマンドだけ `--force` を持たない）。`config()` で有効化し、**ゲートが開いていること自体を assert** する形に直した。**変異2件で検知を確認**（リマインダで例外／LINEステップ配信サービスの `run()` で例外 → コマンドが異常終了して落ちる）。**製品コードの変更なし**。検証: **603 passed**。

- **マージ後監査（PR#71）: キューのジョブを異常な引数で実際に動かすようにした（不具合0件）**: 今回も入ったのは自分のコミットのみだったので、資料が次に挙げていた**キューのジョブ**に移った。**`ProcessScenarioBroadcastChunkJob` / `ProcessScenarioLineBroadcastChunkJob` に触れるテストは0件**で、**ジョブ本体は一度も実行されていなかった**（画面と送信経路しか見ていなかった）。ジョブは利用者の目に触れないところで動くため、ここで落ちると「配信したのに何も起きない・理由も分からない」になる。`BroadcastJobRobustnessTest` で **`handle()` を直接動かす**5形（存在しない id・0・負数／行を消してから／親シナリオが消えた孤児／アーカイブ済みアカウント／送信経路が無い）を走査し、**不具合0件**。**DB の事実を確認して分かった非対称** —— `scenario_broadcasts.scenario_id` には **`cascadeOnDelete` の外部キーがある**が、`scenario_line_broadcasts` には**外部キーが無い**（`information_schema` と作成マイグレーションの両方で確認）。そのためシナリオを消すと**メールの配信行は一緒に消え、LINE の配信行は孤児として残る**。孤児は画面に出ず、拾ったジョブも `failBroadcast()` で理由を残して止まるため**対象外と判断**（直すには移行が必要でそちらの方が危険）。この非対称はテストで固定したので、どちらかが変われば落ちる。**最初のテストは前提を間違えていた** —— 「親を消すとメールの配信も孤児になる」と考えて書いたが、`refresh()` が `ModelNotFoundException` で落ち、**外部キーを確認して cascade だと判明**した（推測せず DB を見る、の実例）。**変異3件で検知を確認**（メールジョブの「行が無ければ return」を `findOrFail` にする／LINE の「シナリオが見つかりません」の記録を無言 return に変える／メールの送信経路チェックを常に true にする）。**製品コードの変更なし**。検証: **598 passed**。

- **マージ後監査（PR#70）: パートナー側の「他契約のデータが見える」を走査し、3つのガードすべてを網羅した（不具合0件）**: 今回も入ったのは自分のコミットのみだったので、最後に残っていた **`partner` ガード**に移った。既存の `PartnerPortalTest::test_portal_shows_only_own_data` は**同じ契約の中の別パートナー**しか見ておらず、**別契約**は走査していなかった（`profile` 画面は一度も確認されていなかった）。`PartnerCrossTenantAccessTest` を新設し、**走査の結果は不具合0件**。ポータルのルートは**引数を取らない**（自分のデータは認証情報から引く）ため「id を差し替える」形が無いので、契約を2つ作って**他契約だけが持つ目印が応答に出てこないこと**を全画面で確かめる形にした。更新（プロフィール・パスワード）は**状態で見る**（他契約の行が変わらないこと）。**ここでも変異が検知されず、原因は目印の選び方だった** —— 最初 `customer_name` を目印にしたが、**成果履歴の画面はその列を描いておらず**、金額も `number_format` で「777,777」になるため生の値では一致せず、`partner_id` の絞り込みを外しても**検知できなかった**（実測）。**画面が実際に描いている項目**（パートナー名・紹介コード・案件名・整形後の金額・口座番号）に直したところ、**変異3件すべてで検知**するようになった（成果履歴の絞り込みを外す／支払明細の絞り込みを外す／登録情報を他人のパートナーで描く）。**素通り防止**として「自分の目印が必ず出ていること」も同時に確かめる（画面がログインへ転送されているだけで緑になるのを防ぐ）。これで **`web`・`site_member`・`partner` の3ガードすべてに他テナント走査が入った**。**製品コードの変更なし**。検証: **593 passed**。

- **マージ後監査（PR#69）: 受講者側の「他サイトへ書き込める」も全経路で見張るようにした（不具合0件）**: 今回も入ったのは自分のコミットのみだったので、前回の続きで**未走査だった会員側の送信経路**（コメント投稿・受講完了・テスト解答・視聴記録）に移った。**応答ではなく状態で見る**（302 では「認可で弾かれた」「バリデーションで弾かれた」「通ってしまった」を区別できない）ため、**全テーブルの件数**を前後で突き合わせ**1行でも増えたら不合格**とした。3つの入り方 × 4種の送信＝**12経路を走査して不具合0件**。**空回り防止に「正の対照」を必ず入れた** —— 自分の正当な道筋への受講完了で**実際に行が増える**ことを先に確かめる（これが無いと、送信が別の理由で常に弾かれているだけの「何も書き込まないから緑」になる）。**ここでも二重・三重防御のせいで変異が検知されなかった** —— 書き込み経路は `authorizeCourse()`（サイト・受講権）・`assertLessonOnCourse()`（親子の対応）・`lessonIsUnlockedForMember()`（解放条件）の**三重**で守られており、`assertLessonOnCourse` を丸ごと無効化しても他が拾って**検知できなかった**（実測）。読み取り側と同じく**受講権の行が他サイトの結合を指している**状態を作って受講権検査を通過させ、**サイト検査が単独で書き込みを止めている**ことを確かめる形を足した。**変異で検知を実測**（サイト検査を外すと他サイトのレッスンに `member_lesson_completions` が 0→1 で記録されて落ちる）。**製品コードの変更なし**。検証: **591 passed**。

- **マージ後監査（PR#68）: 受講者側の「他サイトの教材が見える」を全経路で見張るようにした（不具合0件）**: 今回入ったのは自分のドキュメント修正のみだったので、**未走査のまま残っていた領域**に移った。`CrossTenantAccessTest` は**管理画面（`web` ガード）だけ**で、**受講者（`site_member`）は一度も走査していなかった**。ここが破れると「お金を払っていない人が他サイトの教材を見られる」ため、`MemberCrossTenantAccessTest` を新設。**走査の結果は不具合0件**。**入り方を4つ試す**のが要点 —— ミドルウェア `EnsureMemberBelongsToSlugSite` は**スラッグと会員の所属しか見ない**ので、①他サイトのスラッグ+他サイトのid（ミドルウェアが弾く）だけでなく、②**自分のスラッグ+他サイトのid**（素通りするのでコントローラが弾く必要がある）を必ず見る。さらに**変異で検知を確かめる過程で、前半2つでは不十分**だと分かった —— `authorizeCourse()` は「サイトが違う→404」と「受講権が無い→403」の**二重**で守っているため、**サイト検査を外しても受講権検査が拾って検知できない**。そこで③**受講権の行が他サイトの結合を指している**状態を作り、サイト検査が単独で効いていることを確かめる形を足した。同様に、他サイトのidで揃えた形だけでは**親子の対応検査（レッスンがそのグループのものか）を外しても検知できなかった**ので、④**自分の正当な道筋にレッスンだけ他サイト**を混ぜる形を足した。**変異3件すべてで検知することを実測**（サイト検査を外す／ミドルウェアの所属検査を外す／レッスンと親の対応検査を外す）。**fixture の落とし穴も踏んだ** —— 結合（`course_bundles`）に**コースを入れ忘れる**と `hasGrantForCourse()` が false になり、受講権を与えたつもりでも 403 で止まって**変異を検知できない**（`course_bundle_items` の紐づけが必須）。**走査自身の誤検知も1件自分で見つけた** —— 他サイトidを含まない画面（自分のお知らせ）まで対象にしていて 200 で誤検知していた（資料の落とし穴「走査系はまず自分を疑う」どおり）。**製品コードの変更なし**。検証: **589 passed**。

- **引き継ぎ資料の「すぐ再開する手順」が新しい環境で通らなかったので実測どおりに訂正（PR#68）**: 引き継ぎを受けて手順をそのまま実行したところ、**テスト全件が `Connection refused` で落ちた**（製品の不具合に見える形）。原因は3つ重なっていた —— ①**テストの接続先は `.env`**（`phpunit.xml` の sqlite 指定はコメントアウト済み）で、`.env` は `.gitignore` なので新しい環境に無い ②**sqlite ではスキーマを作れない**（ドライバ分岐のない MySQL 専用の生SQL が3件あり `migrate` が止まる。これは `degradation-policy.md` 形5 の「必ず本番と同じ MySQL で確かめる」判断の結果で**意図的**。資料の「開発DBは sqlite」で使われていた sqlite は**移行から作り直せない過去の遺物**だった） ③**MySQL 自体が未インストール**。さらに資料の `mysqld ... &` は**親シェルが終わると落ちる**ことも実測（`setsid` が必要）。§4 に「0) 環境を用意する」を新設し、`composer install` → `.env` 作成 → MySQL 導入・起動 → `migrate --force` までを**実際に通したコマンド**で記載。§3・§8 の sqlite の記述も訂正し、**ローカル PHP と CI(8.2)・composer(^8.1) が一致せず 8.2 以降の構文を手元でも CI でも検出できない**点を注意として追記。**資料の数値も監査した** —— 最初ルート単位で数えて「送信273→256」等と食い違い「資料が古い」と読みかけたが、**検査本体は verb ごとに数えている**（`AdminSubmitSmokeTest` の `foreach ($methods as $method) { $checked++; }`）ためで、合わせると**送信273・更新171・削除63・引数なし46 がすべて一致**。資料の数値は正しく修正不要だった（落とし穴「走査系はまず自分を疑う」を自分で踏んだ形）。

- **マージ後監査（PR#66）＋引き継ぎ文書の整備**: 前回入れた `CrossTenantAccessTest` には**明記した限界**があった —— 更新（POST/PUT/PATCH）は「認可で弾かれたのか、バリデーションで弾かれたのか」を応答から区別できないため対象外にしていた。今回それを**状態で見る形**で埋めた（対象の行の全カラム＋全テーブルの件数を前後で突き合わせ、**書き換わった／行が増えたら不合格**）。**171経路を走査して不具合0件**。**ここで検査自身の弱点も見つけた** — 最初の変異（`saveMemo` の認可を外す）が**検知されなかった**。空の送信は `admin_memo` を null にするが、fixture の値も元から null なので**差が出ない**（同じ値を書く操作は見えない）。fixture に**空の送信で消える値**をあらかじめ入れて再確認し、検知することを実測した（変異を戻すと 0 件に戻ることも確認）。この限界はクラスの docblock に明記。**あわせて引き継ぎ文書 `docs/HANDOFF-audit-20260804.md` を新設** —— 作業サイクル（マージ→監査→実測→変異確認→PR）、曲げないルール10件、7つの形と対応する自動検査の対応表、検査の網の数字（管理186画面・送信303経路・他契約 読138/書171/削63）、**すぐ再開するためのコマンド**（基準線の取り方・MySQL の立て方・実ブラウザの動かし方・応答量の測り方）、**落とし穴10件**（ルート1本の裏に63画面／走査系スクリプトのバグをまず疑う／上限は抑える軸を間違えやすい／応答が小さくてもメモリは増える 等）、残っている宿題、次に手を付けるとよい4領域を1枚にまとめた。`SPEC.md`・`docs/README.md`・`CLAUDE.md` の索引から辿れるようにした。検証: **585 passed**。

- **マージ後監査（PR#65）: 「他契約のデータが見える・消える」を全経路で機械的に見張るようにした（不具合0件）**: 今回は自分のコミット1本だけのマージで、そこに追加の欠陥は見つからなかったため、**未検査のまま残っていた領域**に目を向けた。**多テナントの分離**は、この repo で起こり得る不具合のなかで最も重い（他社のデータが見える／消える）のに、**全経路をまとめて見張る検査が無かった**。実際に3件見つかっている（ABテストのページ・パターンの所属・パートナーの他契約403・シナリオ一括プレビューの `ids[]`）が、いずれも個別対応だった。契約Bに一式のデータを作り**契約Aでログインして**叩く `CrossTenantAccessTest` を追加 — **管理画面（GET・idあり）75本＋ワークスペースの中の画面63枚**は 200 が返ったら不合格、**削除（DELETE）63本**は**対象の行が消えたら不合格**。削除を応答で見ないのは、消したうえで 302 を返す実装があり得るため（**状態で見る**）。**走査の結果は不具合0件** — 認可は全経路で正しく効いていた。**「0件で緑」を信じる前に変異3件で検知を確認**（ファネル詳細の認可を外す→該当画面が 200 で検知／シナリオワークスペースの認可を外す→60画面で検知／ファネル削除の認可を外す→実際に行が消えて検知）。**fixture の登録簿（約57モデル）は `Tests\Support\MakesAdminFixtures` に切り出して `AdminSubmitSmokeTest` と共有**（別々に持つと片方だけモデルを足したときにずれる）。ついでに登録簿の穴も塞いだ（`ScenarioBroadcast`・`ScenarioLineBroadcast` が無く、その2経路は**一度も叩かれないまま緑**だった）。素通り防止つき（解決できないパラメータがあれば落とす・拾えた画面数が少なすぎれば落とす）。**限界も明記** — 空の入力で叩くので、更新（PUT/PATCH）が認可で弾かれたのかバリデーションで弾かれたのかは区別しない。検証: sqlite・MySQL 8.0 の両方で **584 passed**。

- **マージ後監査（PR#64）: 別担当が入れた「まず〇〇を設定して」案内が、設定する場所そのもので出て堂々巡りになっていたのを修正**: 今回は自分の変更に加えて**別担当のコミット13本**（初期設定まわりの UX 改善・v0.4.20〜v0.4.32）が同時にマージされたので、そちらを重点的に見た。新規の `App\Support\FeatureSetupGate` は `layouts/app` に入っており**すべての管理画面**を通る（＝取り違えると影響が全画面に出る）。**見つけた1件** — **送信用アカウントの画面（`/account`・`/account/create`）で「まず『メール設定 または LINE設定』の設定を実施してください」と出て、「設定する」を押すと同じ画面に戻る**。`/account/create` に至っては**まさにその設定を作っている最中**に「先に作ってください」と言われる。初期設定フローからの流入（`?setup_channel=mail|line`）だけ除外していたため、**左メニューから来ると素通り**していた。しかも**会社情報だけ済ませた状態**（初期設定の順序どおりに進めた途中）でしか再現しないため、何も設定していない状態で見ると気づけない（最初の走査では「なし」と出て、状態を作り直して発見した）。`account.*` では配信設定を求めないようにした（会社情報は別の設定なので従来どおり案内する）。**問題なかったもの** — ダッシュボードの開始案内は「初期設定を1つでも行っていれば出さない」で誤警報なし（`||` の短絡評価で設定済みユーザーは1問い合わせ）。案内の追加問い合わせは機能ページのみ2本（`/funnel` 全体で4本）。オペレーターは `operator_identities` ログインが `Auth::login($owner)` なのでオーナーの設定で正しく判定される。`route()` が絶対URLな点は**ビュー全体の慣習**（相対は765件中3件）なのでここだけ直すのは筋が悪いと判断。**用語集アコーディオンは実ブラウザで確認**（既定は閉じる・クリックで開く・用語5件・各項目が展開・JSエラーなし）。**検査を新設**（`FeatureSetupNoticeTest` 5件）— 引数の要らない管理画面すべてを走査して**案内先が「いま開いている画面」でないこと**を見張る（素通り防止つき: 1画面も案内が出ていなければ落とす）。**変異2件で検知を確認**（修正前の形に戻す／案内を丸ごと止める）。検証: **582 passed**。

- **マージ後監査（PR#63）: 前回入れた上限が「抑える軸を間違えていた」ことを実測で発見（2回目の取り違え）**: 前回シナリオ数を 20 に制限したが、**枠の数はシナリオ数 × 通数**なので、1シナリオの通数が多ければ意味が無かった。実測（20シナリオ・メール+LINE）: **50通/シナリオ=3.5MB → 100通=7.0MB → 200通=14.1MB・ピークメモリ 88MB**（`memory_limit=128M` の目前）。**抑える軸はシナリオ数ではなく「描くステップ枠の総数」**だった。`StepPreviewBodyBudget` を **`StepPreviewBudget`** に改め、**通数（既定2000）と本文の合計文字数（既定200万字）を同じ器**で持たせた（別々に持つと片方だけ直したときにずれる。どちらもシナリオをまたいで合計で数える）。**さらにメモリだけが増える経路も見つけた** — 通数の上限は応答を抑えるが、**行は全部読んでから捨てて**いたため、20シナリオ × 500通で応答 3.5MB のまま**ピークメモリ 109MB**だった。`stepCountsByScenario()` で**行を読む前に通数だけ数え**、読む対象のシナリオを決めるようにした（読む行は「上限ぶん＋最後の1シナリオぶん」で頭打ち）。**結果、選んだ数・通数によらず 3.5MB・ピーク 43MB・9問い合わせ**（20×500 でも同じ）。通常規模（20シナリオ × 20通）は 1.43MB・ピーク 36MB で**従来と変わらない**。**シナリオ内のプレビューも同じ器を通す**ようにし、見出しには**実際の通数**を出す（描いた数だけ見せると規模を見誤る）。**変異4件で検知を確認**（一覧側の上限を外す／シナリオ内の上限を外す／予算の共有をやめる／行を読む前に数える処理を外す —— 最後の1件は応答が小さいままなので**読んだシナリオ数**で見張るテストを別に用意した）。**あわせて画面の量的な棚卸しも実施** — 管理14画面を1件→200件→2000件で実測し、**危険域に入るものは無し**（読者一覧はページ送りあり。シナリオ一覧 2000件=4.98MB・ピーク53MB、ページ一覧 2000件=5.08MB・ピーク61MB で、いずれも開く）。**ドキュメントの陳腐化はゲートが検知した**（クラス名を変えた結果 `DocFileReferenceTest` が赤くなり、`docs/funnel-spec.md`・`CLAUDE.md` の古い参照を是正）。検証: sqlite・MySQL 8.0 の両方で **576 passed**。

- **マージ後監査（PR#62）: 一覧プレビューが「すべて選択」で青天井になっていたのを実測で発見し、上限・まとめ取得・読み取り専用化の3件を修正**: 前回入れたシナリオ一覧の一括プレビューを、**推測せず実測**した。**利用者操作は実ブラウザで確認済み** — チェックボックスを押しても行のリンクへ遷移しない（行クリックの導線に `input` の除外がある）・ボタンの有効化と件数表示・**すべて選択**・選択を変えると取り直すこと、いずれも正常だった。**問題は規模だった** — シナリオ一覧は**ページ送りが無く**（`globalIndex` は全件取得）、**すべて選択が1クリックで全シナリオを選ぶ**のに、応答の大きさも問い合わせ本数も**選んだ数に比例して青天井**だった。実測（1シナリオ20ステップ換算）: **10件=1.7MB/55問い合わせ・30件=5.2MB/155・100件=11.2MB/505・ピークメモリ 82.5MB**（PHP 既定の `memory_limit=128M` に届くとパネルが開かない＝形1「画面が開かない」）。前回入れた本文の上限（`App\Support\StepPreviewBudget`・当時の名前は `StepPreviewBodyBudget`）は**本文だけ**を抑えるもので、番号・タイミング・差出人・件名の枠は必ず描くため、**通数そのものが増えると効かない**（＝上限を入れたつもりで抜けていた）。**①一度に描くシナリオ数の上限** `ScenarioController::PREVIEW_MAX_SCENARIOS = 20` を入れ、**黙って落とさず**「残り〇件は表示していません」と件数を画面に出す。**②まとめ取得** — ステップ・カスタム送信者・拡張設定を `whereIn` で1回ずつ引くようにし、シナリオごとの問い合わせを廃止。**③プレビューは読むだけにした** — 拡張設定は `ensureForScenario()`（無ければ作る）を使っており、**見るだけの操作で選んだ数だけ INSERT** が走っていた。読み取りのみに変え、未作成なら `null` のまま渡す（`ScenarioMailComposer` も画面も `null` を受け取れることを確認）。**結果、選択数によらず 3.45MB・9問い合わせ**（100件選んでも同じ）。**変異3件で検知を確認**（上限を外す／シナリオごとに引く形に戻す／`ensureForScenario` に戻す）。検証: sqlite・MySQL 8.0 の両方で **570 passed**。

- **シナリオ一覧から「選んでプレビュー」できるようにした（利用者の要望）**: 「中に入らないと見れないのだと、どのシナリオが何かをすぐにチェックできない」というご指摘。各行に**チェックボックス**を置き、ツールバーの **選んだシナリオをプレビュー** で、選んだシナリオの**メール・LINE両方**の配信内容を1つのパネルに並べるようにした（`scenario.global.preview`）。**描画はシナリオ内の一括プレビューと同じ partial** を通す（別に組むと片方だけ直したときに食い違う）。**所有権はサーバ側で必ず確認** — `ids[]` は画面から書き換えられるので送信用アカウントの `user_id` で絞り、**出せなかった件数は画面に出す**（黙って減らすと理由が分からない）。**本文の合計量の上限は `App\Support\StepPreviewBudget`（当時の名前は `StepPreviewBodyBudget`）に切り出して共有** — シナリオごとに上限を持つと10個選べば10倍になりパネルが開かなくなるため、選択したシナリオ**全体**で数える（シナリオ内プレビューも同じ部品を使うようリファクタし、上限ロジックの二重管理を解消）。未紐づけのチャネルは「メールは紐づいていません」と明示（黙って空白にしない）。**実機で確認** — チェック0件ではボタンが押せない・1件選ぶと有効になり件数が出る・選択を変えると中身を取り直す・すべて選択で2シナリオ並ぶ・JSエラーなし。**変異2件で検知を確認**（所有権チェックを外す／予算をシナリオごとに持つ）。検証: 全体 **567 passed**。

- **「プレビューボタンがどこにもない」の原因は導線の欠落だった（利用者からの指摘）**: シナリオ一覧の画面を見ながらのご指摘。調べたところ**2つの原因**があった。①**LINEステップ配信が左メニューに無かった** — メールの「ステップ配信」は `layouts/app.blade.php` にあるのに、LINE側だけ抜けており、**シナリオ概要のタイルからしか行けない**状態。お客様のシナリオは**メール未設定・LINE設定済**で、その場合メール管理のセクションは丸ごと隠れる設計なので、**メニューのどこにも「ステップ配信」が出てこない**（実機で左メニュー18項目を列挙して確認）。2026-07-27 の LINEステップ配信実装時のサイドバー追加漏れ＝形3「出荷したが到達できない」。②**ステップ0件だとボタンを隠していた**（私の設計判断）— 「機能そのものが無い」と読まれるので、0件でも出すようにした（パネル側は「ステップがありません」と出す）。**検査の素通りも自分で発見** — 到達性テストをページ全体で書いたら、**概要ページのタイルに一致して素通り**した（サイドバーのリンクを消しても緑）。`sidebarHtml()` で**左メニューの範囲だけ**を切り出して見るように直し、変異2件（サイドバーからリンクを削除／0件でボタンを隠す）で検知を確認。検証: 全体 **560 passed**。

- **マージ後監査（PR#61）: プレビューを実ブラウザで検証し4件修正／LINE自動応答のエラーが画面に出ていなかったのを修正**: ご指摘の観点（画像・動画・リンク・LINEエラーの取り込み）を**推測せず Chromium で実測**した。**動いていたもの**: 画像は表示（`sandbox` は画像を妨げない）・本文のCSSは iframe 内で有効・共通ヘッダー/フッターも描画・`<video>` タグも残る（スクリプトは動かないので実際のメールクライアントと同じ）。**直した4件** — ①**本文中のリンクを押すとプレビュー枠の中がリンク先に置き換わり**、内容確認の続きができなくなっていた（実測）。`<base target="_blank">` を srcdoc 先頭に足し sandbox に `allow-popups allow-popups-to-escape-sandbox` を付与して**別タブで開く**ようにした（`allow-scripts` は付けない＝本文のスクリプトは動かさない）。実測で新タブが開きプレビューが無事に残ることを確認。②**本文が長いと 420px の枠内スクロールのみ**だったので「**本文を広げて見る**」トグル（80vh）を追加（実測 420→576px）。iframe 内はスクリプトを止めているため高さを測れず自動リサイズはしない。③**LINE本文の URL がただの文字列**だった（LINEアプリは自動リンクする）。`App\Support\LinePreviewText` でエスケープ後に安全にリンク化（順序を逆にすると利用者のHTMLが動くため順序が肝）。④**Bootstrap の JS が読めないと、ボタンが何もしない**（`bootstrap.Offcanvas...` が先に例外→fetch に到達せず「読み込み中…」のまま）。取得を先に始め、開く処理を保護した（CDN 遮断を再現して実測）。**LINE配信エラーの取り込みも精査** — ステップ配信は3経路すべて記録・一斉送信も記録されていたが、**LINE自動応答だけ `Log::warning` のみで画面に出ていなかった**。`sourceLabel()` には以前から `auto_reply`（LINE自動応答）の表示名があった＝**画面は受け皿を持っていたのに書き込む側が無かった**形。自動応答は読者のメッセージをきっかけに裏で動くため、失敗しても運用者は気づけない（読者に返信が届かないまま）。アクセストークン未設定と API エラーの両方を記録するようにした（記録の失敗で自動応答は止めない）。チャットは手動送信で失敗がその場で見えるため対象外と判断。**リッチメニューは既に専用プレビュー**（画像＋タップ領域）があり対象外。**テストの素通りも自分で発見** — 自動応答の fixture の `action_type` が誤りで返信自体が走っておらず、成功系テストが「送っていないから0件」で通っていた。`Http::assertSent` を足して塞いだ。**変異3件で検知を確認**。検証: sqlite・MySQL 8.0 の両方で **558 passed**。

- **マージ後監査（PR#60）: 過小評価していた「本文が大きいとパネルが開かない」を実測で発見し、見える形で上限を入れた**: ご指摘のとおり影響範囲を見直したところ、**一括プレビューが応答サイズ青天井**だった。本文は1通50万文字まで保存でき、**実測で 60通 × 約469KB → 応答 27.5MB・ピークメモリ 115MB**（PHP 既定の `memory_limit=128M` にほぼ届く＝**70通あたりでパネルが 500**）。**本文の合計200万文字**を上限にし、超えたぶんは**本文だけ**省略するようにした。**黙って減らさない** — 番号・タイミング・差出人・件名は必ず出し、本文の代わりに編集画面へのリンクと「省略した」案内、パネル冒頭に省略件数を出す。**同じ60通で応答 5.6MB・ピーク 71MB、件名60件はすべて表示**されることを実測。変異（上限を無限大に戻す）で検知を確認。**あわせて「抜け漏れ」を全経路で確認** — シナリオ設定を読むメール送信経路を洗い出し、**リマインダ配信（`ScenarioReminderDeliveryService`）だけ composer を通していない**のは**意図的な正解**と確認した（`scenario_reminders` は `body_text` 列しか持たず `Mail::raw()` でプレーンテキストを送るため、HTMLのヘッダー/フッターを足すとタグが読者に見える。件名もイベント名入りの既定を自前で持つ）。後から善意で繋がないよう、クラス docblock と仕様書に**通す経路／意図的に通さない経路**を明記。**DB・マイグレーションの変更はゼロ**（今回の機能は既存列のみ）。**その他の確認**: シナリオのメール用アカウントがルートのアカウントと違う場合もプレビュー・編集リンクとも 200（実測）、offcanvas は id 固定のため**1ページ1つまで**の制約を partial に明記。検証: sqlite・MySQL 8.0 の両方で **554 passed**。

- **ステップ配信に「一括プレビュー」を追加し、保存されるだけで送信に使われていなかったメール設定3つを接続（機能追加＋形4の修正）**: ①**一括プレビュー** — メール／LINE のステップ配信一覧に **一括プレビュー**ボタンを置き、**右から出るパネル**（Bootstrap Offcanvas）に全ステップを**配信順**（`timelineSortKey()`・一覧と同じ）で表示する。それまで本文を読むには1通ずつ編集画面を開くしかなく、何通のシナリオを組んだのか俯瞰できなかった。**メールは差出人・件名・本文を実際に届く形で**、**LINE はトーク風の吹き出し**（カスタム送信者の名前・アイコン反映）で出す。本文は**押したときに取りに行く**（本文HTMLは1通最大50万文字あり、一覧に埋めると極端に重くなる）。②**メール本文は `<iframe srcdoc sandbox>` に隔離** — 利用者の書いたHTMLを管理画面へ直接埋めると `<style>` や閉じ忘れタグが管理画面のレイアウトを壊す。iframe なら CSS も DOM も分離され、`sandbox=""` でスクリプトも動かない。③**保存されるだけだったメール設定3つを接続（形4）** — シナリオ設定の **デフォルト件名・デフォルトメールヘッダー／フッター（HTML）** は DB には正しく保存されていたが、**送信処理から一度も参照されていなかった**（`$fillable` は正しいので `MassAssignmentTest` も実行時の `preventSilentlyDiscardingAttributes` も検知できない形）。画面のラベルは「デフォルトメールヘッダー（HTML）」なので利用者は当然入ると読む。**`App\Services\ScenarioMailComposer` に組み立てを集約**し、ステップ配信・メール一斉送信・一括プレビューの**3つとも同じ経路を通す**ようにした（片方だけ直すと「プレビューには出るのに届かない」という新しいずれを作る）。**テキスト形式には付けない**（HTMLタグがそのまま読者に見えるため）。④**プレビューに出さないもの** — `preview_text`（プレビュー一行テキスト）は保存のみで送信に使われていない（仕様書に明記済み）。パネルの約束は「実際に届く形」なので**あえて表示しない**。⑤**導線URLは相対**（`route(..., false)`）— 独自ドメインで `APP_URL` と実ホストがずれると別オリジンへ取りに行くため（過去に同型の事故あり）。⑥**一覧のタイミング表記も `timingLabel()` に統一**（画面ごとのベタ書きを解消し、一覧とプレビューで文言が揃う）。**変異3件で検知を確認**: ヘッダー/フッターを付けない・送信側が composer を通らない（＝修正前の状態）・プレビューの並びを作成順に戻す、のいずれもテストが落ちる。検証: sqlite・MySQL 8.0 の両方で **553 passed**。

### 2026-08-03

- **マージ後監査（PR#59）: CIテストゲートの初回実行が本番CIで成功・取り残し回収の完全性を検証（不具合0件）**: ①**前回の取り残し事故の検証** — PR#58 がマージされた**後**に全体精査のコミットを同じブランチへ push してしまい、main に入っていなかった（PR#59 で cherry-pick 回収）。回収の完全性を `git diff` で機械的に確認 — 孤児コミットと main の差分は**その後のマップ修正3ファイルのみ**（＝100%回収）。**再発防止の教訓: push する前に PR の状態（open/merged）を確認する。マージ済みPRのブランチへの push は誰にも届かない**。②**CIテストゲートの初回実行を GitHub Actions の実ランで確認** — fresh-migrate ジョブで空DB migrate（61秒）→ `php artisan test` **542件 success（47秒）**→ deploy ジョブも完走し本番反映まで正常。ゲートが実CIで機能することを実測した。③ファネルマップのメール/LINE導線修正はマージ後も 543 passed・仕様書 §3.1（ファネルマップの意味）にチャネル区別の記述を追記（実装だけ変えて仕様書が旧挙動のままにならないように）。

- **全体精査（ドキュメント×実装のMECE突合）: 文書の矛盾7件を是正・CIにテストゲートを追加・製品コードの不具合は0件**: 5軸（A文書↔実装 / B設定・環境変数 / C死蔵・参照 / D DB↔モデル / E既存ゲート）で機械的に突合した。**B: 問題なし** — config参照38種欠落0・`app/` 直の `env()` 0件・`.env.example` との差分はすべて意図的（MONAKA_エイリアス・コメント記載・フレームワーク標準）。**C: 死蔵0件** — ルート未接続コントローラ0（最初の走査は89件出したが私のスクリプトのエスケープバグ。直すと0）・未参照クラス0。**D: 問題なし** — 全100モデルの `$fillable`×実列で不一致0。**A: 文書の「未実装」33件を全部実装と突合し、7件が実装済みなのに未実装と書かれたままだった** — ①`membership-site-settings-design.md` の「Webhookから会員作成・自動付与＝未接続・**最重要ギャップ**」（実際は 2026-05-29 に接続済み。`OrderPurchaseFulfillmentService` が会員自動作成＋付与＋シナリオ登録まで実行）②同・コースカテゴリ「未実装」（同文書§3と自己矛盾）③`partner-spec.md` の「パスワード再発行は未実装・ログイン画面にも明記」（実装済みでログイン画面にはリンクが**ある**。サポート対応を誤らせる）④`contract-spec.md` の「配置パーツのリサイズ未実装」（実装済み）⑤`funnel-spec.md:784` 「視聴完了判定は未実装」（同文書811と自己矛盾）⑥同809「設問シャッフル未実装」（同じ行の前半で保存項目として列挙済み）⑦同604「`after_purchase_*` 未接続」（同文書603・manual.md と自己矛盾）。**索引の穴も是正** — 判断基準の正本 `degradation-policy.md`・UTAGE取り込みの正本 `Convert_utage_to_monaka.md` を含む13ファイルがどの索引にも無かった（docs/README.md に追加・歴史的記録の節を新設・partner行の「成果自動記録が未接続」も是正）。**E: CIがテストを一度も実行していなかった** — ゲートは fresh-migrate と view 参照だけで、542件のテストはローカル頼み。fresh-migrate ジョブの MySQL に対して `php artisan test` を実行するステップを追加。**このとき、開発用 sqlite にしか無い旧列にテスト6件が依存していたことが判明** — `member_bundle_grants.bundle_course_id` は `2026_05_26_120000` が削除するので正本スキーマには無いが、開発用 sqlite には NOT NULL で残存。`TestCase::insertMemberBundleGrant()`（列がある場合だけ入れる）で新旧両対応にし、**sqlite・空DBから作った MySQL の両方で 542 passed を実測**（`partner_conversions.source_id` に文字列を入れていた fixture も整数列の定義に合わせて修正 — sqlite は黙って通すが MySQL strict は弾く）。

- **「型の嘘」を機械的に見張るようにした — 同じ形の横断調査で他は0件を確認（マージ後監査）**: `line-registration-routes` の 500 は「`show(): View` のまま転送を返す分岐を足した」形で、**条件分岐の中に隠れる**ため実測の検査（既定の経路しか通らない）では拾いきれない。同じ形が他に無いか、全コントローラ（public 468本・うち View/Redirect/Json の型宣言 247本）を静的に走査した — **不一致は0件**（現物はきれい）。最初の走査は6件出したが、**全部私のスクリプトのバグ** — メソッド境界を「戻り値型のある関数」だけで区切ったため、型の無い隣のメソッドの本体が混ざっていた（ドキュメント精査のときの `App\`→`app/` と同じ型の失敗。**走査系は境界の取り方をまず疑う**）。境界を直すと0件になったが、「0件で緑」を信じる前に**修正前のコードを取り出して検出できることを実測**してから採用した。恒久化は `App\Support\ControllerReturnTypeScanner`＋`ControllerReturnTypeTest` — 素通り防止（走査数が少なすぎたら落ちる）に加え、**検知能力の証明をテスト自身に含めた**（実際に起きた形と逆方向の形のプローブを与えて検出すること・境界ずれで誤検知しないことを毎回確かめる）。限界も docblock に明記（`match` の腕・メソッド内クロージャは見ない。一次の網は実測のスモークテスト）。**変異で確認**: 戻り値の型を `View` に戻すと該当行を名指しで検知。検証: 全体 **542 passed**。

- **「保存ボタンを押した後」が丸ごと未検査だったのを解消 — 送信経路303本を実測し、製品側の不具合はゼロ（未検査部分の検査）**: これまでの画面検査はすべて GET（開くか）で、**送信経路（POST/PUT/PATCH/DELETE）は個別テストのある機能しか見ていなかった**。管理側 **273経路**・公開／パートナー／受講者側 **30経路**を空の入力で実測。**500 になる製品側の経路は1本も無かった**（プローブ中に出た2件の 500 は両方とも**私の fixture の誤り** — ①ビルダーのテンプレートキーに実在しない値を使った ②これで `back()` が `未対応のテンプレート` 例外になった。実在キー `email_lead` では正常）。**見つけた挙動はすべて実装を読んで設計どおりと確認** — 空の回答でビルダーが進むのは `resolveAnswer` が「あとで設定」に倒す設計、`saveMemo` の空保存はメモのクリア、グローバルのシナリオグループ経路にアカウント配下グループを渡すと 403 は認可が正しい、コメント無効レッスンへの投稿 403・テストでないレッスンへの解答 404 も設計どおり。恒久化は `AdminSubmitSmokeTest`＋`PublicSubmitSmokeTest` — **ルートパラメータはコントローラ引数の型（リフレクション）からモデルを特定**し、登録簿（約50モデル）の作成手順で実体を作って埋める。**新パラメータは「未解決」で落ちる**・**404 が残ると落ちる**（＝送信処理を実行できていない）・**403 は期待経路を明示**・公開側は**設計どおりの応答を値で固定**（購入=決済未設定なら503・LINE署名なし=400 など）。**空の入力という限界も明記**（正しい入力の保存結果までは見ない。形4は `MassAssignmentTest` の担当）。**変異で確認**: `FunnelController::update` と `PublicScenarioRegistrationController::storeWeb` の先頭で例外を起こすと、それぞれ該当経路が 500 で検知。検証: 全体 **538 passed**。

- **「1本のルートの裏に63画面」を見落としていた検査の穴を塞ぎ、埋もれていた HTTP 500 を1件修正（マージ後監査）**: 画面の検査は110画面まで来ていたが、**数え方が間違っていた** — `.../workspace/{slug}` は**ルートとしては1本**なのに、実際はスラッグごとに別の画面（送信用アカウント18・シナリオ31・イベント7・コース7＝**63画面**）。ルートを列挙する作りだったため、**既定のスラッグ1枚しか開いていなかった**。全スラッグを実際に叩いたところ、**`line-registration-routes` が HTTP 500**（真っ白）だった。**原因は戻り値の型** — 2026-05-19 に「登録経路」をメール/LINE のタブへ分割したとき、旧スラッグは `registration-routes?tab=line` へ**転送する互換経路**として残したが、`show()` の戻り値が `View` のままだったため、転送しようとした瞬間に `TypeError`（`RedirectResponse returned`）になっていた。**古いブックマーク・社内メモ・外部リンクから来た人が踏む**。`View|RedirectResponse` に直し、**転送先が実際に開くところまで**見る `ScenarioWorkspaceLegacySlugRedirectTest` を追加（「開くか」だけでは行き先の間違いを拾えない）。**あわせて死蔵の分岐を削除** — 転送するので `workspace.blade.php` の `@elseif($slug === 'line-registration-routes')` と `_line_registration_routes.blade.php` は**決して描画されない**（タブ分割時の取り残し。`UnreferencedViewTest` は「参照はある」ので気づけない形）。**検査の穴は他にも塞いだ** — 契約書・パートナー・独自ドメインなど「下ごしらえが重いから」で外していた **26画面**も対象にした（外したままだとそこだけ検査の外に残る）。**製品側の不具合はこの1件のみで、残り89画面はすべて正常**。スラッグ一覧は**コントローラの定数から読む**ので、後からスラッグが増えても自動で検査対象になる。**変異で確認**: 戻り値の型を `View` に戻すと該当画面が 500 で検知。これで画面の検査は **管理186＋公開8＋受講者5**。検証: 全体 **536 passed**。

### 2026-08-02

- **初期設定トップの3度目の刷新でテストが2件落ちた：製品は正しく、自分のテストが見た目に密着しすぎていた（マージ後監査）**: v0.4.19 で初期設定トップが「できること別グループカード」に作り直され、`SetupHubBadgeVocabularyTest` が2件落ちた。**調べたところ製品は正しい** — 9つの設定はすべて残り、`variant === 'done'` の判定も健在（`($it['card']['variant'] ?? '') === 'done'` という書き方に変わっただけ）で、むしろグループごとの完了数表示やチェックアイコンが増えて改善していた。**落ちた原因は私のテスト** — ①語彙の判定を `['variant'] === '...'` という**特定の書き方**で正規表現照合していた ②画面の検査が `<span class="su-row-status is-done">設定済み</span>` という**マークアップの完全一致**だった。この画面は2日で3回作り直されており、見た目に密着したテストは作り直すたびに誤検知する（＝ゲートとして信用されなくなる）。①は書き方に依存しない形に緩め、②は**設定前後の差**（`is-done` の出現回数が増えるか）で見るようにした（素の文字列検索だと**スタイル定義にも一致して素通り**するため、差分で見るのが要点）。**あわせて作り直しで一番こわい「設定がどれか1つ画面から消える」を見張る検査を追加** — 見た目ではなく**リンク先**で9つすべてへ到達できることを確かめる（作りが変わっても意味が保たれる）。**通知設定のカードを画面から外すと落ちること**を確認。検証: 全体 **533 passed**。

- **イベントリマインダが二重送信され得た（形6 の棚卸しで発見・マージ後監査）**: 6つの形で唯一体系的な検査が無い「形6（二重に走る）」を、定期実行ごとに棚卸しした。**決済（1回の UPDATE で確保）・LINE webhook（記録を先に確保）・メール/LINEステップ配信（`Cache::lock`）・パートナー成果（unique）はすべて守られていた**が、**イベントリマインダ（旧固定）だけ防護が無かった**。対象の絞り込みは `reminder_*_sent_at IS NULL` なのに、その印を**送信した後**に付けていたため、5分ごとのコマンドが5分以内に終わらないと次の実行が同じ予約を拾い、**お客様にリマインダが2通届く**。`scenario_mail_deliveries` 側にもこの経路の unique は無く、イベントにシナリオが無ければ `reserveEventReminder` は null を返して何も守らない。決済と同じ「**1回の UPDATE で確保する**」形にし、**失敗したら確保を戻す**（そうしないと送れなかったのに送った扱いになり再送されない）。あわせて定期実行6本に `withoutOverlapping()` を追加し、**有効期限を明示**した（既定 24 時間のままだと、プロセスが異常終了してロックが残ると**まる1日配信が止まる**）。**テストの作り方でも一度つまずいた** — 「コマンドを2回続けて呼ぶ」だけでは重なりを再現できず（1回目が終わってから2回目が始まる）、修正前の形に戻しても**素通りした**。**送信が起きた瞬間に印が付いているか**を見る形に変えて、変異で落ちることを確認。検証: 全体 **532 passed**。

- **ドキュメントの精査：正本の仕様書が実在しないファイルを指していた1件を修正（マージ後監査）**: ご指定の観点でドキュメントを機械的に洗った。**正本系ドキュメントが参照するファイル・クラス・ルート名**を実在と突き合わせ、次が分かった。**①ルート名は 0 件欠落**。**②クラス参照も 0 件**（最初 49 件出たが、`App\` → `app/` のパス変換を誤った**私のスクリプトのバグ**で、既知の実在クラスが並んだ時点で気づいた）。**③ファイル参照は 10 件中 1 件が本当の取り残し** — `docs/funnel-spec.md` が **2026-07-28 に削除済みの `funnel/page-editor.blade.php`** を「二重管理に注意」と書いたままだった（残り9件は `resources/views/` を省く略記5件と、計画書・監査スナップショットという歴史的記録4件で正常）。**これは実害のある形** — 同じ 2026-07-28 に「仕様書が死蔵側を正本と書いていたため直したつもりが反映されない」事故が起きている。`DocFileReferenceTest` で**正本系ドキュメントだけ**を見張るようにした（計画書・監査スナップショットは「これから作るもの」を書いてよい場所なので対象外。ここまで見ると誤検知だらけでゲートとして信用されなくなる）。素通り防止つき・**変異2件で検知を確認**。テーブル名の突合せは列名や JSON キーと区別できず誤検知だらけになるため**不採用**とした。検証: 全体 **529 passed**。

- **受講者（会員）がログイン後に見る画面も検査対象にした（マージ後監査）**: 公開側は前回入れたが、**`site_member` ガードの5画面（マイページ・お知らせ・パック・レッスン一覧・レッスン）は一度も描画されていなかった**。既存の会員系テスト（`MemberCatalogPreloadTest` 等）はサービス層だけで、画面を開いていない。ここが 500 になると**「お金を払ったのに見られない」**になるため、公開側と同じ扱いにした。受講権（`member_bundle_grants`）まで作って実測し、**5画面すべて正常・製品側の不具合は無し**。変異で確認: 会員レイアウトを隠すと5画面すべてが 500 で検知。これで画面の検査は **管理97＋公開8＋受講者5＝110画面**。検証: 全体 **527 passed**。

- **公開側（お客様が見る画面）も実際に開くか検査するようにした＋現状の棚卸し（マージ後監査）**: 管理画面は97本まで見たが、**公開側は1本も見ていなかった**。公開ページが 500 になるのは管理画面より影響が大きい（見ているのが利用者ではなく**その顧客**のため）。公開の GET は **31本**あり、うち顧客動線の中心8本を実測した。**製品側の不具合は無し** — 6本は正常に開き、残り2本は**意図的に開かない**ことを確認（登録完了ページはセッションが無ければ 404＝URLを直に叩かせない、購入ページは決済連携が未設定なら 503＝買えないのに購入画面を出さない）。あわせて **503 のときに運営者向けの文言（「商品登録で公開してください」等）がお客様に出ないこと**も `APP_DEBUG=false` で実測（出ない）。`PublicScreenSmokeTest` で恒久化し、意図的に開かない2画面も**その状態のまま**であることを固定した（「まあ 404 だし」で流すと本当に壊れたとき気づけない）。変異で確認: 公開ページの partial を隠すと `/p/{uid}` が 500 で検知。**現状把握（依存関係・DB・マイグレーション）**: `composer.json` 妥当・platform 要件 OK、マイグレーション 140本、`$fillable` × 実在の列の不一致 **0件**（MySQL の正本スキーマで確認）。テストDBで Pending 6件はいずれも既知の MySQL 専用マイグレーションで、**本番同等の MySQL での完走は CI の `fresh-migrate` が担保**。検証: 全体 **526 passed**。

- **「ID が要る画面」も実際に開くか検査するようにした（マージ後監査）**: 前回入れた `AdminScreenSmokeTest` は**引数の要らない画面46本しか見ていなかった**が、**2026-07-27 の事故はまさに ID 側**（`/funnel/{id}/map` ほか6画面が 500）で起きており、前回の検査ではその事故を検知できない。ID が要る管理画面は **84 本**あり、検査されていたのは `FunnelDetailScreensTest` の 7 本だけだった。ファネル・送信用アカウント／シナリオ・会員サイト・イベント・商品を1件ずつ作り、そこから開ける **51 画面**を叩くようにした。**製品側の不具合は無し**（引数なし46＋ID付き51＝**97画面すべて 2xx/3xx**）。変異で確認: **`funnel/partials/detail-nav.blade.php` を隠すと `/funnel/{id}/map` ほかが 500 で検知**（＝過去の事故そのものを再現して捕まえられる）。契約書・パートナー・独自ドメインの ID 付き画面は下ごしらえが重いため対象外とし、理由を `docs/degradation-policy.md` に記録。検証: 全体 **523 passed**。

- **管理画面が「実際に開く」ことを全画面で検査するようにした（マージ後監査）**: ここ数回は自作テストの監査が続いたので**製品側**に目を向けた。「画面が開かない」は 2026-07-27 に実際起きた事故（partial 欠落でファネル詳細7画面が 500）だが、既存の検査は**参照先のビューが実在するかしか見ておらず**、変数の渡し忘れ・リレーション切れ・コントローラの例外では落ちない。**引数の要らない管理画面46本をすべて実際に GET** する `AdminScreenSmokeTest` を追加した（`funnel-builder` は機能フラグが既定 off で 404 になるため、テスト内でフラグを立ててから叩く）。**全画面 200 で、製品側の不具合は無し**。**素通り防止**として拾えた画面数が少なすぎたら落とすようにしてあり、実際これに助けられた — 最初 `gatherMiddleware()` をクラス名（`Middleware\Authenticate`）で照合したが、**middleware は別名（`auth`）のまま返る**ため0件になっていた（数の検査が無ければ「0画面を検査して緑」だった）。変異で確認: **partial を1つ隠すと `/dashboard` が 500 で検知**（過去の事故そのもの）、コントローラで例外を起こすと `/setup` が 500 で検知。検証: 全体 **522 passed**。

- **「日本時間で出す」テストが、それを検証していなかった（マージ後監査）**: 前回の教訓（検査は実測しないと意味が無い）を**自作テスト全体に当てて棚卸し**した。控えの状態（機能が無い状態）で画面を描画し、アサーション文字列が出ないかを数えたところ、ステップ配信まわりは**すべて 0**（素通りしていない）だったが、**ダッシュボードの `日本時間` は印が1つも無くても2回出る**ことが判明。さらに `SystemHeartbeatService` から**タイムゾーン変換を丸ごと外しても9件すべて通った**（実測）。原因は期待値も `config('app.timezone')` から作っていたため両辺が同じ値になっていたこと。**製品側にも実際のずれがあった** — 画面は「日本時間」と決め打ちなのに、値は `config('app.timezone')` 依存だった（いまは Asia/Tokyo なので表示は同じだが、設定を変えると**ラベルだけが嘘になる**）。ユーザーの要望は「日本時間で出す」なので `SystemHeartbeatService::DISPLAY_TIMEZONE = 'Asia/Tokyo'` に固定し、テストは `config(['app.timezone' => 'UTC'])` にしても日本時間で出ることを見るようにした。ダッシュボードの検査も「`日本時間` という語」ではなく**実際に記録した時刻**を見るようにした。**両方とも変異で落ちることを確認**（変換を外す／画面から時刻を消す）。検証: 全体 **520 passed**。

- **前回自分が入れた検査が、ほとんど何も見ていなかった（マージ後監査）**: 初期設定トップの状態バッジを見張る `SetupHubBadgeVocabularyTest` は `'variant' => '...'` という**直接記述しか見ていなかった**が、**実際のカードはほぼ全部 `hubCountBadge($count, '件登録済み', 'done')` とヘルパーの第3引数で渡している**。プローブで確認したところ、引数に `success` を混ぜても**素通りした**。ヘルパー引数（三項演算子の中も含む）も見るように広げ、**3つの書き方すべてで検知することを実測**。あわせて**自分の E2E アサーションが素通りだった件も是正** — `assertStringContainsString('is-done', $html)` は**スタイル定義（`.su-row-status.is-done { ... }`）にも一致する**ため、何も設定していない利用者でも通る（実測: 出現回数 1）。描画された要素（`<span class="su-row-status is-done">設定済み</span>`）を狙うようにし、**変異で落ちること**を確認。教訓は前回と同じで、**検査は「落ちるべきときに落ちるか」を実測しないと意味が無い**。検証: 全体 **520 passed**。

- **初期設定トップの刷新を監査し、状態バッジの語彙違いを1件修正（マージ後監査）**: 初期設定トップ（`setting/setup.blade.php`）が横長の行スタイルに刷新されたため、**9枚のカードが1枚も欠けていないこと**（＝どの設定も画面から到達できなくなっていないこと）、画面が 200 で開くこと、静的検査が通ることを確認。**1件見つけた** — 画面は `variant === 'done'` のときだけ「完了」の見た目（緑・太字）にするが、**通知設定のカードだけが Bootstrap の色名（`success` / `warning` / `secondary`）を返しており**、他8枚が共通語彙（`done` / `progress` / `attention` / `unset`）なのに揃っていなかった。そのため**設定済みでも完了に見えない**（ラベルは「設定済み」なので実害は見た目のみ）。ステップ配信で起きた「DBに入り得る値が画面の選択肢に無い」のと同じ形。呼び出し元がハブ1箇所だけであることを確認してから共通語彙に統一し、`SetupHubBadgeVocabularyTest` で**画面の知らない値が混ざらないこと**を見張るようにした（**`success` に戻すと落ちること**を確認）。**影響が無かったもの**: 新しいCSSは5画面で共有される partial に入ったが、新クラス（`su-row` 等）は初期設定トップでしか使われておらず、他4画面は `layouts.app` 経由で二重 include も無いため見た目は変わらない。検証: 全体 **519 passed**。

- **自分が入れた `route()` 化が絶対URLをDBに焼き付けていた（マージ後監査）**: 前回「URLが文字列直書きでルート変更を検査で拾えない」ため `route()` に変えたが、**`route()` は既定で絶対URL**（`http://ホスト/...`）を返す。この値は `map_json` として**DBに保存される**ため、`APP_URL` と実際のホストがずれると**別ホストへ飛ぶ**。本製品は独自ドメインで複数ホストから使われる（**過去に同型の事故あり** — メディアAPIが `Storage::url()` の固定ホストを返し、実アクセスホストと違うと画像が表示されなかった件・2026-05-09）。第3引数 `false` を付けて**相対パス**に戻した（ルート名を使う利点はそのまま）。退行防止に「`map_json` の導線URLは `/` で始まり `://` を含まないこと」を検査（**絶対URLに戻すと落ちること**を確認）。あわせて `mail_steps_created` を足したときの docblock の記載漏れも補った。**同じ形の横断調査も実施** — DBの既定値が画面の選択肢に無い列を全走査し、`funnels.status`（既定 `draft` / 画面は `private`・`public`）が候補に挙がったが、`Funnel::create` は2箇所とも `private` を明示しており**発生しない**ことを確認（更新時の検証が `nullable|string|max:32` と緩い点は、通常操作では起こらず公開判定も閉じる方向に倒れるため**対象外**として記録）。配信を止める判定はすべて `status='active'` のみで、下書きが送られる経路が無いことも確認。検証: 全体 **517 passed**。

- **チャットで作ったメール配信が「稼働中」に見えて一通も届かない状態を解消（AIファネル×シナリオ配信の連動精査）**: 「AIでファネルを作ると、それに合った配信シナリオができるのか」という問いを起点に、依存関係・API・DB・マイグレーション・ドキュメントを突き合わせた。**連動自体はしていた** — `FunnelBlueprintApplier` がシナリオ（メール／LINE別）を作り、`account_id` 等が正しく入るので**シナリオ配信の一覧に必ず出る**（ビルダーもアーカイブ済みアカウントは選ばないため、一覧の条件と食い違って不可視になる隙間は無いことを確認）。メールは**ステップ本文まで自動生成**（`status='draft'`）、LINEステップは**意図的に生成しない**（B4・画面／メッセージ／ドキュメントすべて一致）。**問題は表示だった** — `step_messages.status` の DB 既定は `draft` なのに、ステップ編集画面の選択肢は「稼働中／停止」の2つしか無く、`draft` はどちらにも一致しないため **`selected` が1つも付かず、ブラウザが先頭の『稼働中』を表示する**（実測で確認）。一覧は灰色（止まって見える）で**2画面が矛盾**。ドキュメント B-3 の「各ステップを開いて有効化する」手順は、開くと既に稼働中に見えるため**成立していなかった**（＝チャットで作った配信が永久に届かない）。**下書きと停止は別の概念**なので選択肢を3つ（下書き／稼働中／停止）にし、バリデーションに `draft` を追加、一覧に「下書き（未配信）」バッジを追加。あわせて**完了メッセージの抜け**を補った（予約枠は「日程を設定して公開すると…」、決済プランは「無効状態で作成…有効化すると購入できます」と案内しているのに、**メールステップだけ案内が無かった**）。**私の見立ての訂正**: 「ビルダーの結果画面からシナリオへの導線が無い」と一度報告したが**誤り**。`email_steps_url` / `line_manage_url` が既にあり、作成したシナリオへ直接飛ぶ（シナリオが作られなかった場合だけ非表示になる、正しい挙動）。ただしURLが文字列直書きでルート変更を検査で拾えないため `route()` に変えた。コメントの陳腐化も1件是正（「LINE配信文面は **step_messages** に自動生成しない」— LINEステップは 2026-07-16 以降 `scenario_line_steps` の別テーブル・別画面）。検証: `StepMessageDraftStatusTest` 4件・ビルダーの端から端まで1件（**シナリオ配信一覧に出ること**まで確認）、**4つの修正をそれぞれ戻すと該当テストが落ちること**を確認、全体 **516 passed**。

### 2026-08-01

- **「保存したのに反映されない」の検査に穴が2つあり、実際に取りこぼしていた（マージ後監査）**: 形4 の検査は静的走査だけで、見られるのは `Model::create([...])` 等の**リテラルのキー**のみ。`$model->update([...])`（`app/` に **165 箇所**）や動的に組んだ配列は見えていなかった。Laravel には `Model::preventSilentlyDiscardingAttributes()`（捨てずに例外を投げる）があるのに**どこでも有効にしていなかった**ため、テスト実行中だけ有効にした（`Tests\TestCase::setUp`）。**本番では絶対に有効にしない** — いままで黙って捨てていた場所が一斉に 500 になるため。有効にして全体を流したところ **19 件が発火**し、次が分かった。**①製品コードの取りこぼし1件** — `EventScheduleSlotGenerator` と `EventScheduleController` が `EventSchedule`（`$timestamps = false`）に `created_at` を渡していた。`event_schedules` に **`created_at` 列は無い**ので、`$fillable` が捨ててくれていたおかげで SQL エラーになっていなかった（＝良かれと思って `$fillable` に足すと**その場で壊れる**罠）。渡すのをやめた。**②検査自身のバグ** — 日時列の許可条件が `usesTimestamps() || CREATED_AT !== null` で、**`CREATED_AT` は既定で常に非 null** なので条件が常に真。**`created_at` を一度も検査していなかった**。`usesTimestamps()` だけで見るよう修正（別名を使う `Subscriber` のため名前は定数から取る）。**③テスト13ファイルが「設定したつもり」で通っていた** — `step_messages` に**無い `body`** 列、`scenarios` に**無い `user_id`**、`Subscriber` の `registered_at`（`CREATED_AT` の別名なので `now()` に置き換わる）などを渡していた。`forceFill` / `forceCreate` / 正しい列名に直した。検証: **変異テストで、修正前の検査なら②を見逃し、修正後なら検知することを確認**。全体 **509 passed**。

- **前回入れたゲートと移行を監査し、黙って何もしない経路を3件塞いだ（マージ後監査）**: **①ゲートが本当に落ちるかを実測した** — 壊れたマイグレーションを1本仕込むと `exit 1`（素通りしない）ことを確認。あわせて**素通り防止**を追加した。`database/migrations` が配置されないまま「0件流して成功」になると、**緑なのに何も見ていない**状態になる（ゲートとしては最悪の壊れ方）。流れた件数がファイル数に足りなければ落とす。**完全一致にはしない** — 将来パッケージが自前のマイグレーションを持ち込んだだけで、本物の異常が無いのにデプロイが止まるため。**②`2026_08_01_100000` が黙って何もしない経路があった** — 型の判定に `DATA_TYPE` を使っていたが、これは `bigint` としか返さないため**符号ありの `bigint` を「揃っている」と誤判定**し、その後の外部キーが（符号違いで）張れずに握りつぶされる ＝ **何もせず DONE と出る**（実測で確認）。`COLUMN_TYPE`（`bigint unsigned`）で見るように直し、握りつぶす箇所には `Log::warning` を残した（記録が無いと、揃ったのか諦めたのかが後から分からない）。本番の形（`int unsigned`）・符号あり `bigint`・二度実行の3経路で実測。**③エディタに到達不能な関数が1つあった** — `forEachPageElement` が**同じ名前で2つ定義**されており、JS は後ろが勝つため**前の定義は決して呼ばれない**（前の方を読んで直しても何も起きない）。公開プレビューが3実装に分裂していたのと同じ型。削除し、`EditorSingleDefinitionTest` で二重定義を見張るようにした（**戻すと落ちること**を確認・重複は他に無し）。**対象外と判断したもの**: 旧形式 `two-col` セクション（要素が `col1`/`col2` にある）を**エディタが知らない**件 — 公開ページは正しく描画され、新規作成の経路も無く、**保存しても `col1`/`col2` は消えない**ことを確認したため据え置き（理由は `docs/degradation-policy.md` に記録）。検証: 全体 **509 passed**、エディタの JS 構文チェック OK。

- **「空のDBから migrate が通る」検査を用意し、実際に止まっていた2件を修正（監査 形5）**: 前回「138件のうち5件が MySQL 専用構文なので SQLite では検証できない」と書いたが、**方向が逆だった** — SQLite に寄せると `MODIFY COLUMN` も外部キーの型不整合も見つからない。**本番と同じ MySQL を CI に立てれば、マイグレーションを1行も書き換えずに、しかもより忠実に確かめられる**。実際に MySQL 8.0.46 の空DBへ流したところ**2箇所で止まった**（＝新しいサーバを立てられない状態だった）。①**`2026_05_26_120000`**: 外部キー `mbg_bundle_fk` を落とさずに `bundle_course_id` を落とそうとして `Cannot drop column ... needed in a foreign key constraint`。名前を決め打ちにせず**実際に張られている外部キーを引いて**落とすようにした（本番は手作成テーブルが混在し外部キーが無いこともあるため）。②**`2026_05_27_170000`**: `course_bundle_id` が `int` なのに参照先 `course_bundles.id` は `bigint` で、外部キーが `errno 150 / 3780` で張れない。**本番では順番の違いで助かっていた** — このマイグレーションが走った時点で `course_bundles` がまだ無く（追加した順とファイル名の順が違う）、`hasTable` が false で外部キーを飛ばしていた。空のDBはファイル名順に流れるので `course_bundles` が先にでき、そこで初めて衝突する。作成側を bigint に直し、**既存のDBを同じ形に寄せる `2026_08_01_100000`** を追加した（孤立行は先に NULL にする・揃えられなければ握りつぶす。閲覧制限は受講権の側で見ているので開く方には倒れない）。**検査は `bin/check-fresh-migrate.sh`**（GitHub Actions の `fresh-migrate` ジョブ・`deploy` は `needs` で待つ）。**テーブルが1つでもあるDBでは何もせず終了**する安全装置つき（接続先を間違えて本番を指しても書き換えない）。**あわせて自分のテストの時限爆弾を1件修正** — `SystemHeartbeatTest` は `hasProblem()` が全部の印を見るため、`schedule:run` を試した環境では**残っていた印が古くなった翌日に突然落ちる**（実際に落ちた）。`setUp` で印を消してから始めるようにした（`DatabaseTransactions` で巻き戻るので実データは消えない）。検証: MySQL 8.0.46 と MariaDB 10.11 の空DBで完走、**2つの修正をそれぞれ戻すと該当箇所で止まること**、**本番の形（int・外部キー無し・孤立行あり）に戻してから寄せ直せること**・**二度実行しても増えないこと**を実測。`FreshMigrateGateTest` 5件（**`needs` を外すと落ちること**を確認）、全体 **508 passed**。

### 2026-07-30

- **スマホ幅の上書きをエディタにも効かせ、セレクタの取り違えを1件修正（監査 M4）**: 公開ページとエディタが別々に書いていたため、①**エディタは文字サイズしか反映せず、余白（`padding_*_sp`）と `br_type=sp-hidden` は公開ページでしか効かない** ②**エディタのサブヘッドのセレクタが `.el-headline` を指していた**（`subhead: '.el-headline'`）ため、**サブヘッドのスマホ文字サイズが編集画面で反映されない**、という食い違いがあった。②はメモに無く、コードを読んで見つけたもの。`App\Support\FunnelPageSpStyle` を新設して**種別・セレクタ・境界幅・値の整え方・宣言の組み立て**を正本にし、公開の partial はそれを呼ぶだけにした。エディタは `@json()` で種別・セレクタ・境界幅を受け取り、余白と `<br>` の非表示も出すようにした。**画像は余白だけで文字サイズを持たない**ため、保存データを整える対象は `fontTypes()`（4種）に分け、画像に意味のない `font_size_sp` が書き込まれないようにした。あわせて**前回の修正のやり残し**（`getSectionBackgroundLayerStyle` の既定値だけベタ書きのままだった）も塞ぎ、パリティ検査に「既定値を直接書かないこと」を追加。検証: `FunnelPageSpStyleTest` 8件・`EditorPublicParityTest` 7件、**セレクタを `.el-headline` に戻すと落ちること**・**余白の項目を減らすと落ちること**を確認、全体 **503 passed**。

- **稼働状況の表示に誤警報の穴が2つあったのを塞いだ（マージ後監査）**: 前回入れた見張りを、**実際に `schedule:run` を動かして**確認したところ、背景実行のキュー（`runInBackground`）でも印が残ることは確認できた（Laravel が `schedule:finish` を後ろに付ける仕組み）。一方で**2件の不備**を見つけた。①**時計がずれていると「動いているのに止まっている」と出る** — `diffInMinutes` は符号を持たないため、記録が未来の時刻だと「30分前」と読めてしまう（実測: 未来30分 → `29` が返る）。アプリと DB で時計がずれていると起きる。**未来なら 0 分前として扱う**ようにした。誤警報は「本当に止まったときに信じてもらえなくなる」ので、見張り機能では致命的。②**テストが空のテーブルを前提にしていた** — `SystemHeartbeat::create()` を使っていたため、`key` の unique により**一度でも実際に動いた環境ではテストが落ちる**（本番や、`schedule:run` を試した開発環境）。`updateOrCreate` に直した。あわせて `FUNNEL_SECTION_BG_SIZE_POS_DEFAULT` が `sizePositionCss('cover')`（一覧に無いキーを引いて既定値を得る）という遠回しな書き方だったのを、`DEFAULT_SIZE_POSITION` の定数にした。検証: `SystemHeartbeatTest` 9件、**未来の時刻の扱いを戻すと1件落ちること**を確認、全体 **493 passed**。

- **配信が止まったことに気づけるようにした（監査 M12）**: cron（`schedule:run`）やキューが止まると、ステップ配信・一斉送信・リマインダが**何も言わずに止まる**。画面は今までどおりに見えるので気づけない。**動いた側に印を残させる**方式にした（`system_heartbeats`・`2026_07_30_100000`）。cron 自体の生存も見るため、`$schedule->call()` で**毎分の印**を別に置く（機能フラグが全部 off でも cron の停止が分かる）。各コマンドは Kernel の `onSuccess` / `onFailure` から記録するので、コマンド側には手を入れていない（失敗しても「動いてはいる」ことが分かるよう note に理由を残す）。ダッシュボード上部に **最後に動いた時刻を日本時間で表示**し、正常時は1行に畳んで騒がしくしない・止まっているときだけ開いて赤く出す。**設定で無効にしている機能は「止まっている」と言わない**（無効なのに警告が出ると本当の異常が埋もれる）。記録の失敗は握りつぶす（見張りのために配信が落ちては本末転倒）。検証: `SystemHeartbeatTest` 8件（**30分動いていなければ検知する**・無効な機能は警告しない・日本時間で出る・ダッシュボードに出る）、**古さの判定を外すと検知のテストが落ちること**を確認。

- **エディタと公開ページの見た目の食い違いを解消（監査 M2・M3）**: 同じ既定値を**エディタ（JS）と公開（PHP）が別々に持っていた**ため、プロパティを持たない要素（取り込んだページに多い）で食い違っていた。実測した差 — **見出し**: エディタ `2rem/700/1.4/#222222/中央` vs 公開 `36px/600/1.3/#212529/左`（**中央寄せか左寄せか**が違う）。**サブヘッド**・**本文**・**ボレット**も同様。**背景 `w100`**: エディタ `center` vs 公開 `left top`（公開側は UTAGE 準拠の根拠コメントあり）。**PHP を唯一の正本**にして（`FunnelPageTypographyStyle::inlineDefaults()` / `FunnelSectionBackgroundStyle::sizePositionMap()`）、エディタは `@json()` で受け取る形にした。**公開ページ側の値は変えていない**ので、公開中のページの見た目は変わらない（エディタ側が公開に合わせた）。`FunnelSectionBackgroundStyle` 内に**同じ match が2箇所**あったのも1つにまとめた。検証: `EditorPublicParityTest` 5件 — ①エディタが PHP から受け取っていること ②既定値の一覧をエディタで組み直していないこと ③公開ページが正本の値で描画されること ④**正本の値そのものを固定**（両者が揃っていても値を変えれば公開ページの見た目が変わるため）⑤`w100` が `left top` であること。**正本の既定値を変えると④が落ちること**を確認。全体 **491 passed**。

### 2026-07-29

- **判断基準を明文化し、その基準で全体を棚卸しして「保存したのに反映されない」1件を修正**: 「何を直しているのか分からない」という指摘を受け、**修正の線引きを [`docs/degradation-policy.md`](docs/degradation-policy.md) に明文化**した。基準は「**操作が予定どおり動かないものだけ直す**」で、この repo で実際に起きた事故を6つの形に整理した（①画面が開かない ②出荷したが動かない ③出荷したが到達できない ④保存したのに反映されない ⑤本番で migrate が止まる ⑥二重に走る）。既存の自動検査は①③のみで、**④には無かった**（`$fillable` 漏れは4回起きている）ため `App\Support\MassAssignmentScanner` と `MassAssignmentTest` を新設。`app/` 以下の `Model::create([...])` 等を走査し、リテラルのキーが `$fillable` にあるかを確かめる。**全体を走査したところ検出1件（誤検知ゼロ）** — **LINE一斉送信の `custom_sender_id` が `$fillable` に無く、画面でカスタム送信者を選んでも保存されていなかった**（実測: 42 を指定 → DB は NULL。ステップ配信側には元からあったので一斉送信だけ効かない状態。送信時は `ScenarioLineBroadcastSendService:235` が読むため、常に未指定として送られていた）。**形②も棚卸し** — `config/putage.php` の env キー 37 件を `.env.example` と突合し、無い5件はすべて既定値が正しいか `MONAKA_*` 側が文書化済みで**該当なし**。**形⑤は検査を作れない状態**（138件のうち5件が `MODIFY COLUMN` 等の MySQL 専用構文で SQLite では止まる。本番は MySQL なので動作に問題は無いが、空DBからの自動検証ができない）を理由つきで記録。**残る指摘も基準で再分類**し、対象は M2/M3/M4（エディタと公開の見た目差）と M12（cron/queue が止まると無言で配信停止）、対象外は M5（FK型・現状動く）・M10（巨大ファイル分割）とした。検証: `MassAssignmentTest` 2件（プローブで検知能力を確認）・`ScenarioLineBroadcastSenderTest` 1件、**`$fillable` から外すと両方落ちること**を確認、全体 **479 passed**。

- **自分が入れた無害化のバグを修正し、さらに2件見つけた（マージ後監査）**: 前回入れた `on*` 属性の除去が**タグの外にも効いてしまっていた** — `\son[a-z]+\s*=` が本文の ` one=1` / ` only=true` に一致し、**本文の文字が消える**。`<div title="see one=two">` は `<div title="see>` になって**タグが壊れる**（実測で発見）。処理を**タグの中だけ**に限定し、さらに**属性名の位置から名前=値の組で読む**ようにした（値の中に `=` や `>` があっても壊さない）。**対象キーも絞った** — `text` はテンプレート側で必ず `e()` を通すので、無害化すると**意味なく本文を失うだけ**（`<script>` という文字列を説明として書いたページで本文が消える）。生のまま出力されるキーを1つずつ確認し `html_content` / `label_html` / `content` の3つに限定。**さらに2件** — ①**`sub_label` で実体参照からタグが復活していた**: `html_entity_decode(strip_tags(...))` の順だと、元ページに `&lt;script&gt;` と書かれていた場合に strip_tags をすり抜けて decode で復活する（実測）。`sub_label` は生のまま `<span>` に出るので動いてしまう。**decode → strip_tags** の順に直した（ボタンラベルも同様）。②**アイコン名・サブ文字が属性/本文にエスケープなしで入っていた**（`class="bi bi-{値}"` / `<span>{値}</span>`）ため `e()` を通した。②は出力時の修正なので**既に取り込み済みのページにも即効く**（①は再取り込みが必要）。検証: `ImportedHtmlSanitizerTest` 13件（**本文の文字を消さない**・**属性の値を壊さない**を追加）、`UtagePageImportServiceTest` に `sub_label` の退行テスト（**順序を戻すと落ちる**ことを確認）、全体 **476 passed**。

- **M 系を棚卸しし、実害のある2件（M6・M7）を修正**: 監査メモは 2026-06-29 のもので古い項目が混ざっているため、**実コードで1件ずつ確認**した。**対応しないと判断したもの** — ①**M8**（`$fillable` に管理系カラム）: `User` / `Operator` への mass assignment 箇所を全走査したところ**すべて明示的に配列を組んでおり**、`$request->all()` を渡している箇所は無い。絞ると管理画面の権限設定が壊れるため据え置き。②**M11**（`LOWER(email) NOT LIKE` のフルスキャン）: 読者 20,000 件で**実測 5.8ms**（`scenario_id` の索引で走査範囲は絞られている）。フラグ列を足すと**列がずれて読者数が狂う**方が害が大きいので据え置き。③**M10**（巨大ファイル分割）: 最も退行しやすく実害が無いため着手しない。**M6（決済 webhook）は監査メモより重い問題だった** — メモは「署名/HMAC 検証なし」だが、実際は **`money`（入金額）が注文金額と違っても `Log::warning` を出すだけで `markPaid` まで進んでいた**。購入フォームは注文金額をそのまま `money` として送る（`TelecomCreditCheckoutService`）ので、**足りない通知は正しくない**のに会員付与・シナリオ登録・パートナー成果が走る状態だった。`money < 注文金額` は**入金確定しない**（注文は pending のまま残す）、`money >` は確定を進めて記録、`money` 無しは従来どおり通す（弾くと正当な入金まで止まる）。送信元IPが通知に無い場合の記録も追加（弾かずに記録のみ）。**署名/HMAC 自体はテレコム側の提供仕様に依存するため未対応**。**M7（取り込み HTML/CSS の無害化）** — `custom_css` は `<style>` の中に生で出力されるため、**取り込んだ CSS に `</style>` があるとそこから抜け出して `<script>` を書ける**（公開ページで動く）。`App\Support\ImportedHtmlSanitizer` を新設し、①取り込み時に `page_content` の HTML と `custom_css` を掃除 ②**公開・プレビュー時にも `custom_css` を通す**（取り込み時だけだと**既存ページが汚れたまま**残るため）。**見た目は変えない**ことを最優先にし、取り除くのは `on*` 属性・`javascript:` の URL・`</style>` など「動作」に関わるものだけ。**DOM を組み直していない**のは、組み直すとタグの書き方が変わって取り込んだページがずれるため（`docs/Convert_utage_to_monaka.md` の方針）。**極端に細工された書き方をすり抜ける可能性は残る**ことを明記。`html_content` は取り込み時のみ掃除する（エディタで書いた本文は利用者自身のものなので公開時に触らない）。検証: `ImportedHtmlSanitizerTest` 10件（**レイアウト用のマークアップが1文字も変わらないこと**を含む）・`TelecomCreditWebhookTest` 5件、**取り込み系 27件が全て変化なし**、**3つの修正をそれぞれ外すと該当テストが落ちること**を確認、全体 **472 passed**。

- **会員サイトの N+1 を解消（監査 H8・最後の HIGH）**: 会員トップ（コース一覧）は、コースごとに「受講権があるか」「どの付与か」「進捗は何％か」を問い合わせており、**コースの数だけクエリが増えて**いた。**まず実測**したところ **1コースあたり6本**（20コースで **121本**）で、内訳は①受講権の確認（`shouldListForMember` と `catalogRow` で**同じものを2回**）②付与の取得 ③レッスン ④グループ ⑤完了済みレッスン。レッスン画面の**左ナビも同型**で、完了済みかをレッスンごとに引いており **30レッスンで35本**だった。`App\Services\MembershipSite\MemberCatalogPreload` を新設し、①付与とそこに含まれるコース ②表示対象レッスン ③完了済みレッスン を入口で1回だけまとめて引くようにした（**121本 → 6本**・**35本 → 6本**。どちらも件数によらず一定）。左ナビは既存の `completedLessonIdMap` をループ前に1回呼ぶだけで済んだ。**1コースだけ見る画面は従来どおりの経路**を使う — `catalogRow()` / `shouldListForMember()` のまとめ取得は**任意の引数**にしたので、渡さなければ動きは今までと同じ（`MemberPortalController` の単体取得4箇所・プレビュー経路は無変更）。進捗の計算式が2箇所に散らばらないよう `BundleCourseAccessService::progressFrom()` に切り出して両方から使う。検証: `MemberCatalogPreloadTest` 3件 — **まとめ取得の結果が1コースずつ引いた場合と一致すること**（受講権・進捗・付与ID・ボタン文言・状態表示を突合）を含む。**まとめ取得を無効化するとクエリ本数のテストが、進捗の集計を壊すと一致のテストが落ちること**を確認。全体 **458 passed**。

- **「上限を超えた時点で読むのをやめる」を本当にした（マージ後監査・2度目の訂正）**: 前回 `withOptions(['stream' => true])` を付けて「上限を超えた時点で読むのをやめる」と書いたが、**Guzzle の `stream` オプションは curl ハンドラでは何もしない**（`CurlFactory` に `'stream'` の分岐が無い。ext-curl がある環境では常に curl ハンドラが選ばれる）。**実測で確認** — 20MB のファイルを取りに行くと、`stream` を付けても**最後まで受信して `php://temp` に落ちる**。つまり前回入れたものは「受信は止まらないが、上限を超える分を PHP の文字列として持たない」だけで、**書いた説明と実装が食い違っていた**（メモリのピークは 18MB → 2MB に下がるので効果自体はあった）。**受信そのものを止める手段があることも実測で確認**したので入れ直した — curl の進捗コールバック（Guzzle の `progress` オプション）から例外を投げると転送が中断され、**20MB のファイルが 1.1MB で停止**する。`BoundedResponseBody::limitOptions()` として提供し、打ち切りは `ResponseTooLargeException`（通信エラーと区別するため専用の型）。**リダイレクトを挟んでも打ち切りが効くこと**、**正当なサイズの画像（300KB）は素通りすること**も実測で確認（打ち切りは進捗コールバックの呼び出し間隔ぶん行き過ぎるため、上限 1MB に対し実測 1.1〜1.8MB で停止する）。**`read()` は残す** — curl が使えない環境では進捗コールバックが効かないため保険として2段構えにする。逆に `read()` だけに頼れないのは上記のとおり受信が止まらないから。効いていない `stream` オプションは削除した（残すと「ストリームしている」と誤読される）。対象はリッチメニュー画像（1MB）と取り込みの画像ミラー（10MB）。ページ本体の取得（5MB）は従来どおり。検証: `BoundedResponseBodyTest` 5件、全体 **455 passed**。

- **応答の読み込みに本当の上限を付けた（マージ後監査・前回の記述の訂正）**: 前回「1MB を超えるものは読み込まない（ローカル・外部の両方）」と書いたが、**外部側は正確ではなかった** — `Http::get()` は既定で**応答をすべてメモリに読み込んでから**返すため、`strlen($body)` で測る形は「読んでから捨てる」でしかなく、巨大なファイルを返す URL を指定されるとその分メモリを使う（PHP の memory_limit に当たれば 500）。**同じ形が取り込みの画像ミラーにも元からあった**（`MAX_BYTES` の判定が `$resp->body()` の後）。`App\Support\BoundedResponseBody` を新設し、`withOptions(['stream' => true])` と組み合わせて**上限を超えた時点で読むのをやめる**ようにした（リッチメニュー 1MB・画像ミラー 10MB の両方）。ページ本体の取得（5MB）は既存のままとし、揃えるかは別途判断。**その他の監査結果は問題なし** — ①取り込みの入口3つ（作成・取り込み確認・再取り込み）はすべて `fetchHtml` を通り検査済み ②`Storage::disk('public')->delete()` に利用者入力由来のパスを渡している箇所（ロゴ・サムネイル・印影など13件）を確認したが、**保存する URL はすべてアプリ側が組み立てたもの**で自由入力ではないため、同じ上位ディレクトリ問題は起きない ③`storagePathFromUrl` の利用者も想定どおり2箇所のみ。検証: `BoundedResponseBodyTest` 4件・`OutboundUrlGuardCoverageTest` 7件、**上限判定を外すと2件落ちること**を確認、全体 **454 passed**。

- **自分が入れたディスク直読みで 500 になる形を塞いだ（マージ後監査）**: 前回、リッチメニュー画像を「自サーバのものはディスクから直接読む」ようにしたが、**`image_url` は自由入力**なので `/storage/../../.env` のような形も入ってくる。`Storage::disk('public')->exists('../../.env')` は **Flysystem が `PathTraversalDetected` を投げる**（実測）ので、囲っていない呼び出しはそのまま **HTTP 500** になっていた。**ファイルは読めない**（Flysystem が拒否するので情報漏れではない）が、操作した本人の画面が落ちる。**上位ディレクトリへ移動する形を先に弾き**、ディスク操作は `try/catch` でまとめて捕まえて「画像が見つかりませんでした」に倒す。あわせて **1MB（LINE のリッチメニュー画像の上限）を超えるものは読み込まない**ようにした（ローカル・外部の両方。巨大ファイルをメモリに載せないため）。`storagePathFromUrl` の他の利用者は `PdfEmbedViewerController` だけで、こちらは**ブラウザ向けの URL を組むだけでディスクを触らない**ため同じ問題は無いことを確認。**解決結果のキャッシュに 60 秒の寿命**を付けた（取り込みは1リクエストで終わるので効果は変わらず、キュー worker のように動き続けるプロセスが名前解決を何時間も持ち続けない）。検証: `OutboundUrlGuardCoverageTest` 6件、**上位ディレクトリの拒否と try/catch を外すと1件落ちること**を確認、全体 **449 passed**。

- **SSRF 対策の取りこぼしを塞いだ（前回のマージ後監査）**: 前回「両経路（ページ取得・画像ミラー）に入れた」と書いたが、**利用者が貼り付けた URL をサーバから取りに行く箇所が他に4つ**残っていた。①**リッチメニュー画像**（`LineRichMenuApiService`）— `image_url` は「画像を選ぶか、URLを貼り付け」の自由入力（`string|max:2000`）で、検査なしで GET していた。②③④**Discord のウェブフック**（シナリオのLINE通知・イベント担当者通知・**LINE通知のテスト送信**）。特に**テスト送信は失敗時に応答本文をそのまま画面に出していた**（`'Discord Webhookエラー: HTTP '.$response->status().' '.$response->body()`）ため、ウェブフックURLに `http://169.254.169.254/...` を入れると**中身が読めてしまう**（前2つは HTTP ステータスのみの目隠し状態）。すべて `OutboundUrlGuard` を通し、テスト送信の応答本文表示は**ステータスのみ**に変更。Chatwork・Slack は接続先が固定なので対象外であることも確認。**あわせて既存の不具合を1件修正** — リッチメニューで**メディアから選んだ画像は適用が必ず失敗していた**。メディアAPIは `/storage/...` の**相対パス**を返す（`MediaFile::publicHrefForDiskPath`）のに、それを `Http::get()` に渡していたため `storage` というホスト名を引きに行き `cURL error 6: Could not resolve host: storage` になる（実測で確認）。自サーバの画像は `FunnelPublicMediaUrl::storagePathFromUrl` で判定して**ディスクから直接読む**ようにした。**性能**: ホスト名の解決結果をプロセス内で使い回すようにした（1ページに同じCDNの画像が何十枚もあると、画像の数だけDNSを引いていた）。解決方法を差し替えたときはキャッシュを捨てる。**数値表記の回避**（`http://2852039166/` = 169.254.169.254 の10進表記・8進・短縮 `127.1`）も実測で塞がれていることを確認。検証: `OutboundUrlGuardCoverageTest` 5件、**3つの検査をそれぞれ外すと該当テストが落ちること**を確認、全体 **448 passed**。

- **画像取り込みの SSRF を塞いだ（監査 H4）**: 取り込みは**利用者が入力した URL** と**取り込んだ HTML の中に書かれている画像 URL** をサーバから GET する。画像側（`FunnelPageImportImageMirrorService::downloadAndStore`）は **URL を一切検査せず** GET しており、`http://169.254.169.254/...`（クラウドの認証情報）や `http://127.0.0.1:6379/`（サーバの内側だけで動くサービス）を代わりに取りに行かせ、その中身を「画像」としてメディアに保存させることができた。入口の `FunnelPageImportUrlValidator` も **IP 直書きしか見ておらず**、`internal.example.com` が `10.0.0.5` を指す形は素通りだった。**`App\Support\OutboundUrlGuard` を新設し、両方の経路で同じ検査を通す** — ①`http`/`https` 以外は拒否 ②**ホスト名を実際に IP へ解決してから**判定（プライベート・予約レンジに加え PHP のフラグに無い **100.64.0.0/10** も拒否。**複数 IP を返すホストは1つでも内部なら拒否**。解決できないホストも拒否）③**リダイレクトは最大3回・1ホップごとに同じ検査**をやり直す（公開ホストから内部へ転送する形を塞ぐ）。正当な画像を巻き添えにしないため、**弾いた画像 URL は元の外部 URL のまま残し取り込みは続行**（`funnel.import.image_mirror.blocked` をログ）。ページ本体が転送先で弾かれた場合は「接続エラー」ではなく**理由をそのまま画面に出す**。テストは実 DNS に依存すると名前解決の可否で揺れるため、`OutboundUrlGuard::resolveHostUsing()` で解決を差し替えられるようにし、`Tests\TestCase::tearDown` で必ず戻す。**既知の限界**（DNS rebinding は塞げない）は `docs/funnel-spec.md` §5.1.1 に明記。検証: `OutboundUrlGuardTest` 9件・`FunnelPageImportImageMirrorServiceTest` 4件・`FunnelPageImportUrlValidatorTest` 5件、**解決後 IP の判定を外すと4件・画像側の検査を外すと2件落ちること**を確認。

- **メール配信記録の重複を DB で塞いだ（監査 H7-③・実データの確認を不要にした）**: `scenario_mail_deliveries` は `updateOrCreate` で「あれば更新・なければ作成」しているのに**索引が無く**、cron が二重に走ると両方が「無い」と判定して**同じ配信の行が2本**できていた（予約中・送信済の画面に同じものが並ぶ）。書き込み側のキーは4通りあり、MySQL / SQLite の unique は **NULL を含む行を重複とみなさない**性質を使って形ごとに索引を分けた（`2026_07_29_100000`）— ステップ `smd_step_unique`、一斉送信 `smd_broadcast_unique`、シナリオリマインダ `smd_reminder_unique`。**旧固定リマインダだけは unique にできない**: `scenario_reminder_id` を持たないため `(source_type, event_booking_id)` で張るしかないが、それだと**同じ予約に対する複数のシナリオリマインダ**（前日・1時間前。`source_type` はどれも `event_reminder`）を誤って弾くので、検索用の `smd_source_booking_idx` のみとした（互換経路で、予約側の `reminder_*_sent_at` でも二重送信は抑止済み）。**本番の実データ確認は不要** — マイグレーションが索引を張る前に既存の重複を自分で整理する（**送信済・失敗の記録を優先して残し**、残りは新しい1件だけ。削除件数はログ）。**unique を張ると `updateOrCreate` が例外になり得る**ため、`ScenarioMailDelivery::upsertRecord` で整合性制約違反（SQLSTATE 23000）だけ捕まえて更新に切り替える（それ以外は投げ直す）。これが無いと二重起動のたびに配信処理そのものが落ちる。検証: `ScenarioMailDeliveryUniquenessTest` 6件（**同じ予約への複数リマインダ・旧固定リマインダが弾かれないこと**を含む）、**復帰処理を外すと1件落ちること**を確認、新規作成／重複ありの既存テーブル／rollback→再適用の各経路で migrate を実測。全体 **443 passed**。

### 2026-07-28

- **LINE Webhook の再送で自動応答が二重送信される問題を解消（監査 H7-②）**: LINE は同じ出来事の通知を**再送する**（応答が遅い・タイムアウト時など）。これまでは受け取った分をそのまま INSERT しており、再送のたびに `line_webhook_events` が重複し、**自動応答が2回送られ**（読者に同じ返信が2通届く）、友だち追加の紐づけやパートナー成果も二重に走る状態だった。**①`line_webhook_events.webhook_event_id`（`2026_07_28_140000`）＋ unique** — LINE が各イベントに付ける `webhookEventId` を保存する。既存行は持たないため **nullable**（NULL は unique 対象外）。**②記録を先に確保してから副作用を実行する順序に変更**（`LineWebhookController::claimEvent`）— **逆だと重複を弾いても自動応答だけ2回走る**。ほぼ同時に届いた場合は unique 違反を捕まえて弾く。**③`webhookEventId` を持たないイベント**（古い形式・検証用の送信）は重複判定ができないため**従来どおり記録して処理**（弾いて取りこぼす方が害が大きい）。`LineWebhookEvent::$fillable` への追加も忘れず実施（**外すとテストが落ちること**を確認）。検証: `LineWebhookIdempotencyTest` 5件（再送は1件だけ記録・別イベントは両方記録・ID無しは従来どおり・**再送で自動応答を送らない**・不正署名は400）、**副作用の順序を戻すと1件落ちること**を確認、全体 **425 passed**。

- **送信用アカウントの秘密情報を暗号化保存にした（監査 H5 の解消・既存行を壊さない移行）**: `delivery_accounts` の `smtp_pass` / `line_channel_secret` / `line_login_channel_secret` / `line_access_token` が平文だった（監査 H5）。**壊さずに移行するため3点セットで実装**。**①カラムを先に TEXT 化**（`2026_07_28_130000`）— これらは `VARCHAR(191)` だが**4文字の値でも暗号文は 200 文字**（実測）。広げずに暗号化すると MySQL の非 strict モードで**黙って切り詰められ、復号不能＝秘密情報が失われる**。**②`App\Casts\EncryptedSecret`**（Laravel 標準の `encrypted` は使わない）— 標準 cast をそのまま付けると**既存の平文行が `DecryptException` で全部落ちる**ため、読み出しは「復号できなければ平文とみなす」、書き込みは常に暗号化とした。**③`monaka:encrypt-delivery-account-secrets`** で残った平文を一括暗号化（何度実行しても安全・`--dry-run` あり）。**空文字は暗号化しない**（`PutageLineAccountSuspendWatchCommand` の `where('line_channel_secret', '!=', '')` のような「未設定か」の判定を壊さないため）。秘密情報カラムを検索条件に使っている箇所を全走査して、壊れるものが無いことも確認。検証: `DeliveryAccountSecretEncryptionTest` 4件（暗号化保存・**平文行もそのまま読める**・コマンドの冪等性・空文字の据え置き）、全体 **420 passed**。運用手順は `docs/manual.md` と `docs/funnel-spec.md` §0.2 に記載（**`APP_KEY` を失うと復号できない**点も明記）。

- **決済の二重確定を塞ぎ、本番DB破壊の再発防止を入れた（既存監査バックログの消化）**: 全テストが緑になったので、`docs/audit-degradation-techdebt-20260629.md` に積まれていた未対応項目を棚卸しした。**①H7-①（購入の二重確定）を修正** — `OrderPaymentFulfillmentService::markPaid` は `if ($order->status === 'paid') return;` で判定してから更新する「読んで判定して書く」形で、**決済 Webhook が再送・同時着信すると2本とも判定を通り抜け、会員付与・シナリオ登録・パートナー成果・ABテストのCVがすべて二重に走る**状態だった。`status != 'paid'` を条件にした**1回の UPDATE で確保**し、更新できた1本だけが後続処理へ進むようにした（`OrderPaymentIdempotencyTest` 2件・**修正前の形に戻すと落ちること**を確認）。**②H6（本番DB破壊リスク）を緩和** — `phpunit.xml` の DB 分離は据え置きだが、実際の事故要因である `RefreshDatabase` の混入を `tests/Unit/NoRefreshDatabaseTest` が機械的に止める（コメント行は無視・**実際に使うテストを置いて検知を確認**）。**③監査メモの棚卸し** — H1（公開ページのセクション余白）は 2026-05-19 に、H2（未定義ルートで500）は今回のパスワード再発行実装と死蔵ビュー削除で、M9（ABテスト死蔵）は §8.1 の実装で、C5 は死蔵一掃でそれぞれ**すでに解消済み**だったため反映。**残る HIGH（H4 SSRF・H5 秘密情報の平文・H7-②③・H8 N+1）は、方針決めか実データ確認が要るため未着手として理由つきで明記**した。検証: 全体 **416 passed・失敗 0**。

- **赤かったテストを全滅させた（413 passed / 失敗 0）— UTAGE 7件は「退行」ではなく古いテストだった**: 残っていた UTAGE 取り込み系7件を調査。**git worktree で当該テストが追加されたコミット（`73a8b3b`・2026-07-03）まで戻して実行し、その時点で既に同じ5件が落ちることを確認**した ＝ 後から壊れたのではなく、**古いスナップショットのテストが丸ごと持ち込まれた**のが原因（テストファイルはそのコミットで `new file` として273行追加されている）。以降11本の UTAGE コミット（すべて「〜（汎用）」の見た目修正）で実装は意図的に進んでおり、**コード側にはすべて根拠のコメントがある**。ユーザー判断で**実装を正**とし、7件のテストを現行挙動に書き換えた（理由コメント付き）。確定した方針: ①画像幅は **101〜480px を px 固定**（UTAGE は `width:Npx; max-width:100%` で描画。%換算は列幅の取り方でずれる。禁止なのは `410%` のリテラル%バグ）②ページ幅は `pcwidth` 未指定なら **900**（`#main` 既定）③フォント推定は **`style` 属性を除外**（見出しだけ明朝→本文まで明朝になる誤判定を防ぐ）④`col_ratios` は `content_max_width` 基準に**逆スケーリング**⑤1列テキストのみの inner 行は親に統合し**行幅は `content_max_width`・行背景は `content_card_bg`** に載る⑥区切り線だけの行は独立セクション⑦ボレットは `items` 保持・`html_content` 空（マーカー描画を残すため）。**`docs/Convert_utage_to_monaka.md` §4 が「すべて%換算」と書いたままコードと矛盾していた**ため実装に合わせて是正し、**§7.1「そのほかの現行ポリシー」を新設**（同じ誤認の再発防止）。**周辺調査**: 取り込みが書き出すセクション/要素キー18種が描画側（`preview.blade.php`・`_preview_element`・`partials/_preview_section_body`・`_text_sp_font_styles`・エディタ）で**すべて参照されていること**を突合（描き落としゼロ）。`funnel_pages` の取り込み関連13列が**DB に実在しかつ `$fillable` にある**ことも確認（mass assignment の取りこぼしゼロ）。検証: 全体 **413 passed・失敗 0**。

- **ずっと赤かった11テストのうち4件を修正（原因はテスト側のURL生成器の扱い）**: 長く放置されていた失敗11件を調べ、**LINE Webhook 同期系の4件はテスト環境の不備**と判明したため修正。`LineMessagingWebhookService::syncWebhookEndpoint` は `route(..., true)` で Webhook URL を作り **HTTPS でなければ即エラー**にする。テストは `config(['app.url' => 'https://...'])` を差し替えていたが、**URL 生成器の root は起動時の `APP_URL`（`http://localhost`）のまま**なので効いておらず、常に「HTTPS である必要があります」で落ちていた（＝製品の不具合ではない）。`URL::forceRootUrl()` ＋ `URL::forceScheme('https')` を併用するよう修正し、`LineMessagingWebhookServiceTest` 4件・`DeliveryAccountLineSettingsCheckServiceTest` 4件がすべて通るようになった。全体 406 passed / **失敗は 11 → 7 に減少**。**残る7件（UTAGE取り込み系）は製品側の退行の疑いが濃い**（例: 画像幅が `70%` になるべきところ `410px` のまま＝`docs/Convert_utage_to_monaka.md` §4 のピクセル→%換算が効いていない、フォント推定が `mincho` を返さない、inner 行の幅が空）。UTAGE は同ドキュメントの検証手順（実ページ再取り込み＋目視比較）が必要なため未着手。

- **死蔵コード（デグレ・技術負債）を一掃し、再発を機械的に防ぐようにした**: 監査で挙げていた到達不能コードを、参照が無いことを1件ずつ確認したうえで削除。**①死蔵コントローラ**（`Auth\VerificationController`＝メール認証・ルート無し／`Auth\ConfirmPasswordController`／`HomeController`＝Laravel UI のスカフォールド）、**②死蔵ビュー9件**（`auth/verify`・`home`・`welcome`・`funnel/page-editor`・`funnel/page/_preview_sections`・`funnel/page/preview.blade.php.new`・`event/show`・`member/partials/_course_thumbnail`・`embed/scenario-register-tag`）、**③二重コピーと成果物**（`_deliveries/` 636KB・ルートの `utage-align-changes-20260327.tar.gz`・`manus_report.md`・空ファイル「メール管理」）。**最大の是正は公開プレビューの正本を1つに確定したこと** — `preview.blade.php`（実体）／`_preview_sections.blade.php`（死蔵）／`preview.blade.php.new`（残骸）の3実装に分裂しており、仕様書は死蔵側を正本と書いていた（＝直したつもりが反映されない状態）。仕様書の記述も実体に合わせて是正した。`_deliveries` は削除前に**ライブに無い9ファイルを個別に照合**し、参照ゼロを確認（`FunnelComment` / `funnel_comments` はコード・マイグレーションのどこからも参照無し）。**再発防止**: `tests/Unit/UnreferencedViewTest`（参照されないビューがあると落ちる・**プローブを置いて検知を確認**）、`bin/check-view-references.php` に `*.new` / `*.bak` / `*.orig` / `*.old` の取り残し検出を追加（デプロイ前ゲート）、`RouteReferenceTest` の**除外リストを廃止**（到達できないファイルは残さない方針に統一）、`.gitignore` に `*.tar.gz` / `/_deliveries` / `*.new` / `*.bak` / `*.orig`。`docs/funnel-spec.md` §12 に方針を明記し、`docs/audit-degradation-techdebt-20260629.md` の C1〜C4・C6・M1 を解消済みに更新。検証: 全体 402 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **ABテストの残り2件を是正（さらにマージ後監査）**: 前回の是正をもう一度監査し、2件見つけて修正。①**契約が特定できないときに絞り込みが外れていた** — `activeSessions` の条件が `$ownerUserId !== null && ...` だったため、所有者を解決できない経路（シナリオに送信用アカウントが無い等）では**契約またぎの加算が復活する**状態だった。**絞り込めないなら数えない**（early return ＋ 条件から `!== null` を除去）。②**`ab_test_sessions.created_at` が黙って捨てられていた** — `AbTestSession` は `$timestamps = false` なのに `created_at` が `$fillable` に無く、`create(['created_at' => now()])` が mass assignment で落ちて**常に NULL**（パートナーで踏んだのと同じ型）。`$fillable` と `$casts` に追加。検証: `AbTestAttributionTest` を4件に拡充し、**両方とも修正前の形に戻すと落ちること**を確認、全体 402 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **ABテストの成果の突合せを是正（マージ後監査で発見した2件）**: 自分がマージした ABテストを監査し、**成果（CV）の紐づけに2つの誤り**を見つけたため修正。①**契約をまたいで加算していた** — 公開ページのドメインは複数の契約が共有するのに、CV をクッキーの有無だけで判定していたため、**A社のLPを見た人がB社のフォームで登録するとA社のABテストにCVが付く**（テストで再現・修正後は0件）。登録はシナリオの所属契約、購入は注文の `user_id`、到達ページはそのページの所有者で `funnels.user_id` を突き合わせるようにした。②**購入の成果が無関係の訪問者を消費していた** — `orders` にバリアントIDしか焼き付けておらず、入金確定時に `where('variant_id', ...)->where('converted', false)->latest()` で**適当な1件**を成果にしていたため、同じパターンを見ていた別の訪問者が巻き添えで converted になっていた。`orders.ab_test_session_id`（`2026_07_28_120000`）を追加し、**購入した本人の訪問だけ**を成果にする（セッションが消えていれば数えない）。`resolveVariantIdForOrder` は `attachToOrder(Request, Order)` に置き換え。検証: `AbTestAttributionTest` 2件を新設し**両方とも変異を入れると落ちること**を確認、`AbTestTest` 13件、全体 400 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **ABテストを実際に使える機能にした（ルート欠落・プレースホルダ画面・計測なしの解消）**: 正本 [`docs/funnel-spec.md`](docs/funnel-spec.md) §8.1。監査で **`AbTestController`（8メソッド）・`ab_test_variants`・`abtest_edit.blade.php` が揃っているのにルートが1本も無く、ライブ画面は「準備中です」のプレースホルダ**（＝作ったが使えない）と分かったため実装。**①ルート9本**を登録し、**②結果画面を作り直し**（パターン・配分・表示・CV・CVR・「現在いちばん良い」・パターン追加/編集/削除・記録リセット）、**③公開ページ側の振り分けと計測**を新設した（`AbTestService`）。振り分けは `traffic_ratio` の**比率**で、結果は**クッキー `monaka_ab_{page_id}`（90日）＋ `ab_test_sessions`** に持ち**同じ訪問者には毎回同じパターン**を見せる。表示するのはバリアントの `page_content` で **`funnel_pages` は書き換えない**（停止すれば元に戻る）。**④CVポイントをページごとに選べるようにした**（`funnel_pages.ab_test_goal`・`2026_07_28_110000`）— 成果をどこで数えるかは運用者が決めるものなので、**登録フォーム完了 / 決済完了 / 指定ページ到達**の3つから選ぶ。**決済 Webhook にはクッキーが無い**ため、パートナー成果と同じく**購入フォーム送信時に `orders.ab_test_variant_id` へ焼き付け**、`markPaid` で計上する。表示もCVも **`(page_id, session_key)` の unique と `converted` により1訪問者1回**。**⑤認可の穴を是正** — 従来は `authorize('view',$funnel)` だけで、**ページがそのファネルのものか／パターンがそのページのものかを検証しておらず、URLのidを差し替えれば他契約のデータに触れられた**（404 で塞いだ）。`AbTestService` の入口はすべて例外を握りつぶし、**計測の失敗で公開ページ・登録・購入を止めない**。検証: `AbTestTest` 13件（**CVポイント判定を無効化すると1件、同一訪問者の固定を壊すと1件落ちること**を確認）、全体 398 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **パスワード再発行に対象ごとの再送クールダウンを追加（マージ後監査）**: 自分がマージした管理画面のパスワード再発行を監査し、**IP 単位の `throttle:5,1` しか無く、IP を変えれば既知の管理者アドレスへ何通でもメールを送れる**（メール爆撃）ことが分かったため是正。Laravel 既定のブローカーが持つ `throttle => 60` と同じ考え方で、**対象ごとに60秒の再送クールダウン**（`RESEND_COOLDOWN_SECONDS`）を設けた。**クールダウン中は送らないだけで、先に届いたURLは有効なまま**（作り直して無効化しない）。**同じ欠落がパートナーの再発行にもあった**ため両方を是正。あわせて、①今回の実装で完全に不要になった Laravel UI のスカフォールド `Auth\ForgotPasswordController` / `Auth\ResetPasswordController`（`$redirectTo = '/home'` は存在しないルート）を削除、②トークン行が溜まり続けないよう、発行時に同じ対象の30日より古い行を削除。テストは `AdminPasswordResetTest` 9件（クールダウン1件追加・`travel()` で経過後は送れることまで確認）、`PartnerPasswordResetTest` 11件（同）。検証: 全体 385 passed（既存の 11 失敗は本変更前から・内訳同一）。**未対応で残した死んだコード**: `Auth\VerificationController` ＋ `auth/verify.blade.php`（メール認証・ルート無し）、`Auth\ConfirmPasswordController`、ABテスト一式、`funnel/page-editor.blade.php`。

- **管理画面のパスワード再発行を実装（監査で見つかった欠落の解消）**: 監査で **管理画面（`web` ガード）に本人によるパスワード再発行が存在しない**（`ForgotPasswordController` / `ResetPasswordController` はあるがルート未登録・ログイン画面にリンクも無し）ことが分かったため実装。正本 [`docs/funnel-spec.md`](docs/funnel-spec.md) §0.1。**Laravel 既定のパスワードブローカーは使わない** — 管理ログインは①契約オーナー（`users.owner_user_id` が null）②オペレーター（`operator_identities`・複数契約で同じメール／パスワードを共有）③旧方式オペレーター（`users.owner_user_id` あり・有効な `operators.login_user_id` から参照）の**3系統**あり、「メール1件で1レコードだけ引く」ブローカーでは対象を取り違える。`admin_password_resets`（`2026_07_28_100000`）に `subject_type` / `subject_id` を持たせ、**対象1件ごとにトークンを発行**（同一メールに複数該当すればそれぞれに1通ずつ）。トークンは **sha256 保存・60分・1回限り**、新規発行で旧トークンを無効化。**存在しないメールでも同じ完了文面**（アカウント列挙防止）、停止中は送らず期限内でも停止されればURL失効、**メール送信に失敗したらトークンも無効化**（届かないURLだけが有効になる状態を作らない）。スロットルは依頼 `5,1`／表示 `30,1`／実行 `10,1`。重複していた Laravel UI のスカフォールド `auth/passwords/*` は削除し、`RouteReferenceTest` の除外リストも更新。`docs/manual.md` §0 と §11 チェックリストを追記。検証: `AdminPasswordResetTest` 8件（**トークン有効判定を壊すと3件落ちること**を確認）、全体 383 passed（既存の 11 失敗は本変更前から・内訳同一）。

### 2026-07-27

- **横断監査：ルート名参照の検査を追加し、`queue_worker_enabled` の設定キー欠落を解消**: 直近2件の事故（パートナー H3＝テーブル欠落／LINEステップ＝env 漏れ／ファネル詳細＝partial 欠落）がいずれも「出荷したが参照先が無い・動かない」型だったため、同型を機械的に洗った。**①スケジューラの機能トグル7種 × `.env.example`** … 対応漏れは無し。ただし `Kernel` が `config('putage.queue_worker_enabled', true)` を参照しているのに **`config/putage.php` にキー自体が無く、`.env` から止められない**状態だったため、既定 true のままキーと `PUTAGE_QUEUE_WORKER_ENABLED` を追加（挙動は不変であることを確認）。**②`route('名前')` の参照先 × 定義済みルート** … 到達可能な画面での欠落は 0。`App\Support\RouteReferenceScanner` と `tests/Feature/RouteReferenceTest`（ルート名を消すと落ちることを確認）を追加し、**到達不能な死んだファイル**は理由つきの除外リストで明示した（`funnel/page/abtest_edit.blade.php`＝`AbTestController` 8メソッドが未ルーティング・ライブの `funnel.page.abtest` は「準備中」プレースホルダ／`funnel/page-editor.blade.php`＝旧エディタ／`auth/passwords/*`・`auth/verify`＝**管理画面（`web` ガード）にパスワード再発行・メール認証のルートが無い**。自己再発行があるのはパートナーポータルのみ）。**③ビュー参照** … OK。検証: 全体 375 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **LINEステップ配信：本番で動かない設定漏れを解消し、仕様書・マニュアル・テストを整備（マージ後監査）**: `b80a2ce` で入った LINEステップ配信（`scenario_line_steps` / `scenario.line-step.*` / `putage:line-step-dispatch`）を監査。認可（`authorize('view',$account)`＋`assertScenario`＋`scenario_id` 突合せ）、`$fillable` の網羅、`(step, subscriber)` unique ＋ `Cache::lock` による二重送信防止、`FileStore` のロック対応（`CACHE_DRIVER=file` でも動く）は問題なし。**ただし `config('putage.line_step_dispatch_enabled')` が既定 false なのに `.env.example` に `PUTAGE_LINE_STEP_DISPATCH_ENABLED` が無く、有効化手順もどこにも書かれていなかった**（＝画面からステップを作れても一通も送信されない。パートナー H3 と同型の「出荷したが動かない」）。`.env.example` に追加し、`docs/funnel-spec.md` に **§13 LINEステップ配信**（有効化・3つのタイミング・ラベル条件・既存読者ポリシー・重複送信防止・**一度スキップしたステップは送り直さない**・既知の限界）と §11 チェックリストを新設、`docs/manual.md` に操作と有効化手順を追記。あわせて**実装済みなのに「できない」と書いたままだったドキュメント**を是正（`docs/funnel-builder-decisions-20260707.md` B-1「ユーザーが通常UIから手動追加することもできない」・`docs/funnel-builder-template-specs-20260706.md`「自動生成不可（現状メールのみ）」・同 plan B4。**ビルダーからの自動生成は引き続き行わない**方針は維持）。テストがゼロだったため `ScenarioLineStepDispatchTest` 10件（Http::fake・3モードの予定時刻・必須/除外ラベル・既存読者ポリシー3種・停止中ステップ/読者・時間軸順に1通ずつ・二重送信なし・設定無効時は何もしない）を追加し、**ラベル判定を無効化すると2件落ちること**まで確認。検証: 全体 373 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **ファネル詳細7画面が全滅していた不具合を修正（共通ナビの include 切れ＝HTTP 500）**: `/funnel/{id}/map` が真っ白／表示されないという報告の原因は、**`resources/views/funnel/partials/detail-nav.blade.php` がリポジトリから失われていた**こと。この partial は `funnel/detail/*.blade.php` の**7画面すべて**（マップ／データ合算／データ日別／登録者／登録経路／コメント／ファネル共通設定）が `@include` しているため、`View [funnel.partials.detail-nav] not found` で全画面が 500 になっていた（アーカイブ `_deliveries/latest-files/` 配下にのみ残存＝過去のルート欠落事故と同型）。partial を復元し、**7画面を実際に GET して 200 を確認する** `FunnelDetailScreensTest`（9件・バグ再導入で落ちることを確認）を追加。既存の `FunnelFlowMapTest` はサービス層のみで、画面を開かないため検知できていなかった。**再発防止**として `App\Support\BladeViewReferenceScanner` を新設し、`tests/Unit/BladeViewReferenceTest` と `bin/check-view-references.php`（vendor も DB も不要）から同じ静的検査を実行、**GitHub Actions のデプロイ前ゲート**にも追加した。あわせて **デプロイ後処理の失敗握りつぶしを修正** — `... && migrate --force && storage:link || true && config:clear ...` はシェルの結合規則上、`composer install` や `migrate` の失敗まで `|| true` が拾い、**壊れたままジョブが緑になる**（`sh -c 'false && echo A || true && echo B'` が exit 0 になることを実測）。`set -e` ＋セミコロン区切りに変更し、握りつぶすのは `storage:link` だけにした。`docs/funnel-spec.md` に **§3.1 ファネル詳細の7画面と共通ナビ**（マップの意味・所有権スコープ・「接続がありません」の読み方を含む）を新設、`docs/deploy-setup-conoha.md` の「main には `.github` が無く」「`.proposed`」という**実態と食い違う記述**を稼働中の内容に是正。検証: 全体 363 passed（既存の 11 失敗は本変更前から・内訳同一）。

- **パートナーポータル：ログアウトが管理画面のセッションまで壊す不具合を修正（マージ後監査で発見）**: `PartnerPortalAuthController::logout` が `session()->invalidate()` を呼んでいたため、**セッションがガード間で共有される**関係で、同じブラウザで開いている管理画面（`web` ガード）まで巻き添えでログアウトしていた（運営者が自分でポータルを確認しただけで管理画面から締め出される）。先行事例の会員サイト（`site_member`）は `invalidate()` を呼ばず `regenerateToken()` のみで、そちらが正しい作法だった。実測で確認（修正前はセッションのキーが空、修正後は `login_web_*` が残る）。あわせて、ログイン時の冗長な `session()->regenerate()` を削除（`SessionGuard::login()` が実施済み）、ログイン候補の取得に `limit(20)`、募集ページの公開GET（`public.partner.apply.show` / `.thanks`）に `throttle:120,1` を追加（`/pa/{token}`・`/p/{uid}` と統一）。回帰テストは**バグを再導入すると落ちること**まで確認済み。`docs/partner-spec.md` §6.1 に方針を明記。全体 352 passed（既存の 11 失敗は本変更前から）。

- **パートナー：削除時の整合性を是正（FK を fail-soft にした影響の後始末）**: 外部キーを try/catch の後付けにした結果、**DB のカスケード削除に依存できなくなった**ため、削除の後始末をアプリ側で明示的に行うようにした。①**パートナー削除**は支払済の報酬（`partner_payouts.paid` / `partner_conversions.paid`）があるとき**拒否**（入金履歴を消さない。「停止中」を案内）、無いときは成果・支払明細・紹介URLを消してから本体を削除。②**案件削除**は紹介URLを消すが**成果の記録は残す**（`campaign_id` を null）。③**紹介URL削除**も成果を残し `partner_link_id` だけ外す。あわせて `2026_07_27_110000` の `after('registration_route')` を、その列がある環境でだけ指定するよう防御的にした（本番 `orders` が手動作成の可能性があるため）。確認ダイアログの文言も新しい挙動に合わせ、`docs/partner-spec.md` §8 に削除時の扱いを明記。検証: `PartnerPayoutTest` 16件（削除の3件を新規追加）。全体 352 passed（既存の 11 失敗は本変更前から）。

- **パートナー：本番で `/partner` が 500（H3 の顕在化）→ 反映手順を明記し、マイグレーションを本番耐性に**: 本番 `putage` に `partner_campaigns` が無く `SQLSTATE[42S02]` で 500 になっていた（＝監査 H3 そのもの。**本ブランチが未反映**なだけで、必要なマイグレーションは既に用意済み）。あわせて、**`users` への外部キーをテーブル作成時のインライン `foreignId()->constrained()` から、後付けの `tryAddForeignKey`（try/catch）に変更**。本番は手動作成テーブルが混在し `users.id` の型を事前確認できないため、型が食い違うと `migrate --force` が途中で止まり本番が中途半端になる。FK を後付けにすれば張れなくても完走する（整合性はアプリ側の `user_id` スコープで担保）。対象は `partner_groups` / `partners` / `partner_campaigns` / `partner_links` / `partner_conversions` / `partner_settings` / `partner_payouts`。`docs/partner-spec.md` §2.2 に方針、**§2.3 に本番反映手順**（`git pull` → `migrate:status` → `migrate --force` → `view:cache`）を追記。検証: users のみの空DBからの新規作成、既存 `partner_campaigns`（行あり）への不足カラム補完の両経路で `migrate --force` 完走・行保持を確認。全体 349 passed（既存の 11 失敗は本変更前から）。

- **パートナー：支払通知メールと画面文言の実装状況ズレを是正**: 支払明細を「支払済」にすると、パートナー本人へ入金のお知らせ（対象期間・件数・金額・支払日）を送る（`PartnerPayoutPaidMail`）。送信可否は **`partner_settings.notify_partner_on_payout`**（`2026_07_27_150000`・既定は送る・設定画面のチェックで切替）。メール未登録なら送らず、**送信に失敗しても支払済の記録は取り消さない**（入金の事実とメールの成否を切り離す）。あわせて **監査で見つけた画面文言のズレを修正** — `_tracking_notice`（「振込・消込とパートナー本人の画面は未実装」）・パートナー編集の `page-subtitle` と「本人による再発行はできない」・報酬集計の `page-subtitle`（「支払処理は未実装」）・設定の「実際の振込・消込処理は未実装」が、実装済みの支払管理・マイページ・パスワード再発行と食い違っていた（仕様書 §0 が求める「実装状況が変わったら画面の案内も同時更新」の不履行）。検証: `migrate --force`、`view:cache`、`php -l`、`PartnerPayoutTest` 13件。全体 349 passed（既存の 11 失敗は本変更前から）。

- **パートナー：パスワード再発行と参加申請・承認フローを実装**: 正本 [`docs/partner-spec.md`](docs/partner-spec.md) §7。**パスワード再発行**は `partner_password_resets`（`2026_07_27_140000`）。**Laravel 既定のパスワードブローカーは使わない**（メールをキーに1人だけ引くため、契約をまたいで重複しうる `partners.email` では取り違える）。トークンは **sha256 でハッシュ保存・60分・1回限り**で、新規発行時に旧トークンを無効化。同一メールが複数契約にあるときは**それぞれに1通ずつ**送る。存在しないIDでも同じ応答を返す（アカウント列挙防止）。依頼 `throttle:5,1`。**参加申請**は `partner_campaigns.public_token` による募集ページ **`/partner-apply/{token}`**（案件を公開範囲=公開で保存すると自動採番）。案件の `approval` が `auto` なら `status=active`＋紹介URL即発行、`manual` なら `pending`（マイページ不可）。同一契約の重複メールは拒否。承認・却下はパートナー一覧から（承認時に紹介URL発行＋本人へメール）。`partners.applied_at` / `approved_at` / `application_campaign_id` を追加。メールは **`PartnerOutboundMailService`**（`partner_settings.mail_delivery_account_id` で送信用アカウントを選択、未選択はプラットフォーム既定）で送り、**失敗しても申請は成立させる**。`docs/manual.md` §4.1 更新。検証: `migrate --force` / `rollback`（unique 索引を先に落とす修正込み）、`view:cache`、`php -l`、`PartnerApplicationTest` 13件・`PartnerPasswordResetTest` 10件。全体 346 passed（既存の 11 失敗は本変更前から）。

- **パートナー本人向けポータル（`/partner-portal`）を実装**: 正本 [`docs/partner-spec.md`](docs/partner-spec.md) §6。**`partner` ガード**を新設（`config/auth.php`・`App\Models\Partner` を `Illuminate\Foundation\Auth\User` 継承に変更・`partners.remember_token` / `last_login_at` を追加＝`2026_07_27_130000`）。管理画面の `web`、会員サイトの `site_member` とは独立で、**管理画面にログイン中でもポータルには入れない**。**`partners.email` は契約をまたいで重複しうる**ため、ログインIDは **メールまたは紹介コード（`uid`）** を受け、パスワード一致が**ちょうど1件のときだけ**ログインさせる（複数一致は取り違え防止で拒否し紹介コードを促す）。画面はホーム（紹介URL・コピー・サマリ）／成果履歴／支払明細／登録情報（**振込先の自己登録**・パスワード変更）。本人が触れるのは氏名・メール・電話・振込先・パスワードのみで、**ステータス・グループ・`user_id`・紹介コードは変更させない**。`Authenticate::redirectTo()` に `partner-portal` 配下の分岐を追加（管理画面ログインへ飛ばさない）。ルート名は `partner.*` と紛れないよう **`public.partner.portal.*`**、パスも `/partner`・`/pa/{token}` と衝突しない `/partner-portal`。管理側はパートナー編集に**マイページURL＋ログイン不可の理由**、一覧に「マイページ不可」を表示。**パスワード再発行（メール）は未実装**で、ログイン画面・仕様書・マニュアルに明記。検証: `migrate --force`、`view:cache`、`php -l`、`route:list`、`PartnerPortalTest` 13件（他契約のデータが見えないこと・管理セッションで入れないこと含む）。全体 323 passed（既存の 11 失敗は本変更前から）。

- **パートナー：報酬の支払管理（支払明細）と監査是正**: `partner_payouts`＋`partner_conversions.payout_id`（`2026_07_27_120000`）。報酬集計の隣タブ **支払明細**（`partner.payouts.*`）で、**確定済みかつ未割当**の成果をパートナー単位でまとめて明細化（`lockForUpdate` で二重計上防止）。**最低支払額**未満は明細を作らず繰り越し（画面に対象名を表示）。支払済にすると含まれる成果も `paid` に揃え、振込待ちの明細は取り消して成果を `approved` に戻せる（**支払済は取り消し不可**）。**振込用CSV**（BOM付きUTF-8・口座情報つき）。**整合性の穴を是正**: 明細に計上済みの成果を成約状況から単独変更できると明細合計と食い違うため拒否し、操作列を「明細に計上済み」表示に置換。あわせて監査で見つけた3点を修正 — ①`totals()` の成果件数に無効が混ざり金額と基準がズレていた（`canceled_count` を分離し画面に内訳表示）、②`rewardSummary()` の成果件数から無効を除外（支払対象を見る画面のため）、③`confirmDueConversions()` が **設定行の無いユーザーの未確定を永久に確定しない**穴を塞いだ。`docs/partner-spec.md` §5 新設・§0/§2/§3/§7/§8 更新、`docs/manual.md` §4.1。検証: `migrate --force` / `rollback`、`view:cache`、`php -l`、`PartnerPayoutTest` 10件ほかパートナー関連 55件。全体 310 passed（既存の 11 失敗は本変更前から）。

- **パートナー：成果の自動記録（紹介 → 成約 → 報酬）を接続**: 正本 [`docs/partner-spec.md`](docs/partner-spec.md) §4 を新設。**決済 Webhook と LINE follow にはクッキーが無い**ため、ブラウザのある地点で紹介元を確定させて DB に橋渡しする設計にした。購入は購入フォーム送信時に **`orders.partner_link_id`**（`2026_07_27_110000`）へ焼き付け、`OrderPaymentFulfillmentService::markPaid` で成果化。メール登録は `ScenarioRegistrationService::createFromValidated` 直後、イベントは `EventBookingRecorder::record` 直後。LINE は **`partner_line_follow_intents`**（新設・既存の `scenario_line_route_visit_intents` には手を触れず判定件数を変えない）に控えを残し、follow Webhook で **未消費がちょうど1件のときだけ**消費。`partner_conversions` の `(source_type, source_id)` を **unique** にして Webhook 再送でも重複しない。報酬は率＝`floor(売上×率÷100)`／固定を**確定値で保存**（後から案件を変えても既存成果は動かない）。未確定→確定は `partner_settings.reward_confirm_days` 経過で **`monaka:partner-conversions-confirm`**（毎日3:10・`PUTAGE_PARTNER_CONVERSION_CONFIRM_ENABLED`）、成約状況の行内ボタンで手動変更も可。テレコムのキャンセル通知で該当成果を無効化。**各フックは `PartnerAttributionService` 側で例外を握りつぶす**（購入・登録・予約を絶対に止めない）。`docs/funnel-spec.md`（テレコム決済節）・`docs/manual.md` §4.1・`.env.example`・`config/putage.php`・`app/Console/Kernel.php` を更新。検証: `migrate --force` / `rollback`、`view:cache`、`php -l`、`PartnerConversionRecordingTest` 16件・`PartnerLineFollowAttributionTest` 4件・既存パートナーテスト25件（計45件）。全体 300 passed（既存の 11 失敗は本変更前から）。

- **パートナー（アフィリエイト）：画面と土台を実装**: 正本 [`docs/partner-spec.md`](docs/partner-spec.md) を新設。**`partners`/`partner_campaigns` の create マイグレーション欠落（監査 H3・新規DBで `/partner` がクラッシュ）を解消**し、`partner_groups`・`partner_links`・`partner_conversions`・`partner_settings` を追加（`2026_07_27_100000` / `100100` / `100200`）。手動作成済みテーブル向けに **不足カラムのみ追加** する分岐と、参照先 id 型差異で `migrate --force` を止めない **外部キーの個別試行** を実装。左メニューのプレースホルダ7項目を実画面化（案件一覧／成約状況／ランキング／報酬集計／パートナーグループ／パートナー一覧／URL管理／設定・各項目に `sidebar-nav-tooltip`）。案件は専用ページ CRUD（報酬は率／固定の二者択一・選ばれない側は保存しない）、パートナーは CRUD＋紹介コード自動採番＋振込先、グループ削除はパートナーを残して「グループなし」へ。**公開 `GET /pa/{token}`**（`public.partner.link`）でクリック計測とクッキー `monaka_partner_ref` 付与→案件LPへ転送（http/https のみ許可）。集計3画面は `partner_conversions` を集計元とし、**成果の自動記録は未接続**である旨を画面（`_tracking_notice`）と仕様書に明記。`docs/funnel-spec.md` §9.1・`docs/manual.md` §4.1・`docs/audit-degradation-techdebt-20260629.md`（H3 解消）・`docs/HANDOFF-engineer-20260629.md`・`docs/README.md`・`SPEC.md`・`docs/SPEC.md` を更新。検証: `migrate --force` / `migrate:rollback`（sqlite・新規作成と既存テーブル補完の両経路）、`route:list --name=partner`（26本）、`view:cache`、`php -l`、`PartnerScreensTest`（全画面200・CRUD・クリック計測・**他契約403**）9件＋`PartnerLinkServiceTest`・`PartnerCampaignPresentationTest`・`PartnerSummaryServiceTest` 15件。

- **`FrameGuardTest` がテストスイート全体を落としていた問題**: `private function run()` が PHPUnit の `final TestCase::run()` と衝突し、`php artisan test` が fatal で停止していた（commit 4898e4b 以降）。`handleThroughMiddleware()` に改名して解消。既知の残課題として、`UtagePageImportServiceTest`・`FunnelPageUrlImportServiceTest`・`LineMessagingWebhookServiceTest`・`DeliveryAccountLineSettingsCheckServiceTest` の **11件が本変更前から失敗** している（パートナー機能とは無関係・未着手）。

### 2026-06-02

- **契約書ステップ3：署名位置の設定**: `contract_instance_placements`（`2026_06_03_170000_*`）。PDF.js キャンバス＋左パレット（送信者／署名者）・クリック配置・ドラッグ移動・ページ／文書切替。`sign-position` GET/PUT。ステップ2「署名位置の設定へ」から遷移。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

- **契約書ステップ2：受領者帯（ワークフロー同型）**: `recipients_enabled`（`2026_06_03_160000_*`）。設定する＋モーダル追加・一覧。自社ワークフロー＝送付前社内チェック、受領者＝締結後再確認の説明を画面に明記。`syncInstanceRecipients`。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

- **契約書ステップ2：署名者追加モーダル**: `contract_instance_signers` 拡張（`party_type`・`access_code`・`allow_signer_change`・`document_actions` 等・`2026_06_03_150000_*`）。GMO 風モーダル。署名方法は **契約印タイプ**（`contract_seal`）のみ。アクセスコード＝閲覧パスワード。`syncInstanceSigners`。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

- **製品表記（画面・通知）を `monaka` に統一**: `resources/views/**` のユーザー向け文言（タイトル・説明・フラッシュ相当）の `Monaka` を `monaka` に置換。通知テスト・LINE設定チェック・友だち取り込み等の PHP ユーザー向けメッセージも同様。`MonakaImagePicker` / `initMonakaImageUpload` 等の JS 識別子は維持。検証: `view:clear`・`view:cache`（apache）。

- **UTAGE取り込み：inner行をセクション化**: `design=inner`・行幅・角丸・行背景を Monaka セクション／カラムとして独立出力（fqWaFHvUfb8Q 系LPのデザイン差の主因対応）。ページ37を再変換・`page_width=100`・`page_frame=none` 適用。検証: `UtagePageImportServiceTest` 8件。

- **UTAGE取り込み：画像幅のピクセル→%換算**: `width:410` 等を誤って `410%` にしていた不具合を修正。セクション幅・2/3/4カラム・行 `width` をコンテナ幅として換算。`repairImportedImageWidthsInPageContent` で既存ページ修復。ページ33適用済み。検証: `UtagePageImportServiceTest`（6件）。

- **URL取り込み：画像を自サーバへ保存**: `FunnelPageImportImageMirrorService` が取り込み後の `page_content` 内の外部画像を `storage`（`media/{user_id}/import/`）へ取得・`MediaFile` 登録し `/storage/...` に差し替え。`FunnelPageController@store` から呼び出し。`docs/funnel-spec.md` §5.3・`docs/manual.md` §3・作成画面の説明文を更新。検証: `FunnelPageImportImageMirrorServiceTest`・`UtagePageImportServiceTest`。

### 2026-06-02

- **URL取り込みの品質管理強化**: `FunnelPageUrlImportService`・`FunnelPageImportUrlValidator`・`import_parse_mode` / `import_diagnostics`。UTAGE 変換失敗時は既定で作成拒否（`allow_html_fallback` で HTML のみ可）。`POST funnel.page.import-preview`・`POST funnel.page.reimport-url`。`v-bind:data` の `"` 属性対応・未対応 type は html プレースホルダ。`docs/funnel-spec.md` §5・`docs/manual.md` §3。検証: `FunnelPageImportUrlValidatorTest`・`FunnelPageUrlImportServiceTest`・`UtagePageImportServiceTest`、`migrate --force`、`view:cache`（apache）。

- **契約書トップ：GMOサイン風ステップ1 UI**: `/contract` に4ステップ表示（1のみ有効）と **文書を選択**／**テンプレートから選択** の2カード横並び。新規は PDF アップロード可（`ContractInstanceDocumentService`・`contract_instance_documents`）。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:cache`（apache）。

### 2026-06-02

- **UTAGE 取り込み（form 除外・bullet・レイアウト・背景 opacity）**: `form` はスキップ（`skipped_types`）。`bullet`→ボレット＋`html_content`。狭い row は `content_max_width`。背景画像 opacity は子要素に掛からないレイヤー分離（`FunnelSectionBackgroundStyle`）。`shiny`→`shine`。`docs/funnel-spec.md` §5.3・`docs/manual.md`。検証: `UtagePageImportServiceTest`・`FunnelPageUrlImportServiceTest`・`php -l`・`view:clear`（apache）。

- **ページエディタ：公開ページを開く**: 公開条件を満たすときヘッダーに **公開ページ**（`GET /p/{uid}`・新規タブ）。`FunnelPage::isPubliclyAccessible()` / `publicPageUrl()` / `editorStatusBadgeLabel()`。`PublicFunnelPageController` はモデルメソッドを利用。ページ設定保存後にバッジ・ボタンを JS 同期。`docs/funnel-spec.md` §7・`docs/manual.md` §5。検証: `FunnelPagePublicAccessTest`・`php -l`・`view:clear`（apache）。

### 2026-06-02

- **UTAGE 取り込み：form 画像CTA・見出し重複・2カラム比率**: `form` の `img_src` を画像＋コンパクト案内に。連続同一 headline/subhead を統合。列内画像幅から `col_ratios`（例 41,15）。`docs/funnel-spec.md` §5.3 限界追記。検証: `UtagePageImportServiceTest`・`7qilDkjPor733p` 再取り込み。

- **UTAGE 取り込み：背景画像（ページ・セクション）**: `page.background_image_src` → `funnel_pages.bg_image_url`（固定背景）。`section_img_src` は row 分割時も最初の Monaka セクションの `bg_image` に反映。`docs/funnel-spec.md` §5.3・`docs/manual.md` 更新。検証: `UtagePageImportServiceTest`・再取り込み `XqzgFv7DeC1vtj`。

- **UTAGE 取り込み：登録フォーム位置のプレースホルダ＋ページ CSS**: `type=form` を `form_register`（`import_utage_form_placeholder`）に変換し、エディタ／プレビュー／公開で「登録フォームは Monaka のページエディタから設置してください」を表示。UTAGE HTML の `<style>` と HTML ブロック内スタイルを `funnel_pages.custom_css` に取り込み（作成・再取り込み）。`docs/funnel-spec.md` §5.3 更新。検証: `FunnelPageUrlImportServiceTest`・`UtagePageImportServiceTest`・対象ページ再取り込み。

### 2026-06-04

- **UTAGE URL取り込み（ヘッダーロゴ）**: `format=top`・`content_max_width`（行幅1000）・ロゴ画像は **px 固定**（`150px`）。`jEorgoOuUF3QOC` 再取り込み済み。`docs/funnel-spec.md` §5.3 追記。

- **UTAGE URL取り込み（特商法テンプレ・ヒーロー）**: 1列テキスト inner 行のヒーロー統合・`col_width_1`→`col_ratios`（23,77）・区切り線のみの行を `section_lead_elements` へ・白カード `content_max_width` 800・区切り線幅 95％・`mobile_font_size`→`font_size_sp`。`docs/funnel-spec.md` §5.3・`docs/manual.md` 更新。検証: `UtagePageImportServiceTest`（tokusho 含む 25 件）・`tjARGQ7sp0qN` 再取り込み（sections 32→18）。

### 2026-06-05

- **UTAGE 取り込み：全幅 LP の 960px デグレ修正**: `applyUtageParentContentMaxWidth` が親 `section_width:100` の row にも `content_max_width:960` を付けていた不具合を修正。inner row の `border_width` / `border_color` / `padding_bottom` をセクションへ反映。`PRlvlm4eq9jd4l` 再取り込み。検証: `UtagePageImportServiceTest`（full_width_row…）・`view:clear`（apache）。

### 2026-06-02

- **UTAGE 特商法取り込み（灰帯・白カード・段落）**: 親灰（`#f8f9fa`）＋行白は `bg_color` + `content_card_bg`。`col.padding_side` → `col_padding`。プレビュー／エディタ／`_preview_sections` 反映。`.el-text p` 段落余白。突合せ表 `docs/utage-monaka-block-mapping.md`。検証: `UtagePageImportServiceTest`・再取り込み `2BTGxyvqRC3bV9`・`view:clear`（apache）。

### 2026-06-02

- **UTAGE 取り込み：セクション統合の撤回（編集デグレ対策）**: `card_blocks` へのまとめ込みを **取り込み時は停止**（行単位 section/columns2 を維持）。エディタは `expandUtageCompactedSections` で旧 `card_blocks` を展開。960px 誤付与修正・`content_card_bg`・プレビュー partial は維持。`docs/funnel-spec.md` §5.3 更新。検証: `UtagePageImportServiceTest` 20件、`view:clear`（apache）。

### 2026-06-02

- **会員サイト URL管理：ゲストプレビューURL（1本・セッション）**: 入口 `GET /member/{slug}/preview/start`（署名付き）でセッション開始→以降 `/member/{slug}/preview/...` をログインなしで通常リンク遷移。URL管理はプレビューURL1本のみ。`MemberSitePreviewController`・`member_portal_route()`・プレビュー帯。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `MemberSitePreviewServiceTest`・`view:clear`（apache）。

### 2026-06-02

- **契約書：自社ワークフロー UI**: `contract_instance_approvers`・署名依頼画面で承認者モーダル追加。`docs/contract-spec.md` 更新。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

- **契約書：フォルダ管理・送信担当コンボ**: `contract_storage_folders`・`contract.folders.*`（文書一覧から遷移）。送信担当者は会社情報 `select`＋手入力欄。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

- **契約書ステップ2：署名依頼 UI 調整**: 送信担当者は会社情報＋手入力・所属と同一行。詳細情報を GMO 型の折りたたみでインライン編集。`docs/contract-spec.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **契約書ステップ2：GMOサイン風署名依頼 UI**: 封筒名・送信担当者・所属・保管先フォルダ・ワークフロー／署名者／受領者の帯レイアウト。`sign-request` PUT で `title`・`storage_location`・`custom_field_1/2` 保存。ステッパー完了ステップにチェック表示。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **契約書ステップ1：案内文整理・文書名個別入力**: レイアウトと画面のフラッシュ二重表示を解消（契約系 Blade）。アップロード画面で PDF ごとに文書名入力、`title` は送付件名に改称。`document_names[id]` を `PUT upload` で保存。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **契約書作成フロー（ステップ1・複数PDF）**: 文書選択後に基本情報へ直行せず `contract.instances.upload` で文書名・複数 PDF の確認・追加。`sign-request` がステップ2。`document_files[]` 最大20件。`docs/contract-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`route:list --name=contract.instances`、`view:clear`（apache）。

- **ページ設定：ヘッダー／フッター色の相互コピー**: コースページタブに **フッターと合わせる**（ヘッダー設定）・**ヘッダーと合わせる**（フッター設定）ボタン。`docs/manual.md` 更新。検証: `view:clear`（apache）。

- **ページ設定の画像差し替えUX**: コースページのヘッダーロゴ・ログインページのロゴ/背景で、既存画像あり時は既定「設定する」＋ファイル欄を表示。新ファイル選択のみで差し替え（`MembershipSiteController::updatePageSettings` は `hasFile` 優先）。削除チェックは画像を消すだけのとき。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

### 2026-06-02

- **契約書設定 メディアから選ぶ**: `contract/settings/edit` に `partials.monaka_image_picker` が無く `MonakaImagePicker` 未定義でメディア選択不可だった不具合を修正。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 署名位置 全ページ表示**: PDF を **全ページ縦並び** でレンダリング（ページ送り廃止）。ドロップ・配置枠はページ単位。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 署名位置 左パレット UI 明確化**: 操作説明・担当者ごとの色分け（黄／青／ピンク等）・縦並びチップ（種別名＋補足文言）。テキスト＝自由入力欄、日付＝カレンダー選択、署名＝印鑑配置。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 日付はカレンダー選択**: 日付枠に `input type=date`（設定パネル「日付を選択」＋枠内カレンダー）。`settings.selected_date`（ISO）。自動埋め込み ON 時は署名日プレビュー。項目名（入力ガイド）を日付から廃止。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 日付枠プレビュー修正**: 契約書上は **日付：**＋カレンダーアイコン（自動埋め込み ON 時のみ日付表示）。`input_guide` は PDF に印字せず署名画面用。日付は項目名（任意）・テキストは入力ガイドにラベル分離。「例：」接頭辞を廃止。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 配置パーツ設定（必須・入力ガイド・日付）**: テキスト／日付の設定パネルに **入力ガイド**・**必須項目にする**（`is_required`）を追加。日付は **署名日を自動で埋め込む**・**日付：** プレフィックス表示・水色パネル（GMO 同趣旨）。`settings.input_guide`・`settings.auto_fill_sign_date`。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 署名枠を印鑑マーク表示**: 署名配置パーツのペンアイコンを **印鑑（丸印）SVG** に変更。枠下に署名者名ラベル（水色・GMO 同趣旨）。左パレットも同アイコン。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

- **契約書 署名位置（テキスト入力・サイズ変更）**: テキスト枠に `settings.default_text` と枠内 `contenteditable`、設定パネル「テキスト」欄を追加。フォントサイズはプレビューへ直接反映（8〜18pt）。右下リサイズハンドルで `width_pct` / `height_pct` 変更。`docs/contract-spec.md` 更新。検証: `view:clear`（apache）。

### 2026-06-02

- **契約書：対応会社の明示選択と一覧タブ**: `contract_instances.business_setting_id`。作成・文書確認の「次へ」で会社選択モーダル（1社でも必須）。署名依頼情報で変更可。一覧は会社別タブ＋「すべて」で対応会社列。`docs/contract-spec.md` 更新。検証: `migrate --force`、`view:clear`（apache）、`php -l`。

### 2026-05-31

- **ファネルページ一覧：取込元URL表示**: `funnel_pages.import_source_url`（`2026_05_31_120000_*`）。URL 取り込み作成時に保存。一覧のページ名下に **取込元：** リンク・コピー・新規タブ（`partials/_public_url_inline` の `linkPrefix`）。`docs/funnel-spec.md` §5.5・`docs/manual.md` §3 更新。検証: `migrate --force`、`php -l`、`view:cache`（apache）。

- **ダッシュボード真っ白**: サーバ上 `/dashboard` は HTTP 200・約 30KB HTML を返すことを確認。原因候補は **Blade コンパイルキャッシュ**（`storage/framework/views` の所有者不一致）。`sudo -u apache php artisan view:clear` → `view:cache` 実施。`layouts/app.blade.php` の `$errors->any()` を `isset($errors)` 付きに変更。`docs/manual.md` にダッシュボード向け追記。

### 2026-05-29

- **商品プラン：コース単体＋結合の選択・購入後シナリオ**: プラン画面で `after_purchase_grant_target`（受講生付与と同型）・`after_purchase_scenario_id`。入金確定時 `OrderPurchaseFulfillmentService` で会員付与・シナリオ登録。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `ProductPlanPurchaseGrantServiceTest`・`php -l`・`view:clear`（apache）。

- **テストレッスン（択一・合格点）**: `lessons.lesson_type`（`normal`｜`test`）・`test_settings` JSON。管理レッスン編集で種別・設問ビルダー。会員は `POST member.lesson.test-submit` で採点・合格時のみ `member_lesson_completions`（`test_pass`）。順番受講も合格＝完了扱い。`LessonTestScoringService`・`LessonTestSettings`。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `migrate --force`、`LessonTestScoringServiceTest`・`LessonTestSettingsTest`、`php -l`、`view:clear`（apache）。

- **受講完了後の画面遷移**: `course_settings.lessons.completion_after_action`（`next_lesson` 既定｜`stay`）。コース基本設定 → レッスン基本設定でラジオ選択。`MemberPortalController::completeLesson` が次レッスン／レッスン一覧／同画面に分岐。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `BundleCourseSettingsTest`・`view:clear`（apache）。

- **コース設定「並べ替え」廃止**: ハブカード・`workspace/reorder` 画面・`PUT workspace/reorder` を削除。旧 URL は `lessons` へリダイレクト。グループ順は **グループ管理**、レッスン順は **レッスン編集の表示順**。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **コース受講状況（管理）**: `workspace/progress` で付与済み受講生の完了数・％・最終完了・受講可否を一覧（`CourseMemberProgressAdminService`）。ハブカード「準備中」廃止。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `CourseMemberProgressAdminServiceTest`、`php -l`、`view:clear`（apache）。

- **会員コース進捗の算出修正**: `progressSummary` が「視聴可能レッスン数」で100%になっていた不具合を、**受講完了登録数**ベースに統一（`all_unlocked` でも未完了は0%）。`docs/funnel-spec.md` 更新。検証: `BundleCourseAccessServiceTest`、`view:clear`（apache）。

- **コメント状況の一覧表示**: 会員サイト一覧にサイト単位の **未回答コメント件数**。サイト内コース一覧に **コメントON/OFF**・投稿数・未回答（コメント管理へリンク）。`MembershipSiteLessonCommentService` に集計 API。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `MembershipSiteLessonCommentStatsTest`、`view:clear`（apache）。

- **受講完了ボタンを会員レッスンに常時表示**: `sequential_completion` 以外（既定の全解放）でも `member/lesson` に完了ボタン・完了済み表示。次レッスン解放は従来どおり順番受講時のみ。管理の説明文・`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **レッスンアイキャッチの会員表示**: `lessons.thumbnail_url` を `member/lesson`（タイトル下・16:9）と `member/bundle` 一覧（`list-thumb`）に反映。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `view:clear`（apache）。

- **グループ管理画面で表示順変更**: `workspace/groups` に ↑↓ と **表示順を保存**（`PUT site.course.workspace.groups.order`）。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `php -l`・`route:list`・`view:clear`（apache）。

- **本番ディスク満杯（errno=28）の復旧**: ルート `/` が 100% で Laravel ログ書き込み不可。主因は MySQL **バイナリログ**（約 32 件×約 1GB）。`mysqld` 停止後に最新 2 件以外を削除し `binlog.index` を再生成 → 空き約 33GB（使用率 66%）。`binlog_expire_logs_seconds=172800`（2日）を設定。`putage` DB 本体は未変更。

### 2026-05-29

### 2026-05-30

- **契約書（GMO Sign 仕様対応）**: テンプレート（基本→詳細・PDF・複製・配置パーツ・共有・お気に入り）・契約書一覧（文書管理番号・検索・CSV・GMO ステータス）・契約書設定（会社情報・印影・メッセージ）。`contract_instances`・`ContractInstanceController` 他。`docs/gmo-sign-functional-spec.md`・`docs/contract-spec.md`。検証: `migrate --force`、`php -l`、`view:clear`（apache）。

### 2026-05-28

- **シナリオグループ（独立一覧と設定の不整合）**: 独立シナリオ用グループ（`account_id=null`）が `scenario-settings` の選択肢・保存バリデーションに出ていなかった不具合を修正（`ScenarioGroup::queryForScenarioSettings`）。一覧は空グループも見出し表示。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **商品プラン追加・編集を専用ページ化**: `product.show` のモーダルを廃止。`GET product.plan.create`・`GET product.plan.edit`・`product/plan-form.blade.php`。編集時は決済タイプ・決済方法は表示のみ。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`route:list --name=product.plan`、`view:clear`（apache）。

- **コース基本設定：レッスン基本設定区分**: `course_settings.lessons`（新規レッスンの既定公開状態・受講完了ボタン文言・本文下ボタン既定）。コメント設定を同一 `card`（`#lesson-defaults-settings`）に集約。会員レッスンは `resolveCompletionButtonLabel`。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `BundleCourseSettingsTest`、`php -l`、`view:clear`（apache）。

- **コース設定のカード順（グループ → レッスン）**: `CourseWorkspaceController::courseHubCards` で **グループ管理** を **レッスン設定** より前に配置。概要・左メニュー案内・ツールバー文言を「先にグループ、次にレッスン」に整合。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`（apache）。

- **コース一覧のサムネイル統一（管理＋会員）**: 管理 `site/show` のコースカードを 120px 固定から会員と同系の全幅 16:9（`partials/_bundle_course_thumbnail`）へ。会員 `member/course-bundle` も同パーシャルに統合。`layouts/app` に `@stack('head')`。コース追加モーダルのプレビューは 160×90。`docs/funnel-spec.md` §10.1 更新。検証: `view:clear`（apache）。

- **会員：コース画像の一覧表示（サイズ拡大）**: `member/partials/_course_thumbnail` の `card-top` をカード全幅・16:9・`min-height` 180px（md+ 200px）・`max-height` 280px に拡大。`member/course-bundle` は `col-12 col-md-6`。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `view:clear`（apache）。

- **コース基本設定：ボタンテキスト・コースカテゴリ**: ボタンは **受講する** / **カスタム**（`button_text_mode`・`button_text_custom`）。サイト設定で `course_categories` JSON を CRUD（`MembershipSiteCourseCategories`）。コースは `course_category_id` を選択。会員は `resolveButtonTextLabel`・結合内コース一覧でカテゴリ絞り込み（`show_category_filter` ON 時）。`2026_05_28_120000_add_course_categories_to_membership_sites`。`docs/funnel-spec.md` §10.1・`docs/manual.md`・`docs/membership-site-settings-design.md` 更新。検証: `migrate --force`、`php -l`、`BundleCourseSettingsTest`・`MembershipSiteCourseCategoriesTest`、`view:clear`（apache）。

- **会員サイト 管理フォームレイアウト（正）**: コース基本設定を白 `card`・上ラベル／下入力の縦積みに統一（`col-lg-3/9` 横並び禁止）。正本 `_settings_form_row` + `workspace/settings`、参照 `site/edit`。`docs/funnel-spec.md` §10.1「管理画面フォームのレイアウト（正）」・`docs/manual.md` §4.1・`monaka-docs-always.mdc` / `docs/rule.mdc` 追記。

- **レッスン追加・編集フォームの整理**: スラッグ自動生成（日本語タイトルは `lesson-{hash}`）・任意の手動変更。アイキャッチ/資料アップロード。ボタン・リンク・公開制御の説明整理。コース基本設定との役割分担を画面内表示。`lesson_settings` にヘッダー/サイドバー非表示・専用CSS。コメント承認 `comments.require_approval`。`docs/funnel-spec.md` §10.1 更新。検証: `MembershipSiteLessonSlugTest`・`php -l`・`view:clear`（apache）。

- **グループ（レッスンまとめ）のサムネイル画像**: グループ追加・編集モーダルの URL 入力を廃止。`CourseGroupThumbnailService`・アップロード/メディア選択（コースと同型）。`docs/funnel-spec.md` §10.1。検証: `php -l`、`view:clear`（apache）。

- **会員サイトレッスンコメント**: `membership_site_lesson_comments`（`2026_05_28_140000_*`）。`course_settings.comments.mode`（disabled/enabled/per_lesson）・`lesson_settings.comments_enabled`・`MembershipSiteLessonCommentService`。会員 `member.lesson.comment.store`、管理 `workspace/comments` と `site.course.comments.*`。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `migrate --force`、`MembershipSiteLessonCommentTest`、`php -l`、`view:clear`（apache）。

- **コース基本設定（区分フォーム）**: `bundle_courses.course_settings`（`2026_05_28_100000_add_course_settings_to_bundle_courses`）。`workspace/settings` を基本／オファー／自動化／コメントの4区分に再構成（縦積みラベル＋下部の緑 **保存**）。`BundleCourseSettings`・`BundleCourseAccessService` で開放日・締切・順番ロック・進捗・ボタン文言を会員側に反映。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `migrate --force`、`php -l`、`BundleCourseSettingsTest`・`BundleCourseAccessServiceTest`、`view:clear`（apache）。

### 2026-05-27

- **教材コース削除時の付与解除**: `BundleCourseDeletionService` で単体開放バンドル・当該コースのみのバンドルと `member_bundle_grants` を削除。会員マイページは教材1件以上ある付与のみ表示。修復 `putage:prune-orphan-course-bundles`。`docs/funnel-spec.md` 更新。検証: site 2 でバンドル2・付与2削除。

- **コース作成時の既定グループ「メイングループ」**: `BundleCourse` 作成後に `ensureDefaultLessonGroup()` で `courses` 行を1件自動追加（`status=published`）。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`。

- **コース内「グループ」表記の統一**: 旧 UI の「トラック／レッスングループ」は **グループ／グループ管理** に改名して復元（DB: `courses`）。`workspace/groups`・`site.bundle.groups.*`・レッスンは `groups/{group}/lessons/…`。ファネル・送信用アカウントの「グループ管理」と同じ呼び方。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`view:clear`。

- **レッスン設定（UTAGE相当・専用フォーム）**: モーダルから `site.bundle.lessons.create|edit` の専用画面へ。`lessons` に `slug`・`browser_title`・`summary`・`status`・`parent_lesson_id`・`published_at`・`thumbnail_url`・`lesson_settings`（`2026_05_27_180000_extend_lessons_for_utage_style_settings`）。会員側は `published_at`・動画オプションを反映。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`MembershipSiteLessonSlugTest`、`php -l`。

- **会員サイトフッター設定**: `course_page_settings` に `footer_bg_color`・`footer_text_color`・`footer_menu_enabled`・`footer_menus`（最大10・D&D並べ替え）。管理はページ設定→コースページ「フッター設定」。会員レイアウト下部に表示。`docs/manual.md` 更新。

- **コース一覧（トップページ）設定の拡張**: トップ説明文を TinyMCE 化、`search_scope`（bundles/all）、表示形式ラベル整理、コース名フォントのみ会員側反映、プレビュー `site.page-settings.preview-course`。`docs/manual.md` 更新。

- **お知らせプレビュー・会員表示のスタイル**: `POST site.announcements.preview` / `GET site.announcements.preview.show` で会員レイアウトのプレビュー。会員側は `.announcement-rich-content` でリッチHTMLを表示（`small text-muted` 廃止）。編集画面にプレビューボタン。`docs/manual.md` 更新。

- **お知らせの公開対象・公開終了**: `audience_type` に **全体** / **指定したコースの登録者**（`course_bundle_id`）。`published_ends_at`（未設定＝永続表示）。会員側は `MembershipSiteAnnouncement::visibleToMember`。マイグレーション `2026_05_27_160000_*`。`docs/manual.md` 更新。検証: `migrate --force`・`MembershipSiteAnnouncementVisibilityTest`。

- **ページ設定（レッスン／ログイン／固定ページ）拡張**: `lesson_page_settings`・`login_page_settings`（JSON）・`membership_site_fixed_pages`。レッスンタブは下部案内文（TinyMCE）・前後導線・CSS。ログインタブはタイトル・ロゴ/背景・遷移先・ヘッダー/フッター・CSS/JS。固定ページは CRUD と `/member/{slug}/page/{pageSlug}`（閲覧制限: 全体/会員/バンドル）。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`MembershipSitePageSettingsTest`、`php -l`、`view:clear`（apache）。

- **教材コースのサムネイルを画像指定に変更**: 「サムネイルURL」テキスト入力を廃止。コース追加モーダル・コース基本設定で **アップロード / メディアから選ぶ**（`BundleCourseThumbnailService`・`partials/_image_file_upload`）。`docs/funnel-spec.md` 更新。

- **用語「コースの結合」に統一**: ユーザー向けの「バンドルコース」を **コースの結合** に一括変更（左メニュー・各画面・商品プラン・会員マイページ・フラッシュ・仕様書）。コード識別子（`course_bundles` 等）は維持。

- **会員サイト サイト設定画面の再構成**: `site.edit` に 3区分マップ（基本/決済/カテゴリ）・関連画面カード・フォーム見出し分割・ログインURL表示。設計正本 `docs/membership-site-settings-design.md`。検証: `view:clear`。

- **会員サイト サイト設定 詳細設計書**: `docs/membership-site-settings-design.md` を新設。基本設定・決済連携・コースカテゴリを Monaka の画面/DB/実装済・未実装にマッピング。`docs/funnel-spec.md` §10.1・`docs/README.md`・`docs/manual.md` からリンク。

- **コースの結合は任意・コース単体で付与**: 受講生管理の付与で `grant_target`（`course:{id}` / `bundle:{id}`）。コース単体は `SingleCourseBundleService` が内部コースの結合を自動作成（一覧非表示）。サイト概要の「公開までの基本順序」を修正。`docs/funnel-spec.md`・`docs/manual.md`。検証: `SingleCourseBundleServiceTest`。

- **お知らせのリッチテキスト拡張**: 追加・編集のリッチテキストを **TinyMCE 7**（日本語UI）に変更。見出し・文字色/背景色・配置・リスト・インデント・リンク・画像アップロード（`media.api.upload`）・動画・表・引用・区切り線・ソースコード表示など。会員マイページは HTML 本文を表示（`announcement-body`）。`docs/manual.md` 更新。検証: `view:clear`。

- **会員サイト左メニューのツールチップ**: `layouts/app.blade.php` の `$isSiteContext` 配下（コース／コースの結合／受講生管理ほか、ページ設定子タブ、コースワークスペースの戻る・コース設定）に `sidebar-nav-tooltip`（右表示・HTML）を追加。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `view:clear`（apache）。

- **会員サイト「ページ設定」を4区分化**: `site.page-settings` を **コースページ / レッスンページ / ログインページ / 固定ページ** の切替 UI に変更。左サイドバーの「ページ設定」配下にも同4項目の子メニューを追加。既存の `dashboard_page_message`・`lesson_footer_message`・`login_page_message` はそのまま利用し、固定ページ用に `membership_sites.fixed_page_message` を追加（`2026_05_27_145500_add_fixed_page_message_to_membership_sites`）。`docs/manual.md`・`docs/funnel-spec.md` 更新。

- **会員サイトのコースページ設定（サイト外観）**: `membership_sites.course_page_settings`（`2026_05_27_152000_add_course_page_settings_to_membership_sites`）を追加し、コースページで **ヘッダー色/文字色・ロゴ表示・ヘッダーメニュー表示・トップ説明文表示・お知らせ表示・カテゴリ表示・検索表示・背景色・文字サイズ・表示形式（カード/リスト）** を保存可能にした。`layouts/member.blade.php` と `member/dashboard.blade.php` に反映。ダッシュボードはカテゴリ絞り込みと検索（結合名/教材コース名）に対応。検証: `migrate --force`、`php -l`（MembershipSiteController/MemberPortalController/MembershipSite）。

### 2026-05-26

- **コースの結合一覧の誤表示・コース選択UX**: 移行で作った「〇〇（単体開放）」を一覧から非表示（`legacy_source_bundle_course_id`・`visibleInAdmin`）。追加モーダルでコースをチェック必須。操作列の `btn-group` をやめ「コースを選ぶ」リンクを有効化。`2026_05_26_130000_add_legacy_source_to_course_bundles`。検証: `migrate --force`・`view:cache`（apache）。

- **コースの結合と教材コースの分離**: **コース**＝`bundle_courses`（教材）。**コースの結合**＝`course_bundles`＋`course_bundle_items`（複数コースの開放パック）。`bundle-settings` はバンドル一覧と「含めるコース」選択のみ（レッスン編集リンクなし）。付与・購入後開放は `member_bundle_grants.course_bundle_id`・`product_plans.after_purchase_bundle_id` → `course_bundles.id`。会員は `pack/{courseBundle}` → 各 `bundle/{course}`。マイグレーション `2026_05_26_120000_*`（旧付与の単体バンドル自動作成・`mbg_site_member_idx` 追加後に旧 unique 削除）。`docs/funnel-spec.md` §10.1 更新。検証: `migrate --force`、`php artisan test`（122件）、`view:cache`（apache）。

- **会員サイト・コースワークスペース**: コース選択後に **コース設定**（`overview` カードハブ）からレッスン等へ遷移。`CourseWorkspaceController`・`site.course.workspace.*`。`docs/funnel-spec.md` §10.1・`docs/manual.md`。検証: `view:cache`（apache）。

### 2026-05-19

- **セクション余白は margin のみ（padding 廃止）**: 右パネルからセクション padding を削除し margin 4方向に統一。旧 padding は読み込み時 margin へ移行。`_preview_sections` 同様。`docs/funnel-spec.md`・`docs/manual.md`。検証: `view:cache`（apache）。

- **セクション余白が効かない不具合**: 旧 `padding:20px` が `padding_top` 等より優先されていた。個別優先・変更時に一括クリア・読み込み時移行。プレビュー `_preview_sections` 同様。`.canvas-section.section-block` の `padding:0` 削除。`docs/funnel-spec.md`。検証: `view:cache`（apache）。

- **セクション右パネル「余白」UIの整理**: 横並びラベルによるフォント崩れ・隣列はみ出し（「デフォルト |」に見える）を `prop-control` 縦積みで解消。ラベルを上／下／左右に変更。SP用 `padding_*_sp` はセクションでプレビュー未反映のためパネル削除。固定表示・表示端末・管理名称は「表示」に分離。`docs/funnel-spec.md`・`docs/manual.md`。検証: `view:cache`（apache）。

- **ページエディタ：①②③整理（段間＋・プレビュー一致）**: `.page-boundary-slot` で段と段の間に `insertBlockBetweenPageSections`。段内は先頭/間/末尾＋。flow 高さ0・opacity のみで揺れ抑制。`docs/funnel-spec.md` §6–7・`docs/manual.md`。検証: `view:cache`（apache）。

- **ページエディタ：＋スロットがプレビューより隙間を作る不具合**: 22px スロット＋段先頭/末尾の二重スロットが編集だけの余白に。`height:0` オーバーレイ化・先頭スロット廃止（空段のみ）。`docs/funnel-spec.md` §6 追記。検証: `view:cache`（apache）。

- **ページエディタ：要素一覧にレイアウト常時表示・＋ちらつき抑制**: セクション内モーダルでもレイアウト（2/3/4カラム含む）。レイアウト選択は `insertLayoutAtPagePosition`（段分割／挿入）。`.section-insert-slot` は高さ22px固定＋ボタンのみフェード、`bindInsertSlotHoverStable`。`docs/funnel-spec.md` §6–7 更新。検証: `view:cache`（apache）。

- **ページエディタ：メイン末尾は「下に新しい段」**（配置ポリシー整理）: `#main-drop-zone` は `appendBlockViaMainDropZone`（常に新 wrapper 段）。左サイドバーは `appendBlockIntoTargetSection`（選択中 or 最後の段の内側）。経路ごとの表を `docs/funnel-spec.md` §6 に追記。ラベル「ここにブロックを追加（下に新しい段）」。検証: `view:cache`（apache）。

- **ページエディタ：編集キャンバスをプレビュー寄せ（案A）**: `editor.blade.php` — `section-block`・プレビュー同系カラムラップ、常時の追加帯／子要素ラベル／白枠廃止、`.section-insert-slot`（ホバー時＋・空のみ常時ボタン）、ブロック選択は `outline`。`docs/funnel-spec.md` §6–7・`docs/manual.md` §4。検証: `view:cache`（apache）。

- **ファネル セクション背景（エディタ復元）**: セクション／2〜4カラム選択時に右パネルで背景・グラデーション・背景画像・余白・枠線を編集。キャンバスは `getSectionCanvasStyles` でプレビューと同等表示。`docs/funnel-spec.md` §6・`docs/manual.md` 更新。検証: `view:cache`（apache）。

- **ファネル H1 等の背景帯の太さ**: ヘッドライン／サブヘッド／テキスト／ボレットで背景が単色・グラデーションのとき、プロパティ **「背景帯の太さ（上下）」** で `padding_top` / `padding_bottom` を同期設定。内側余白は背景付き要素側、左右は外枠のまま。`_preview_element.blade.php` をエディタと同等に整合。`docs/funnel-spec.md` §8・`docs/manual.md` 更新。検証: `view:cache`（apache）。

- **通知設定の Chatwork テスト送信（エラー文言・ルーム名）**: 失敗時は `ChatworkApiErrorMessage` で APIトークン／ルーム ID 向け表示。成功時は `GET /rooms/{id}` でルーム名をテスト送信欄下に表示（alert 廃止）。入れ子 form 解消・`test_room_id` 直送は継続。検証: `ChatworkApiErrorMessageTest`・`SettingNotificationChatworkTestTest`。

- **初期設定の通知設定（Chatwork）**: `operator_notification_preferences`・`operator_notification_chatwork_accounts`（最大5アカウント×5ルーム）。`setting.notification`・`OperatorChatworkNotifyService`。通知種別3種のチェックのみ（申込/キャンセル個別トグルなし）。`docs/funnel-spec.md`・`docs/manual.md`。検証: migrate、`OperatorNotificationChatworkNormalizerTest`。

- **公開イベント予約枠のカレンダー/一覧**: `public/event/show` にカレンダー（既定）・一覧タブ。初期月は開催中＝当月、開催前＝最も早い枠の月。`EventScheduleCalendarPresenter::resolveInitialCalendarDate`・`docs/funnel-spec.md` §10.1・`docs/manual.md`。検証: `EventScheduleCalendarPresenterTest`・`view:cache`。

- **予約枠の間隔UIを廃止**: 基本設定（所要時間・開催前後の確保時間）から自動算出のみ。`EventScheduleSettingsNormalizer`・`EventScheduleSlotGenerator`・日程設定画面を整理。`docs/funnel-spec.md` §10.1 更新。

- **初期設定の通知設定（Chatwork）**: `operator_notification_preferences`・`operator_notification_chatwork_accounts`（最大5アカウント×5ルーム）。`setting.notification`・`OperatorChatworkNotifyService`。通知種別3種のみ（申込/キャンセル個別トグルなし）。`docs/funnel-spec.md`・`docs/manual.md`。検証: migrate、`OperatorNotificationChatworkNormalizerTest`。

- **イベント公開URLのUI統一（ファネルページ一覧と同型）**: 折りたたみ「公開URLを表示」を廃止。`partials/_public_url_inline`・`_copy_public_url_script` を追加し、イベントワークスペース上部・イベント一覧・ファネルページ一覧で URLリンク＋コピー＋公開ページアイコンを共通化。`event/index` は `table-light`・公開列・`btn-group-sm` に整理。`docs/funnel-spec.md` §10.1・`docs/manual.md` 更新。検証: `view:cache`。

- **動画メニューブロック（`video_menu`）**: メディアグループに追加。複数話（最大10）の URL・公開話数・タブ切替プレイヤー（YouTube/Vimeo/mp4）。`FunnelPageVideoMenuElement`・`_video_menu_embed`・エディタ右パネル（UTAGE 相当の動画数/公開数/各話URL/カラー/余白/表示端末/管理名称）。検証: `FunnelPageVideoMenuElementTest`・`view:cache`。

- **決済完了後リダイレクト（ファネル決済フォーム）**: `form_payment` にリダイレクト先（ファネル他ページ / 指定URL）。`orders.post_payment_*`・`OrderPostPaymentRedirectService`・テレコム `redirect_url` 連携。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan migrate --force`、`php -l`。

- **ファネル決済フォームブロック（商品プラン）**: `form_payment` に `product_plan_id`。商品未登録時は初期設定→商品登録の案内。公開・プレビューは `public.partials._product_checkout_form` を埋め込み（テレコム単発・決済連携済みのみ）。`ProductPlanCheckoutEligibility`・`FunnelPageProductCheckoutService`。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan test tests/Unit/FunnelPageProductCheckoutServiceTest.php`、`php -l`。

- **登録経路：ファネル使用箇所の表示と削除時のクリア**: メール/LINE 登録経路一覧に **ファネルでの使用** 列。ファネル `form_register` の `registration_route_token` を走査（`FunnelPageScenarioUsageService`）。削除確認は使用中ページを列挙し、確定後に `page_content` の経路を空にしてから経路行を削除。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan test tests/Unit/FunnelPageScenarioUsageServiceTest.php`、`php -l`。

- **登録経路 UI（メール/LINE タブ分割）**: `registration-routes` を **メール / LINE** の Bootstrap タブに分割（種類コンボ廃止・表示順は channel 別）。`line-registration-routes` は `?tab=line` へリダイレクト。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`sudo -u apache php artisan view:clear`。

- **表ブロックのセル文字入力**: リサイズハンドルを `contenteditable` セル内から分離（`table-cell-wrap` + 内側 `div.table-cell-editable`）。`syncTableCellsFromCanvas` は内側 div のみ同期。`docs/funnel-spec.md` 追記。検証: `sudo -u apache php artisan view:clear`。

- **エディタ空キャンバス（表ブロック含むページ）**: `migratePageElements()` が `TABLE_DIM_LIMITS` より前に実行され JS 例外 → `renderCanvas()` 未実行。DB の `page_content` は保持（プレビューは PHP のため表示可）。`migratePageElements()` を初期化（`renderCanvas` 直前）へ移動。`docs/funnel-spec.md` 追記。検証: Node で `migratePageElements`+`renderCanvas` 成功、`sudo -u apache php artisan view:clear`。

- **ボタン初期値**: 新規ボタンは URL 空・`target=_blank`・URL 欄プレースホルダ「遷移先のURLを入力してください」。`docs/funnel-spec.md` 更新。

- **ボタン「アクション種別」の整理**: エディタから LINE登録・シナリオ登録・決済・イベント予約・会員サイト誘導を削除。旧 `action_mode` は `migratePageElements` で `url` に正規化。公開側は `url`/`target` のみ。ポップアップ・スクロールは設計区分（専用動作未実装）。`docs/funnel-spec.md` 更新。

- **左サイドバー（初期設定スクロール・ファネル構築モジュール）**: `.sidebar` / `.sidebar-scroll` の flex 高さ制約で初期設定メニューがスクロールできないデグレを修正。`funnel.*` 時は **メイン（ダッシュボード）→ ファネル構築（＋選択中ファネル）→ サポート** のみ（初期設定と同系）。`layouts/partials/sidebar-support-nav` を共通化。`docs/funnel-spec.md` §9.1 更新。検証: `php artisan view:cache`。

### 2026-05-18

- **シナリオ一覧の読者数をメール／LINEで分離表示**: `subscribers_mail_count` / `subscribers_line_count`（`Subscriber::scopeCountableForMail|ForLine`）。`docs/manual.md` 更新。

- **シナリオのアーカイブ操作（概要画面）**: `POST scenario.workspace.archive` / `scenario.workspace.unarchive`。シナリオ概要に「アーカイブに移す」（確認モーダル）と、アーカイブ済み時の警告バナー・解除ボタン。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `ScenarioWorkspaceArchiveTest`、`view:cache`。

- **LINE設定 STEP 3 の結果保存・総合判定・一覧表示**: `line_settings_last_check_*`・`line_followers_count`。STEP 3 は DKIM 同様に永続化・最終確認日時・総合 OK/NG/確認不足。一覧は総合 OK 時にアイコン・@basic ID・友だち数。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **LINE設定 STEP 1 の Messaging API メモ**: `delivery_accounts.line_messaging_memo`（任意・2000文字）を追加。STEP 1 保存とシナリオ **LINEアカウント設定** で入力。`setup_channel=line` の一覧行に表示。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: migrate、`DeliveryAccountLineSettingsServiceStepSaveTest`。

- **LINE設定の STEP ごと保存**: `account.edit`（`setup_channel=line` 含む）で LINE 連携を独立カードに分離。STEP 1 / 2 は `PUT account.line-settings.save-messaging|login` で段階保存（Webhook 同期は STEP 1 のみ）。親 `account.update` は LINE 項目なし（基本情報のみ）。STEP 3 確認 API は従来どおり。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan test`（DeliveryAccountLineSettingsServiceStepSave・LineSettingsCheck）、`view:cache`。

### 2026-05-19

- **初期設定子画面の UI 統一**: 左メニューにハブと同じ項目一覧（`setting/partials/setup-sidebar-nav`）。`setup_channel=mail|line` の送信用アカウント画面と `product.*?setup=1` も初期設定レイアウト。戻る導線を `setup-back-bar`（ボタン＋案内）に強化。`docs/funnel-spec.md` 更新。

- **初期設定「メール送信用ドメイン」**: ハブカード（「ドメイン」直後）・左メニュー・`GET /setup/mail-domain`（`setting.mail-domain`）。DKIM/DMARC/SPF UI は `account/partials/dkim-mail-domain-*` を `layouts/setup` で共用。POST 成功時は `from_setup=1` で同画面へ。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `view:cache`（apache）。

- **LINE管理：読者数推移・流入経路一覧**: `line-reader-trends`（`ScenarioSubscriberTrendService::dailyRows` の `line` チャネル）と `line-registration-routes`（LINE 種別の登録経路＋来訪/紐づけ/読者数）を **LINEチャット直下** に追加。`docs/funnel-spec.md` 更新。検証: `ScenarioSubscriberTrendServiceLineChannelTest`。

- **テキスト系4種「設定内容を初期設定に戻す」**: 規定値セクション直下のボタンで選択中ブロックのみスタイル初期値へ（本文・項目は維持）。`getTypographyFactoryStylePreset`・`resetTypographyBlockToDefaults`。`docs/funnel-spec.md` 更新。

- **PDF埋め込み（PDF.js ビューア・ホスト不一致修正）**: Chrome の iframe 直リンクは「開く」UIのみになるため `GET /pdf/embed-viewer`（ゲスト可・PDF.js CDN）で描画。**`file` は `/storage/...` パスのみ**（閲覧中ホストで PDF URL を再構築。`APP_URL` と別ホストの絶対 URL は 403）。独自ドメインで真っ白になる不具合の本命対応。`FunnelPublicMediaUrl`・`_pdf_embed`・エディタ `getPdfEmbedViewerUrl()`・`.pdf-wrapper` CSS。検証: `FunnelPublicMediaUrlTest`・`PdfEmbedViewerTest`・`curl` embed-viewer/PDF 200。

- **テキスト系4種のスマホ文字サイズ**: ヘッドライン／サブヘッド／テキスト／ボレットに `font_size_sp`（既定 `same`＝PCと同じ、`8px〜100px`）。エディタ右パネル・`syncTypographySpFontStyles`・公開/管理プレビュー（`partials/_text_sp_font_styles.blade.php`・`data-el-id`）。`docs/funnel-spec.md` §8 更新。検証: `view:cache`。

- **ファネル登録フォームブロック × シナリオ連携**: `form_register` に `scenario_id`・`registration_route_token` を JSON 保存。エディタでシナリオ選択・他ページ利用時の確認モーダル（`FunnelPageScenarioLinkController`・`FunnelPageScenarioUsageService`）。公開/プレビューは `FunnelPageScenarioFormService` と `_scenario_register_embed` でシナリオ登録フォームを埋め込み POST。`docs/funnel-spec.md` §7 更新。検証: `FunnelPageScenarioUsageServiceTest`、`view:cache`。

- **ファネル LINE ブロック × シナリオ連携**: `line_btn` / `line_qr` に `scenario_id`・任意 `registration_route_token`（LINE 登録経路）。手入力 URL 廃止。ボタンは `/line/open/{uid}`、QR は経路またはベーシックID由来 URL を QuickChart 化。`FunnelPageLineBlockService`・`funnel.scenario.line-registration-routes`。`docs/funnel-spec.md` 更新。検証: `FunnelPageLineBlockServiceTest`・`FunnelPageScenarioUsageServiceTest`。

- **コメントブロック（`comment_block`）**: インタラクションに配置。フォーム送信→`funnel_page_block_comments`→一覧表示。list/balloon/card・事前コメント JSON。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`FunnelPageCommentBlockServiceTest`。

- **次へ進む（`next_link`）**: リンク文字・`funnel_next`/`url`・`redirect_url`。次ページは sort_order 直下。`FunnelPageNextLinkService`・`_next_link`。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `FunnelPageNextLinkServiceTest`。

- **購入ボタン（`buy_button`）× 商品プラン・別タブ申込**: `product_plan_id` とボタン見た目を JSON 保存。フォームは出さず `_buy_button_checkout_link` で `/buy/{planUid}` を `target="_blank"`。`FunnelPageProductCheckoutService` は `buy_button` も走査。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `FunnelCheckoutButtonStyleTest`・`FunnelPageProductCheckoutServiceTest`。

- **コメントブロック拡張（入力項目・シナリオ連動・管理者返信）**: `form_fields_mode`（名前+メール+コメント／名前+コメント）、①選択時の `scenario_link_mode` + `scenario_id` で `FunnelPageCommentScenarioRegistrationService` が読者登録。`admin_reply` / `admin_replied_at`。ページ一覧→`funnel.page.comments` で返信。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: migrate、`FunnelPageCommentBlockServiceTest`。

- **イベント担当者設定**: `event_staff_members`・一覧/追加/編集（基本・Zoom/Meet手動・メール/チャット通知・曜日/特定日の日程）。Googleカレンダーなし。申込時 `EventStaffBookingNotifyService`。`docs/funnel-spec.md` §10.1。

- **予約枠カレンダー（日程設定）**: FullCalendar で週/月表示・色分け（予約可/残席/満席/終了）。`EventScheduleCalendarPresenter`。検証: `EventScheduleCalendarPresenterTest`。

- **イベント日程設定（UTAGE風 UI）**: `schedule_settings` JSON・`EventScheduleSettingsNormalizer`・`EventScheduleSettingsController`・`generateFromSavedSettings`。日程設定画面に基本/担当者/曜日・特定日/表示ルール。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `migrate --force`、`EventScheduleSlotGeneratorTest`。

- **イベント申込フォーム項目（シナリオ登録フォーム同系 UI）**: `event_booking_form_fields`・`EventBookingFormFieldController`・`EventBookingFormService`。ワークスペース **申込フォーム・申込者項目** はテーブル UI（並び替え・カスタム追加）。公開予約は有効項目のみ。既存 `booking_field_definitions` JSON は初回表示で取り込み。`event.edit` の JSON 欄は廃止。検証: `migrate --force`、`EventBookingFormDefaultsTest`、`view:cache`。

- **イベント予約ワークスペース（UTAGE風左メニュー）**: `EventWorkspaceController`・`GET event/{event}/workspace/{slug}`（日程設定/申込者/リマインダ/申込フォーム項目/申込フォーム/担当者/イベント設定）。`layouts/app` 左ナビを会員サイト同様に拡張。`event.show` は日程設定へリダイレクト。`docs/funnel-spec.md` §10.1 更新。

- **イベント設定（参加費・重複申込・リマインダ・連携アカウント）**: `events` に参加費種別/金額・期間限定価格・重複申込・`delivery_account_id`・リマインダ送信者を追加。`event/partials/_settings_fields`。`EventBookingRecorder`・`EventOutboundMailService`。`docs/funnel-spec.md` §10.1。検証: `migrate --force`、`EventSettingsNormalizerTest`。

- **決済フォームの送信ボタンデザイン**: `form_payment` に `button_label`・`bg_color`・`text_color`・`font_size`・`align` 等を保存。エディタ右パネル「送信ボタン」、キャンバスプレビュー・公開 LP で `FunnelCheckoutButtonStyle` / `_checkout_submit_button` を適用。`/buy/…` 単体ページは既定のまま。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan test tests/Unit/FunnelCheckoutButtonStyleTest.php`、`php -l`。

- **LINE 公式アカウント停止（Suspend）の検知と BAN 通知**: `LineAccountSuspendService`・`LineMessagingApiBanDetector`・`putage:line-account-suspend-watch`（5分ごと・`PUTAGE_LINE_SUSPEND_WATCH_ENABLED`）。Messaging API の 403 / suspended 文言・`botSuspended` Webhook（`/api/webhook/line-module`・`MONAKA_LINE_MODULE_CHANNEL_SECRET`）を検知し、紐づくシナリオの **LINEアカウントBAN通知**（Chatwork/Slack/Discord）へ送信。`delivery_accounts.line_suspend_*`・`line_bot_user_id`。`docs/line-account-ban-detection.md`・`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php artisan migrate --force`、`php artisan test`（LineMessagingApiBanDetector・LineAccountSuspendService 他）。

- **LINEチャット／友だちが空（Webhook 配信オフ）**: `monaka用アカウント`（delivery_accounts.id=16）で LINE API の Webhook endpoint が `active: false` のため、友だち追加・メッセージ送信しても `line_webhook_events` が 0 件のまま。STEP 3 は URL 一致のみ OK になっていた不具合を修正（**Webhook 配信（LINE側）** 行を追加、`active: false` で総合 NG・`line_webhook_last_error` 保存）。`LineMessagingWebhookService` は PUT で `active: true` を試行。LINEチャット／友だち画面に赤案内。検証: `php artisan test`（LineMessagingWebhookService・DeliveryAccountLineSettingsCheckService）、acc16 で sync/check 実行。

- **オペレーター同一メールで複数契約**: `operator_identities`＋ログイン後の会社選択（`login.operator.choose`）・ログイン中の契約切替（`operator.switch-company`）。権限 OFF は操作不可を UI 文言で明示。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **オペレーター編集・権限を module-nav と統一**: 一覧の編集・`PUT setting.operator.update`。権限はダッシュボード／初期設定／ファネル構築／シナリオ作成／会員サイト／イベント・予約／パートナー（`OperatorPermissionCatalog`・`2026_05_19_140000_*`）。`operator.module` とナビ非表示。`docs/funnel-spec.md` 更新。検証: `migrate --force`、`view:cache`。

### 2026-05-15

- **決済連携のプロバイダ別設定項目（UTAGE準拠）**: `payment-form-fields.blade.php` を決済代行ごとに分岐（テレコム＝クライアントIPのみ、UnivaPay新＝店舗ID＋本番/テスト各アプリトークン・シークレット、UnivaPay旧＝店舗IDのみ）。`PaymentGatewayAccount::isConfigured()`・`SettingController::validatePaymentGatewayAccount` をプロバイダ別に変更。商品プランの決済連携選択は **active かつ isConfigured** のみ。コピー用URL（通知先・ウェブフック）は **未実装**（受信実装後）。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `php -l`、`php artisan test tests/Unit/PaymentGatewayAccountConfiguredTest.php`（4件パス）。

### 2026-05-15

- **テレコムクレジット単発決済**: 決済連携編集画面に UTAGE 相当の **送信元IP・決済/退会 Webhook URL**（コピー付き）。公開 `GET|POST /buy/{planUid}` で購入フォーム→`orders` 作成→`secure.telecomcredit.co.jp` へリダイレクト。`POST|GET /api/telecomcredit/webhook/{token}`（および `/cancel`）で入金確定・`orders.status=paid`。`OrderPaymentFulfillmentService` は会員サイト/シナリオ等はログのみ。検証: `php artisan test`（PaymentGatewayAccountConfigured・TelecomCreditCheckout・TelecomCreditWebhook）、`php -l`、`sudo -u apache php artisan view:cache`。

### 2026-05-13

- **初期設定の決済連携（テレコム / UnivaPay 新旧）**: `payment_gateway_accounts` を追加し、初期設定ハブに **決済連携** カードと `setting.payment` 画面を追加。テレコム、UnivaPay（新システム）、UnivaPay（旧システム）の接続情報をユーザー単位で複数登録できる。APIキー / シークレット / Webhookシークレットは `PaymentGatewayAccount.credentials` の encrypted cast に保存し、画面へ再表示しない。`product_plans.payment_gateway_account_id` / `orders.payment_gateway_account_id` を追加し、商品プランのクレジットカード決済で利用する決済連携を選択可能。現時点では接続情報保存と商品プラン紐づけまでで、決済フォーム生成・代行会社API・Webhook入金確定・会員サイト開放/シナリオ登録/ラベル付与は後続。`php artisan migrate --force` 実行済み。検証: `php -l`、`ReadLints`、`route:list`、初期設定/決済画面/商品プラン画面のレンダリング、トランザクション内の保存・紐づけ確認。DB件数は作業前 `payment_gateway_accounts=MISSING`・`product_plans=0`・`orders=0`、作業後 `payment_gateway_accounts=0`・`product_plans=0`・`orders=0`。

- **初期設定のメール/LINE設定一覧のタブ整理**: `account.index?setup_channel=mail|line` は初期設定カード配下の用途別一覧として扱い、通常の配信画面向け **トップバー中項目（送信用アカウント / 送信ドメイン認証）** と本文先頭の **送信用アカウント一覧 / アーカイブ済** 小項目タブを非表示。通常の `account.index` では従来どおり表示。`docs/funnel-spec.md`・`docs/manual.md` 更新。検証: `ReadLints`、認証付き `account.index` レンダリング（setup_channel あり/なし）。

- **ファネルエディタのテキスト編集ツールバー/文字サイズ修正**: `editor.blade.php` のリッチテキストツールバーが選択範囲基準で文字に被り、ツールバー内クリック/プルダウン操作時の `selectionchange` で消える問題を修正。`activeRichEditable` と選択範囲保持を使い、ツールバー操作中は非表示にせず、編集中ブロックの上または下へ逃がして配置。`normalizeFontSizeValue` は `rem` / `em` を 16px 基準で px 換算し、右パネルの文字サイズ変更はテキスト系ブロックへ即時反映。吹き出し本文の `font_size` もエディタ/公開プレビューへ反映。検証: `ReadLints`、認証付き `funnel.page.editor` / `funnel.page.preview` レンダリング。

- **ファネル新規ページの空キャンバスD&D修正**: `resources/views/funnel/page/editor.blade.php` の空ページ `#main-drop-zone` は `data-section-id` を持たないため、共通 `setupDropZone()` で `addElement(type, undefined, …)` となり、左サイドバーからドラッグ＆ドロップしても無反応に見えていた。メインドロップ用に分岐し、セクション系は直接追加、要素系は通常セクションを自動作成して追加。メディアアップロードD&D（`#upload-area`）は未変更。検証: `ReadLints`、認証付き `funnel.page.editor` レンダリング。

- **ファネルを「読者状態遷移設計」として扱う土台**: `funnels` に **目的**・**状態遷移設計メモ**・**ラベル/停止条件メモ**、`funnel_pages` に **ページ種別**・**ヘッダー/フッター表示**・**全体フォント**を追加（`2026_05_13_164500_add_funnel_state_and_page_design_fields`）。ページ追加/ページ設定/ページ一覧/ファネル設定で保存・表示。ボタンブロックに **アクション種別**（URL/スクロール/ポップアップ/LINE登録/シナリオ登録/決済/イベント予約/会員誘導）と設計メモを追加。ただし実処理接続は従来の URL 遷移までで、決済・予約・会員開放などは後続の専用設定で接続する。画像/動画アップロード経路は変更しない。`php artisan migrate --force` 実行済み。検証: `php -l`、認証付きビュー描画、`MediaController@apiUpload` で画像/動画の実アップロード→後片付け（HTTP 200）。

- **イベント作成項目の保存漏れ修正**: `event.create` にあった **開催場所**・**申込開始日時**・**申込締切日時** が `EventController@store` / DB に未接続で、作成後・公開ページに反映されない不具合を修正。`events.location`・`apply_start_at`・`apply_end_at` を追加し、作成/編集で保存、管理詳細と公開ページに表示。公開ページは受付期間外なら予約枠を出さず「申込受付開始前」「申込受付は終了しました」を表示。イベント作成だけでは予約枠は作られないため、作成画面に詳細画面で予約枠を追加する案内を追加。`php artisan migrate --force` 実行済み。検証: `php -l`、`php artisan view:cache`、トランザクション内の作成値保存・公開表示レンダリング。

- **メール一斉送信の動作確認・配信日時指定**: トランザクション内 `Mail::fake()` で `ScenarioBroadcastSendService::processNextChunk` を実行し、`completed` / `sent_count=1` / `scenario_mail_deliveries.status=sent` / 件名置換を確認。現環境は `QUEUE_CONNECTION=database`・`jobs=1`・`queue:work` プロセス未検出のため、既存キュー未処理の可能性あり。`scenario_broadcasts` に `scheduled_at`・`queued_at` を追加し、作成画面で **配信日時** を指定可能にした。空欄は即時、未来日時は `ProcessScenarioBroadcastChunkJob::delay($scheduledAt)` で `jobs.available_at` を配信日時へ設定。ジョブ側も予定時刻前に実行された場合は再 delay。`docs/funnel-spec.md`・`docs/manual.md`。

- **リマインダ配信（メール）**: シナリオワークスペース `reminder-delivery` を実装（`scenario_reminders`・`ScenarioReminderController`・`ScenarioReminderDeliveryService`）。対象イベントは `events.scenario_id` が当該シナリオのもの。開始前/後・日/時/分・固定時刻・必須/除外ラベル・キャンセル除外・件名/本文を保存し、`putage:event-reminders` で期限到来分を送信。`scenario_mail_deliveries` に `scenario_reminder_id` を追加し、`source_type=event_reminder` で予約中/送信済/失敗/取消を記録。既存のイベント固定リマインドは、有効なシナリオリマインダが無いイベントのみ互換実行。LINEリマインダ・Googleカレンダー/Zoom API 自動連携・再試行回数管理は後続。`php artisan migrate --force` 実行済み。検証: `php -l`、`php artisan view:cache`、DB件数前後確認、トランザクション内の `Mail::fake()` 送信済遷移。

- **シナリオワークスペースの横タブ撤去**: `scenario.workspace.show` 本文上部の **シナリオ一覧 / シナリオ概要 / 読者一覧 / メール一斉 / ステップ配信** 横タブを削除。シナリオ内移動は左サイドバーと概要カードに集約し、本文ショートカットは概要の **シナリオ一覧へ戻る** のみにする。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **メール配信の予約中・送信済**: 共通テーブル `scenario_mail_deliveries`（`2026_05_13_151000_create_scenario_mail_deliveries_table`）を追加。`scheduled` / `sent` を実装済みにし、`_mail_deliveries.blade.php` で件名・配信種別・対象読者・配信予定/送信日時を表示。`MailDeliveryStateService` が `reserved` / `sent` / `cancelled` / `failed` を更新。ステップ配信は未来の次回ステップを予約中へ同期し、送信成功で送信済、条件除外・既存読者ポリシーで予約取消。メール一斉送信は読者ごとに送信直前予約→成功/失敗へ更新。イベントリマインドはシナリオ連携済み予約に限り予約/送信済を記録。`php artisan migrate --force` 実行済み。検証: `php -l`、`php artisan view:cache`、DB件数前後確認、トランザクション内の予約中描画・`Mail::fake()` 送信済遷移。

- **メール配信エラー検知（送信処理内）**: `scenario_mail_delivery_errors` を追加し、ステップ配信・メール一斉送信で SMTP 接続/認証/送信拒否など Monaka の送信処理中に例外検知したものを保存。`email-errors` を実装済みに変更し、`_mail_delivery_errors.blade.php` でメールアドレス・検知元・エラー内容・検知日時を表示。`StepMailDispatchService` は失敗時に `step_message_deliveries.status=failed` も記録して無限再試行を防止。後追いのバウンスメール/Webhook取り込みは別途受け口が必要な後続拡張。

- **置き換え文字の読者項目候補をフォーム利用中に限定**: ステップ配信・メール一斉送信の置き換えチップとシナリオ設定の置き換え文字表は、読者項目のうち **登録フォームで「フォーム利用」オンの項目のみ**を表示。説明文に「登録フォームに設置している項目のみ選択可能」を明記。`StepMailMergeService` もフォーム利用中の読者項目から `%キー%` の値を構築し、電話番号・住所など `subscribers` 直カラムへ保存される項目も置換できるようにした。

- **シナリオ概要ヘッダー整理**: `scenario.workspace.show` はシナリオ選択後の画面として、上部の送信用アカウント/送信ドメイン認証タブを出さず、ページ見出しと左サイドバーの選択中表示を **シナリオグループ名 / シナリオ名** に変更。画面内タブの現在位置は **シナリオ概要** と表示し、シナリオ一覧への戻り先は独立シナリオ一覧へ統一。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **ステップ配信一覧・送信順の時間軸化**: `step.index` の一覧は `sort_order` / 作成順ではなく **登録起点の時間軸順**（登録直後→登録から〇時間〇分後→〇日後＋時刻）で表示。`StepMailDispatchService` も読者ごとの `scheduledSendAt()` が早い順に並べて **未送信の次の1通** を判定するよう変更し、作成順で 1日後が先に送られる/表示される事故を防止。検証: `php -l`、トランザクション内で「1日後→登録直後→10分後」の作成順でも時間軸順になることを確認。

- **STEP配信エンジンの条件・送信後アクション接続**: `step_messages` に `condition_config` / `action_config` を追加（`2026_05_13_134500_add_step_delivery_engine_configs`）。ステップ追加/編集で **必須ラベル・除外ラベル**、送信後の **ラベル付与/除外・現在シナリオ停止・別シナリオ登録** を保存。`StepMailDispatchService` は予定到来後に既存読者ポリシーとラベル条件を判定し、対象外は `step_message_deliveries` に `skipped_existing_reader` / `skipped_condition` を記録、送信成功後にアクションを実行。検証は `php -l`、`php artisan migrate --force`、`php artisan view:cache`、トランザクション内の `StepMailDispatchService::run(..., true)`（送信1・条件スキップ1・ラベル/停止/別シナリオ登録）で確認。DB件数は作業前後とも `step_messages=0`・`step_message_deliveries=0`・`subscribers=0`・`scenarios=1`・`delivery_accounts=1`。

- **画面表示の旧サービス名除去**: `resources/views/**` の画面タイトル・説明文・補足文に残っていた旧サービス名表記を、**Monaka** または中立表現へ置換。LINE設定、登録/解除フォーム、ページエディタ、独自ドメイン詳細、会員サイトURL、イベント編集、管理新規登録などの表示文言を確認。`ScenarioWorkspaceController` のLINE通知テスト文面も `[Monaka]` に変更。

- **独立シナリオ一覧のグループ管理**: `/scenario`（シナリオ作成）でも **グループ管理** を表示し、グローバル用 `scenario_groups`（`user_id` あり・`account_id=null`）を追加/編集/削除・行割当できるようにした。送信用アカウント配下グループ（`account_id` あり）とは分離。マイグレーション `2026_05_13_113500_add_user_scope_to_scenario_groups` で `scenario_groups.user_id` と nullable `account_id` を追加。グループ削除時は所属シナリオを未分類へ戻す。`php artisan migrate --force` 実行済み。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **シナリオ一覧のメール/LINE設定確認**: `account/scenario/index.blade.php` の各シナリオ行に **メール設定済 / メール未設定**、**LINE設定済 / LINE未設定** バッジを表示。設定済みなら送信用アカウント名・送信元メール、LINE 表示名 / Basic ID も併記。新形式で片方を「紐づけない」にした場合は `account_id` から補完せず未設定として表示（旧行は互換表示）。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **シナリオ概要の未紐づけチャネル非表示**: シナリオ作成/設定でメールまたはLINEを **「紐づけない」** にした場合、`scenario.workspace.show` の概要カード・左メニュー・上部ショートカットから該当チャネルの管理項目を非表示。両方の新カラムが空の旧行のみ `account_id` から互換表示。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **会社情報 UI のグループ管理統一**: 複数登録系 UI は既存の送信用アカウント一覧・ファネル一覧に揃えるルールを `.cursor/rules/ai-collaborative-development.mdc` / `.cursor/rules/monaka-docs-always.mdc` / `docs/rule.mdc` に追加。`business_setting_groups` と `business_settings.group_id` / `sort_order` を追加し、`/business` は **追加 / グループ管理 → グループ別一覧（未分類含む）→ 行内グループ選択・編集・削除** に変更。グループ削除時は所属会社を未分類へ戻す。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **会社情報の複数登録・会社選択**: `business_settings.user_id` の unique を解除して複数会社を登録可能にし、`is_default` を追加。`funnels.business_setting_id` / `scenarios.business_setting_id` を追加し、既存ファネルは既定会社へバックフィル。`/business` は会社追加・編集・削除（最後の1件は削除不可、削除時はファネル/シナリオを残存会社へ付け替え）。ファネル作成/編集、シナリオ追加で **使用する会社** を選択可能。`php artisan migrate --force` 実行済み。

- **会員サイト削除確認**: サイト一覧のゴミ箱・サイト設定の **削除する** から `site.delete-confirm` へ進む導線を追加。削除確認画面は対象サイト名・URL・結合数・受講生数・お知らせ数・商品プラン連携を表示し、**「削除すると元に戻すことはできません。削除します。」チェック必須**で `DELETE site.destroy`。削除時は管理下ロゴ削除、商品プランの `after_purchase_bundle_id` を null 化、サイト配下データは FK cascade。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **会員サイトロゴのアップロード失敗改善**: サイト設定のロゴ画像で PHP 側 `upload_max_filesize=2M` に先に弾かれると `validation.uploaded` が出ていた。`public/.user.ini` に `upload_max_filesize=8M` / `post_max_size=12M` を追加し、アプリ側ロゴ上限を **5MB** に統一。`logo_file.uploaded` / `max` / `extensions` の日本語メッセージを追加し、画面補足と `docs/funnel-spec.md`・`docs/manual.md` を更新。

- **会員サイトのUTAGE風左メニュー・管理画面追加**: 選択中サイトの左メニューを **コース / コースの結合 / 受講生管理 / お知らせ管理 / URL管理 / サイト設定 / ページ設定** に整理。`site.bundle-settings` は `bundle_courses` を権限パックとして表示し、トラック数・付与済み受講生数・`product_plans.after_purchase_bundle_id` の商品プラン連携を確認。`membership_site_announcements` とページ案内文（ログイン/マイページ/レッスン下部）を追加し、会員マイページに公開中お知らせを表示。`php artisan migrate --force` 実行済み。購入完了Webhookからの会員作成・自動付与は引き続き未接続。

### 2026-05-09

- **独自ドメイン一覧 UI・トップ未設定の案内**: `setting/domain.blade.php` をファネル「ページ一覧」と同系（`card` + `table-light` + `btn-group-sm`）。**active かつトップページ未設定**時は一覧上に **警告アラート**（ディレクトリ一覧が出る場合は vhost/`public` の切り分け）。`SettingController@verifySiteDomainDns` 成功メッセージに **トップページ保存**を追記。`docs/funnel-spec.md` §9.1・`docs/manual.md`・`docs/rule.mdc`・`.cursor/rules/monaka-docs-always.mdc`・`funnel-spec-impact.mdc`。

- **回帰調査（2026-05-09）**: `php artisan test` **40** 件すべてパス。メディア **`/storage/` 相対 URL** は **OG・メール等の絶対 URL 必須コンテキスト**では手当てが必要な場合あり（`docs/funnel-spec.md` §8 回帰調査メモ）。**`funnel/page-editor.blade.php`** はルートから未使用（正本は **`funnel/page/editor.blade.php`**）。エディタは **CSRF meta 欠落時にスクリプト全体が落ちない**よう `csrf-token` 読み取りを防御的に変更。

- **ファネル／メディア API の公開 URL**: `MediaController` の `apiList` / `apiUpload` が **`Storage::url()`（`APP_URL` 固定）** のため、実アクセスホストと `APP_URL` がずれると **一覧サムネ・新規画像が表示されず「アップロードできない」ように見える**。**対応**: `MediaFile::publicHrefForDiskPath()` で **`request()->getBasePath()` 付きの `/storage/...`** を返却。`resources/views/media/index.blade.php` のサムネも同方式。`editor.blade.php` の `fetch` に **`credentials: 'same-origin'`・`X-CSRF-TOKEN`**、D&D は **`isFileAcceptedForCurrentTab`**（HEIC・空 MIME 等）。`MediaApiUploadTest` に画像ケース追加。`docs/funnel-spec.md` §8。

- **`php artisan migrate`・`php artisan storage:link` は利用者丸投げにしない**: シェルが使える **協働セッションではエージェントが実行**し、本番は **CI／デプロイスクリプト**に含める。`.cursor/rules/ai-collaborative-development.mdc`（回帰チェック「サーバで実行可能なデプロイ作業」）・`.cursor/rules/monaka-docs-always.mdc`・`docs/rule.mdc`・`docs/funnel-spec.md`（メディア）・`docs/manual.md` を整合。**この作業環境での確認**: `php artisan migrate --force` は **Nothing to migrate**、`php artisan storage:link` は **`public/storage` 既存**で新規作成は不要。

### 2026-05-12

- **会員サイト構築の拡張**: 既存の会員サイト土台を UTAGE 風の「サイト → コース（コースの結合） → トラック → レッスン → 会員/商品プラン付与」に寄せて拡張。`2026_05_12_233000_extend_membership_site_builder_fields` でサイトのロゴ/ヘッダー/外部メニュー/テーマカラー、コース（コースの結合）のサムネイル/公開状態、トラック公開状態、レッスン動画/ボタン/添付/リンクを追加。`config/auth.php` に `site_member` guard/provider を復帰。会員側は `status=active` サイトのみログイン、公開済みバンドル/トラックとレッスン公開＋ドリップ条件で表示。商品プランで **購入後に開放するコース**（`after_purchase_bundle_id`）を保存可能。ただし購入完了Webhookからの自動会員作成・自動付与は未接続として `docs/funnel-spec.md` / `docs/manual.md` に明記。`php artisan migrate --force` 実行済み。検証: `php -l`、`php artisan route:list --name=site|member`、`php artisan view:cache`、管理/会員ビューの tinker レンダリング。DB件数確認は作業前後とも `membership_sites=1`・`bundle_courses=0`・`courses=0`・`lessons=0`・`membership_site_members=0`・`member_bundle_grants=0`・`products=0`・`product_plans=0`（列追加のみ、意図せぬ行増減なし）。

- **会員サイトロゴのアップロード化**: `site.create` / `site.edit` のロゴ欄を URL 手入力から **ファイルアップロード**に変更。`logo_file` は png / jpg / gif / webp、現在の上限は **5MB以下**。保存先は `storage/app/public/membership-site-logos/{user_id}/{site_id}.{ext}`、`logo_url` には `asset('storage/...')` を保存。差し替え/削除時はこの管理下の旧ロゴのみ削除。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **会員サイト設定のヘッダーリンク文言**: `site.create` / `site.edit` の「メニュー名」「メニューURL」は用途が不明瞭だったため、**「ヘッダー右上リンクの表示名」「ヘッダー右上リンク先URL」** に変更し、会員サイト上部に出す任意の外部リンクであること、表示名とURLの両方があるときだけ表示されることをフォーム補足に追記。`docs/funnel-spec.md`・`docs/manual.md` 更新。

- **シナリオ独立作成・メール/LINEアカウント紐づけ**: `GET/POST /scenario`（`scenario.global.*`）を追加し、送信用アカウントを先に選ばずシナリオを作成可能にした。`scenarios` に `mail_delivery_account_id` / `line_delivery_account_id`（マイグレーション `2026_05_12_223000_add_channel_accounts_to_scenarios_table`）を追加し、既存 `account_id` は主アカウント/互換用として維持。ステップ配信・メール一斉送信・独自置き換え文字はメール紐づけを優先、LINE登録ページ・LINEチャット・LINE友だち・LINE自動応答・LINE経路意図はLINE紐づけを優先。`php artisan migrate --force` 実行済み。検証: `php -l`、`php artisan test tests/Unit/ScenarioChannelAccountTest.php`、`php artisan route:list --name=scenario --except-vendor`、`php artisan view:cache`。DB件数確認は作業前後とも `scenarios=0`・`delivery_accounts=1`・`scenario_groups=0`・`subscribers=0`（意図せぬ行増減なし、列追加のみ）。

- **Gmail テストメール不達の切り分け・MTA修正**: `oshima.maspis.com` は画面上の DNS 確認（SPF/DKIM/DMARC）は OK だったが、MTA ログでは OpenDKIM が **`no signing table match`**、Postfix が SPF 未登録の IPv6 で Gmail へ直送し **`not authorized to send email directly`** で bounce。`php artisan putage:opendkim-sync`、`chown -R opendkim:opendkim /etc/opendkim/keys /etc/opendkim/KeyTable /etc/opendkim/SigningTable`、`postconf -e 'inet_protocols = ipv4'`、`systemctl restart opendkim postfix` を実行。確認: `opendkim-testkey` は key OK、再送テストは DKIM 署名付与後 Gmail MX が **250 OK**、mail queue 空。`docs/funnel-spec.md` §9.1・`docs/manual.md` §4.1.1 に追記。

- **ページ設定保存で公開ステータスが変わらない件のデグレ修正**: メディア API が `/storage/...` 相対 URL を返す仕様に変わっている一方、`FunnelPageController@update` の `bg_image_url` / `og_image_url` が `url` バリデーションのままで、相対URLが入ったページでは公開ステータス変更も一括失敗し得た。両項目を `nullable|string|max:500` に変更し、`funnel/page/editor.blade.php` は保存失敗時に最初のバリデーションエラーをトースト表示。`docs/funnel-spec.md` §8 更新。

- **独自ドメイン詳細からトップページ公開**: 「ファネル自体を公開」してもトップ指定の `funnel_pages.status` が `private` のままだと独自ドメイン `/` は非公開案内になる。`POST setting.domain.publish-root-page` を追加し、`domain-show` の警告内 **「このトップページを公開する」** で `root_funnel_page_id` のページを `public` にできるようにした。`lp001text.maspis.com` のトップページ `AAAA` は `public` に更新済みで、`https://lp001text.maspis.com/` は 200 応答。`docs/funnel-spec.md` §9.1・§11、`docs/manual.md` §4.1.1 更新。

- **独自ドメイン `lp001text.maspis.com` のサーバ到達修正**: DNS は `tools.monaka-app.com` を向いていたが、Apache に当該ホストの vhost が無く、HTTP はディレクトリ一覧、HTTPS は別サイトのデフォルト vhost に落ちていた。`/etc/httpd/conf.d/lp001text.maspis.com.conf` と `lp001text.maspis.com-le-ssl.conf` を追加し、Let’s Encrypt 証明書を発行。`apachectl configtest` は Syntax OK、`systemctl reload httpd` 済み。`custom_domains.ssl_enabled=1`。`docs/funnel-spec.md` §9.1・`docs/manual.md` §4.1.1 に「別サイトが出る／SSL vhost」切り分けを追記。

- **開発開始前確認の強化**: 「仕様書を読んだ」だけでは着手可にせず、コード編集前に **読んだ正本と章・既存挙動・受け入れ条件・デグレしやすい箇所・検証計画** を短く残す運用を明文化。`.cursor/rules/ai-collaborative-development.mdc`・`monaka-docs-always.mdc`・`funnel-spec-impact.mdc`・`docs/rule.mdc`・`docs/AI Collaborative Development - Design Philosophy.md`・`docs/funnel-spec.md`・`docs/README.md` を更新。

- **ファネルページエディタの画像操作**: `loadMediaList` が JSON 配列以外で **`filter` が TypeError** になり一覧・以降の操作が不安定になる件を修正（配列ガード・`res.ok` 判定・非 JSON 時の案内）。アップロード成功後 **返却 URL のアイテムを自動選択**（「選択」までの迷い削減）。キャンバス上 **リンク付き画像**の `<a>` で編集画面から遷移してしまう件は **`event.preventDefault()`** で抑止。プロパティの画像 URL 入力は **`"` を含む URL でも壊れない**よう `escapeHtml`。メディアモーダル **`z-index: 12000`**（ブロック選択オーバーレイより前面）。`applyImageUrl` で **`img_src` を削除**（旧データとの二重参照防止）。`docs/funnel-spec.md` §8・`docs/manual.md` §4。

- **DKIM 画面のテストメールが Gmail に届かない件の切り分け**: アプリは `Mail::mailer()` で SMTP 送信するのみで、**受信トレイ到達は DNS・MTA（OpenDKIM 署名）・受信側評価**に依存。誤って `MAIL_MAILER=log` / `array` のとき「送信成功」に見える問題を防ぐため、**プラットフォーム既定メールが log/array のときは送信ブロック**（`DeliveryAccountController::platformMailerNonDeliveryMessage`）。成功時は **`Log::info('account.mail_test.sent')`**（`context`・`mailer`・宛先ドメインのみ。メールアドレス全体はログに出さない）。`docs/funnel-spec.md`（テスト送信・OpenDKIM 補足）・`docs/manual.md`。

- **ページエディタのメディアアップロードが常に失敗する件（本番確認）**: `Schema::hasTable('media_files')` が **false** だった。`MediaFile` 用の **`media_files` テーブルがマイグレーションに存在しなかった**ため `apiUpload` で SQL 例外 → HTTP 500 → フロントの `res.json()` が失敗し「アップロードに失敗」表示。**対応**: マイグレーション `2026_05_12_220000_create_media_files_table` を追加し **`php artisan migrate` を本番で実行**（開発環境でも同様）。`MediaController::apiUpload` に永続化失敗時の **`Log::error('media.api.upload.persist_failed')`**・孤立ファイル削除・JSON 500 を追加。`editor.blade.php` の `fetch` に **`Accept: application/json`** 等と **419／非 JSON 応答のトースト**、動画・音声タブで `media_type` 未送信にならないよう **フォールバック付与**。`tests/Feature/MediaApiUploadTest.php`（`DatabaseTransactions`・`User::create`）。`docs/funnel-spec.md` §2・§7、`docs/manual.md` 追記。

- **公開ファネル `/p/{uid}` ルート欠落の解消**: `routes/web-public.php` に `GET /p/{uid}`（`public.funnel.page`・`PublicFunnelPageController`・公開条件は `public` または予約公開の開始後・未期限切れ）。`POST /funnel/{funnel}/page/upload-image`（`funnel.page.upload`・`FunnelPageController@uploadImage`）を `routes/web.php` に復帰（`page-editor` の `route('funnel.page.upload')` 404 解消）。`docs/funnel-spec.md` §0・関連ファイル表。Feature `PublicFunnelPageRouteTest`。

- **協働ルール**: 開発前・後に「必要箇所以外のテーブル行が変わっていないこと」の確認を必須化（`.cursor/rules/ai-collaborative-development.mdc` の回帰チェック・`docs/rule.mdc`）。
- **管理ユーザー3件**: `admin1@putage.com` / `admin2@putage.com` / `admin3@putage.com` を `User::updateOrCreate`（`plan=trial`・`status=active`・パスワードは依頼どおり。チャットに値は残さずログにも出さない運用を推奨）。
- **初期設定 UI**: `layouts/setup.blade.php` から **左列サブナビを撤去**。ハブに **オペレーター管理** カードを追加。子画面は **「初期設定トップへ戻る」** のみ。（2026-05-09 追記: ハブに **メール設定**・**LINE設定** カードと、全体バッジの **会社・ドメイン・メール・LINE** 判定を追加済み。）`docs/funnel-spec.md` §9.1 更新。
- **独自ドメイン管理（UTAGE相当）**: `setting.domain` / `setting.domain.show`。`App\Support\SiteCustomDomainDnsHelp`（サブドメイン必須・CNAME 案内・ステータス文言）。一覧は `target_type` が `funnel`/`site` のみ。`PUTAGE_SITE_CUSTOM_DOMAIN_CNAME_TARGET`。`docs/funnel-spec.md` §9.1。
- **`operators` テーブル**: マイグレーション `2026_05_12_130000_create_operators_table`（`hasTable` ガード）。`setting.setup` が `Operator::count()` で落ちる不具合の解消。`docs/funnel-spec.md` §9.1 追記。

### 2026-05-09

- **独自ドメイン詳細**: `POST setting.domain.dns-check`（`SiteCustomDomainDnsVerifyService`・成功時 `status=active`＋`dns_last_*`）。ファビコンは **ファイルアップロード**（`storage/app/public/custom-domain-favicons/`・`favicon_url` は生成 URL）。`docs/funnel-spec.md` §9.1・`docs/manual.md`。

- **製品表記ルール（Monaka 統一）**: `.cursor/rules/ai-collaborative-development.mdc` に「製品名・ユーザー向け表記（必須）」を追加（**Monaka** に統一、**PUTAGE** / **Putage** / ブランド文脈の **putage** は禁止。`PUTAGE_*`・`putage:*`・`config/putage.php` 等の識別子はコード上維持・UI に出さない）。`.cursor/rules/putage-docs-always.mdc` を **`monaka-docs-always.mdc`** に改名・内容更新。`docs/rule.mdc`・`funnel-spec-impact.mdc`・`SPEC.md`・`docs/README.md`・`docs/SPEC.md`・ルート `README.md`・`docs/manual.md` 見出し・`docs/funnel-spec.md` 冒頭・`docs/delivery-line-utage-roadmap.md`・`docs/AI Collaborative Development - Design Philosophy.md` に表記方針を整合。

- **独自ドメイン一覧 UI**: 三点メニューをやめ、**設定を開く／サイトを開く／削除**を行内ボタン化。`POST setting.domain.store` 成功後は **`setting.domain.show` へ即リダイレクト**。`docs/funnel-spec.md` §9.1・`docs/manual.md` 追記。

- **独自ドメイン（UTAGE相当の表示設定）**: `custom_domains` に `root_funnel_page_id`・`favicon_url`（マイグレーション `2026_05_09_220000_*`）。`setting.domain.update`・詳細画面の説明枠・一覧の「トップページ」列。ゲストの `GET /` は `PublicCustomDomainRootController` が `Host` で解決し、`status=active` かつ公開ページ指定時は `funnel.page.preview` をプレビューバーなしで表示。`docs/funnel-spec.md` §9.1・`docs/manual.md` 更新。

- **上部 `module-nav` から「メール・LINE配信」を削除**: 横断ナビは初期設定・ファネル・会員サイト等のみ。送信用アカウント一覧は左「配信」および初期設定のメール／LINEカードから。`docs/funnel-spec.md` §9.1・チェックリスト、`docs/manual.md` §4.1、`account/index` の `page-subtitle` を整合。

- **初期設定ハブの「メール設定」「LINE設定」**: `setting/setup.blade.php` にカード追加。永続化は従来どおり **`delivery_accounts` 複数行**（`type` は `email` / `line` / `both`）。`account.index?setup_channel=mail|line` で一覧絞り込み、`account.create?setup_channel=…` でタイプ固定作成。`POST after_setup_channel` で作成後の戻り先を初期設定フローに合わせる。ハブ全体バッジは **会社・ドメイン・メール・LINE** の4カードがすべて完了で「完了」（`SettingController::setupHubOverallBadge`）。メール／LINE 各カードの集計は `SettingController`（メール系: `from_name`+`from_email`、LINE 系: `line_channel_id`+`line_channel_secret`）。`docs/funnel-spec.md` §9.1 更新。
- **（謝罪・再発防止）PHPUnit `RefreshDatabase` と本番 DB**: 一時的に Feature テストへ `RefreshDatabase` を入れた状態で **`php artisan test` が `.env` の MySQL を向くと `migrate:fresh` 相当で全テーブルが消える**。指示はなく **完全に誤り**。該当テストファイルは削除済み。`docs/funnel-spec.md` §0 に **本番で `php artisan test` をしない／テストは専用 DB** を明記。**データ復元はバックアップのみ**（例: `storage/backups/putage-latest.sql.gz`・ホスティングのスナップショット）。
- **初期設定ハブ（カード UI）**: `GET /setup`（`setting.setup`）・`layouts/setup.blade.php`・`setting/setup.blade.php`。会社情報（`BusinessSetting` の主要項目が揃えばカード「完了」）と独自ドメイン（`CustomDomain` に `status=active` があれば「完了」）のステータス表示。`module-nav`・左メニュー「設定」は `setting.setup` へ。`setting.domain.store|destroy`・`setting.operator.store|destroy` を `routes/web.php` に追加（Blade が参照していた欠落ルートの復元）。**`business_settings`**: マイグレーション `2026_05_09_120000_create_business_settings_table`（`hasTable` ガード）。`docs/funnel-spec.md` §9.1 更新。
- **上部 `module-nav` に「初期設定」**: `layouts/app.blade.php` で「ファネル構築」の左隣（`bi-house-gear`・`setting.*` で active）。リンク先は **`setting.setup`**（ハブ）に統一。
- **ルート欠落の一括復元（デグレ対応）**: `routes/web.php` から落ちていた **公開系**を `routes/web-public.php` に集約（`require`・`auth` 外）: 公開登録 `public.scenario.*`・`POST /r/{uid}/store`・解除 `public.scenario.unsubscribe.*`・`GET /line/open/{uid}`・公開イベント `public.event.*`・会員 `member.*`。**認証内**に復元: `step.create|edit|update|destroy`・`subscriber.store|bulk-status|destroy`・`event.schedules.*`・`site.bundle.*`・`dashboard.chat.store`。`Kernel` にミドルウェア別名 **`site.member`**（`EnsureMemberBelongsToSlugSite`）。`/register` を **`RegisterController`** に再接続。`MembershipSiteBundleTrackController` / `MembershipSiteLessonAdminController` の `BundleCourse` 引数を **`$course`** にリネームし `site/{site}/course/{course}/…` と整合。`docs/funnel-spec.md` §0 追記。
- **送信テスト・DKIM 補助ルート**: `account.mail-test`・`account.dkim.dns-check`・`account.dkim.mail-test` を `routes/web.php`（`auth` 内・`account` resource より前）に追加。`throttle` は funnel-spec §9.1 に合わせ mail-test 系 10,1、dns-check 30,1。`docs/funnel-spec.md` §0 に正本追記。
- **ワークスペース・一斉送信ルートの復元**: `account.workspace.*`・`scenario.workspace.*`・`scenario.broadcast.*` を `routes/account-workspace.php` に集約し、`routes/web.php` の `auth` グループ内で `require`。`Route [account.workspace.show] not defined` 等の解消。`docs/funnel-spec.md` §0 に正本パスを追記。
- **会員サイト管理ルート欠落**: `site.url`・`site.members.*` が `web.php` に無く `RouteNotFoundException`。`GET site/{site}/url` と `MembershipSiteMemberController` 用の members / grants / password を resource より前に追加。`docs/funnel-spec.md` 10.1 の管理画面行をルート名明示に更新。
- **MySQL 論理バックアップ（最新1ファイル・0時/12時）**: `php artisan putage:db-snapshot`（`App\Console\Commands\PutageDatabaseSnapshot`）。**接続先 `DB_DATABASE` の全 BASE TABLE＋データ**に加え **`--routines --events`**。実行直前に **テーブル件数**をコンソール表示。`--dated` または **`PUTAGE_DB_BACKUP_DATED_EACH_RUN`** で **`storage/backups/putage-YYYYMMDD-HHmmss.sql.gz`** も保存可能。`config/putage.php` の `db_backup_*`・`.env` の **`PUTAGE_DB_BACKUP_ENABLED`**（既定 true）。`Kernel` の Scheduler で **`twiceDaily(0, 12)`**（既存の毎分 `schedule:run` 前提）。出力は **`storage/backups/putage-latest.sql.gz`** を上書き。二重 cron 防止の例は **`bin/putage-db-snapshot.cron.example`**。運用注意を **`docs/manual.md`** に追記。
- **空 DB 向けコアスキーマ baseline マイグレーション**: `2026_03_25_000000_putage_legacy_core_baseline_tables` で `funnels` / `funnel_pages` / `delivery_accounts` / `scenarios` / `subscribers` 等を `hasTable` ガード付きで CREATE（行データは復元しない）。続くマイグレーションに空 DB 用の早期 return / `hasColumn` を追加。`dashboard_chat_messages.user_id` を bigint `users.id` に整合、`delivery_account_custom_placeholders.delivery_account_id`・`scenario_line_route_visit_intents.delivery_account_id` を `foreignId` に変更（意図テーブルで重複 FK 定義を削除）。`docs/funnel-spec.md` に「空 DB でスキーマだけ通す」を追記。
- **管理ユーザー3件の再作成（復旧白）**: `admin1@putage.com` / `admin2@putage.com` / `admin3@putage.com`（いずれも `plan=trial`・`status=active`・パスワードは依頼どおり。既存なら `updateOrCreate`）。緊急用の `admin@putage.local` は未削除（不要なら削除可）。
- **本番 DB 欠落（「データが全部消えた」）の切り分け**: MySQL `putage` が **空に近い DB に `migrate` した直後の状態**と判明（`SHOW TABLES` が `users` 等数件のみ・`migrations` も Laravel 標準数件のみ）。続くマイグレーションが **`funnel_pages` への ALTER** で **`Table 'putage.funnel_pages' doesn't exist`** により停止。別名の本番 DBは同サーバ上では **`keiba_ai` / `ml_db` / `putage` のみ**（Laravel 経由 `SHOW DATABASES`）。リポジトリ配下に putage 向け `.sql` ダンプは見つからず。**行データの復元はバックアップ必須**。`docs/funnel-spec.md` に節「データが一斉に消えたように見えるとき」、`docs/manual.md` にリンクを追記。
- **管理ログイン / 会員ガードの共存**: `LoginController` / `RegisterController` の `guest` を **`guest:web`** に変更（会員 `site_member` セッションがあっても管理ログイン画面を開ける）。`Authenticate::redirectTo` は **`$request->is('member/*')`** で会員エリアの未認証を会員ログインへ誘導（`routeIs('member.*')` 依存を避ける）。運用向けに **`docs/manual.md`** に「管理と会員は別」「`users` が空だと失敗」「`plan` の enum」を追記。
- **イベント予約（公開）・会員サイト**: マイグレーション `2026_05_12_120000_*`（`events` 拡張・`event_bookings` 拡張）・`120025`（`scenario_id` 型と FK 整合）・`120100`（`membership_site_members`・`member_bundle_grants`）・`120200`（`lessons.is_published`）。公開ルート `GET/POST /e/{uid}/...`（`PublicPublishedEventController`・`publishedEvent` バインド・`EventBookingRecorder`・確認メール）。管理: 枠 CRUD・`EventScheduleSlotGenerator`。会員: ガード `site_member`・`MemberAuthController` / `MemberPortalController`・ミドルウェア `site.member`・`member/{slug}/...`。管理: `MembershipSiteMemberController`・トラック/レッスン `MembershipSiteBundleTrackController` / `MembershipSiteLessonAdminController`。リマインド: `putage:event-reminders`・`PUTAGE_EVENT_REMINDERS_ENABLED`。`docs/funnel-spec.md`・`docs/manual.md`・`.env.example`・`config/putage.php`・`config/auth.php`。
- **LINE 登録経路の実紐づけ**: `scenario_line_route_visit_intents`（来訪記録）。`GET /line/open/{uid}?rt=` で LINE 種別経路を検証し記録。`follow` Webhook で `ScenarioLineRouteFollowAttributionService` が未消費意図を突合せ（複数時は既定スキップ・`PUTAGE_LINE_ROUTE_VISIT_FIFO_WHEN_AMBIGUOUS`）。`ScenarioLineAutoReplyService` の新規読者作成時はシナリオ内未消費が1件のとき救済消費。公開ページ QR は経路付きで `/line/open?rt=` をエンコード。`config/putage.php`・`.env.example`・`docs/funnel-spec.md`・`docs/manual.md`・登録経路画面説明を更新。**DB**: `delivery_account_id` は `delivery_accounts.id` に合わせ `unsignedInteger`（FK 不整合の解消）。**救済消費**: `tryConsumeSinglePendingForScenario` は同一条件を `lockForUpdate` でロックしてから件数判定し、競合時の取り違えを抑止。
- **登録経路（UTAGE 相当・メール部分）**: テーブル `scenario_registration_routes`。メールは `GET/POST /r/{uid}/register?rt=`・`POST /r/{uid}/store`・`POST /api/r/{uid}/register` で `ScenarioRegistrationService::normalizeOptionalEmailRouteToken` → `registration_route = public:rt:{token}`。確認フローは `register_pending['rt']`。読者一覧ラベルは `Subscriber::registrationRouteLabel` が経路名を参照。LINE の実装は上記「LINE 登録経路の実紐づけ」行を参照。
- **他シナリオ連携（同期）**: `ScenarioSubscriberCrossSyncService` を新設。`createFromValidated` と `SubscriberController@store` の作成直後に、同一アカウント・同メールの他シナリオ読者へ対称条件で反映。`_registration_fields.blade.php`・`docs/funnel-spec.md`・`docs/manual.md` 更新。
- **登録フォーム・読者項目**: （旧）保存のみ注記は同期実装により置換。
- **公開登録ページ**: ゲスト向けに DNS 検証の説明文を出していたが**削除**（検証ロジックは据え置き。仕様は `docs/funnel-spec.md` 更新）。
- **登録メールの DNS 検証**: 公開登録・読者手動追加で既定 **`email:rfc,dns`**（`config/putage.php` の `registration_email_dns_validation`・`PUTAGE_REGISTRATION_EMAIL_DNS_VALIDATION`）。読者一覧に「有効≠実在証明」の注記。`ScenarioRegistrationService::registrationEmailFormatRule` / `registrationEmailValidationMessagesForInputKey`。単体テスト `tests/Unit/ScenarioRegistrationServiceEmailRuleTest.php`。`docs/funnel-spec.md` 追記。
- **公開登録のメールエラー表示**: `allow_duplicate_registration` とバリデータ `unique` の連動が逆になっていた不具合を修正（`PublicScenarioRegistrationController` で `enforceUnique = ! allow_duplicate_registration`）。重複はメール項目に明示メッセージ（`ScenarioRegistrationService`）。登録画面にエラー一覧アラートを追加。確認画面確定失敗時は `withInput` で入力復元・成功時のみ `register_pending` 破棄。API の重複は **422** + `errors` に統一（従来の 409 専用分岐を廃止）。`docs/funnel-spec.md` 追記。
- **メール一斉送信（シナリオ）**: テーブル `scenario_broadcasts`（マイグレーション `2026_05_11_100000_*`）。ルート `scenario.broadcast.index|create|store|show`（`ScenarioBroadcastController`）。送信は `ProcessScenarioBroadcastChunkJob`（キュー）＋`ScenarioBroadcastSendService`（チャンク・`StepMailMergeService`・送信元はシナリオ extended → アカウント `from_*`）。`config('putage.scenario_broadcast_chunk_size')`・任意 env `PUTAGE_SCENARIO_BROADCAST_CHUNK_SIZE`。`jobs` テーブルは `2026_05_11_100001_create_jobs_table`（既存ならスキップ）。画面は `broadcast_*.blade.php`・ワークスペース `bulk-email`・上部タブに「メール一斉」。**`php artisan queue:work`** が無いと送信が進まない。`docs/funnel-spec.md`・`docs/manual.md` 追記。
- **ステップメール送信バッチ**: `step_message_deliveries`・`StepMailDispatchService`・`StepMailMergeService`・`putage:step-mail-dispatch`（`--force` で設定無視）。`PUTAGE_STEP_MAIL_DISPATCH_ENABLED`・`Kernel::schedule` 毎分。送信経路は `DeliveryAccountMailService`。既存読者ポリシー詳細・conditional は未実装を funnel-spec に記載。
- **本番サーバ運用（putage.maspis.com）**: `.env` に `PUTAGE_STEP_MAIL_DISPATCH_ENABLED=true` を追記済み。root crontab に `* * * * * cd /var/www/html/putage.maspis.com && /usr/bin/php artisan schedule:run >> storage/logs/scheduler-cron.log 2>&1` を追加済み（重複時は追加しないロジックで投入）。
- **ステップ本文の置き換えチップ**: ラベル（説明）＋ `%キー%` の2行表示。読者項目は登録フォームの項目名、システムは日本語説明。重複トークンはシステム側から除外（`partials/step_mail_placeholder_chips.blade.php`）。`docs/funnel-spec.md` 追記。
- **ステップ配信メールの送信元ヒント**: `step.create` / `step.edit` にシナリオ既定→アカウント送信元の順で「空欄時に使われる送信元」を表示（`partials/step_mail_from_defaults.blade.php`）。`docs/funnel-spec.md` 追記。
- **ステップ配信はメールのみ UI**: タイプ選択・タイプ別タブ撤去。`step.store` は常にメール。一覧はメールを通常表示、メール以外はレガシー行で削除のみ。`step.edit`/`update` はメール以外を一覧へ案内。`docs/funnel-spec.md`・`docs/manual.md` 追記。
- **ステップ削除**: `DELETE step.destroy`。編集画面上部に「削除」ボタン、成功時は `step.index` へ。一覧で flash 表示。
- **ステップ配信 UX**: `step.store` 成功後は一覧ではなく **`step.edit` へリダイレクト**。一覧に詳細設定の案内ブロックと「詳細設定を開く」リンクを追加。
- **ステップ追加を専用ページへ**: ポップアップ廃止。`GET step.create`・`step_create.blade.php`。一覧は `step.create` へのリンクのみ。
- **ステップ配信を UTAGE 寄せ**: `step_messages` 拡張（`2026_05_09_140000_extend_step_messages_utage_style`）。`schedule_mode`・プレビュー・送信者上書き・HTML/テキスト・配信条件モード・既存読者ポリシー・URL置換モード等。`step.edit` / `PUT step.update`・`account/scenario/step_edit.blade.php`。一覧に編集導線とタイミング要約。`docs/funnel-spec.md`・`docs/manual.md` 追記。
- **読者数推移（`reader-trends`）**: `subscribers.inactive_at`（マイグレーション `2026_05_09_120000_add_inactive_at_to_subscribers`・既存 inactive は `updated_at` で近似）。`Subscriber` の `saving` で active→inactive 時に記録。一括ステータス更新はモデル経由に変更。`ScenarioSubscriberTrendService`・`ScenarioWorkspaceController`・`account/scenario/_reader_trends.blade.php`。`docs/funnel-spec.md`・`docs/manual.md` 追記。
- **公開登録ページの脚注**: `public/scenario-register.blade.php` 末尾の PUTAGE / UTAGE 参照文言を削除（ゲスト向けに中立化）。
- **読者一覧の登録経路**: `GET route=`（`public_form`/`line`/`manual`/`other`）と列「登録経路」。`Subscriber::registrationRouteLabel()`。一括ステータス後のリダイレクトで `route` 維持。`docs/funnel-spec.md`・`docs/manual.md` 追記。
- **送信ドメイン認証の運用方針（繰り返し文言の正本）**: `docs/mail-auth-ops-defaults.md` を新設。`docs/README.md`・`SPEC.md`・`docs/manual.md`・`docs/mail-auth-for-nonengineers.md`・`docs/funnel-spec.md`・`docs/monaka-dkim-cloudflare.md`・`docs/rule.mdc`・`.cursor/rules/monaka-docs-always.mdc` に「サーバで `.env` を都度触らない・既定は config とデプロイ」を追記。**DMARC 製品既定を `quarantine` に変更**（`config/putage.php`・空の env は `MailDmarcSpec` が quarantine）。
- **DMARC 強度の環境設定**: `MONAKA_DMARC_POLICY`（none/quarantine/reject）・任意 `MONAKA_DMARC_RUA`。`App\Support\MailDmarcSpec` で案内 TXT を一元化し、`MailDomainDnsCheckService` が DNS の `p=`・`rua` を照合。`docs/funnel-spec.md`・`docs/manual.md`・`docs/mail-auth-for-nonengineers.md`・`.env.example` 更新。
- **送信テストメールの件名・本文**: `DeliveryAccountController::mailTest` / `dkimMailTest` の `[PUTAGE]` 表記をやめ、件名「送信テスト（送信経路の確認）」・本文は中立文言に。`dkim.blade.php` モーダルの「この PUTAGE の画面」を修正。`docs/funnel-spec.md` 追記。
- **運用側 SPF include TXT の Cloudflare 反映**: `App\Services\MonakaCloudflareSpfIncludeService`・`php artisan monaka:spf-include-sync-cloudflare`。`MONAKA_SPF_INCLUDE_DOMAIN`（例 `_spf.monaka-app.online`）を `MONAKA_CLOUDFLARE_SPF_ZONE_NAME`（未設定時は `MONAKA_CLOUDFLARE_ZONE_NAME`）配下として TXT を upsert。`docs/funnel-spec.md`・`docs/manual.md`・`config/putage.php`・`.env.example` 更新。
- **SPF「複数検出」の誤判定**: `MailDomainDnsCheckService::txtStringsAtHost` が `dns_get_record` の TXT で `txt` と `entries` を両方配列に入れており、SPF が 1 件でも `v=spf1` が 2 件に見えていた。`entries` 優先で 1 RR につき 1 文字列に修正。`docs/funnel-spec.md` 追記。
- **送信ドメイン認証「ドメイン登録」表示**: 各ドメイン折りたたみ内「設定確認」付近に `custom_domains.created_at` を **ドメイン登録**（`APP_TIMEZONE` で整形）として表示。DNS 反映待ちの経過判断用。`docs/funnel-spec.md` 追記。
- **送信ドメイン認証「状態」列の初回表示**: `dns_last_check_result['items']` を `account/dkim.blade.php` でキー別にマップし、`account/partials/dkim-dns-check-status.blade.php` でリロード後も **設定完了 / 未設定 / —** を SSR 表示（AJAX のみだと再読込で消える問題の是正）。`docs/funnel-spec.md`・`docs/rule.mdc` 追記。
- **送信ドメイン認証「最終確認」の時刻表示**: `config/app.php` の `timezone` を `env('APP_TIMEZONE', 'Asia/Tokyo')` に変更。`.env` / `.env.example` に `APP_TIMEZONE` を追加。`account/dkim` の Blade と `DeliveryAccountController::dkimDnsCheck` の `checked_at_human` をアプリタイムゾーンで整形。`docs/funnel-spec.md` 追記。

### 2026-05-08

- **monaka DKIM（Cloudflare 自動 TXT・案A）**: `App\Services\MonakaCloudflareDkimService`。`custom_domains`（`target_type=mail`）の作成時に Cloudflare へ DKIM 公開鍵 TXT を upsert、削除時に削除試行。設定は `.env` の `MONAKA_CLOUDFLARE_API_TOKEN` / `MONAKA_CLOUDFLARE_ZONE_NAME`（既定 `tools.monaka-app.com`）/ `MONAKA_CLOUDFLARE_DKIM_AUTO_PUBLISH`。バックフィルは `php artisan monaka:dkim-sync-cloudflare`。運用手順の正本は `docs/monaka-dkim-cloudflare.md`。`docs/funnel-spec.md`・`docs/manual.md`・`docs/README.md` 更新。
- **`docs/monaka-dkim-cloudflare.md`**: 説明時は「どこを見るか」を必ず明記する運用ルールを追記。アカウントホームの「ドメイン」空カード（地球アイコン＋「ドメインを追加」のみ）＝ゾーン未作成である旨を明記（`ドメインを追加` は B案では使わず API でゾーン作成）。
- **`docs/mail-auth-for-nonengineers.md`**: SPF/DKIM/DMARC の「何を設定し、なぜ必要で、設定すると何が起きるか」を非エンジニア向けに整理。`docs/manual.md` と `docs/README.md` からリンク追加。

### 2026-05-07

- **DKIM 署名ゾーン（運用）**: `custom_domains.dkim_private_key`（encrypted）・`dkim_public_txt`。`App\Services\DkimKeyService`・マイグレーション `2026_05_07_200000_add_dkim_keys_to_custom_domains`。`php artisan putage:dkim-signing-zone-export` で署名ホスト用 TXT を一覧。`docs/funnel-spec.md` 追記。
- **送信ドメイン認証（DNS 脚注）**: レジストラが CNAME 先の A／AAAA 存在を要求する場合・サブドメイン時のホスト名の取り違えについて `dkim.blade.php` と `docs/funnel-spec.md` に追記。
- **外部SMTPの明示トグル**: `delivery_accounts.use_external_smtp`（マイグレーション `2026_05_07_160000_*`）。オン時のみ SMTP 必須・オフ時は列クリア＋既定メール経路。各種設定フォームに UTAGE 風のプルダウン。
- **SMTP 任意・プラットフォーム既定メール**: `PUTAGE_DELIVERY_USE_PLATFORM_MAIL_WHEN_SMTP_BLANK`・`App\Services\DeliveryAccountMailService`（オフ時は外部SMTPオン必須）。
- **送信ドメイン認証のテスト送信**: 画面下部の一括フォームをやめ、**認証ドメインごとの展開内**に配置。ドロップダウンは **送信元の @ 以降がその認証ドメイン（またはサブドメイン）と一致する送信用アカウントのみ**。POST に **`mail_domain_id`** を付与し、`DeliveryAccountController::dkimMailTest` で整合検証。`docs/funnel-spec.md`・`docs/manual.md` を更新。
- **送信ドメイン認証画面**: `resources/views/account/dkim.blade.php` から **「UTAGE」表記**を除去（見出し・本文・リンク文言を中立化）。`docs/manual.md`・`docs/funnel-spec.md` の DKIM 説明も同趣旨で更新。
- **送信用アカウント一覧**: `resources/views/account/index.blade.php` の冒頭（DKIM 案内文＋注意事項 `alert`）を削除し、タブ直下から一覧が始まるようにした。

### 2026-05-03

- **管理ログインのルート修正**: `routes/web.php` の `/register` を **`RegisterController`** に接続（従来 `LoginController` に誤結線していたため `/register` が 500 になる不具合を解消）。
- **ログイン画面が白紙になる件**: `storage/framework/views` の Blade コンパイルキャッシュ破損時、HTTP 200 かつ本文 0 バイトになり得る。対処は **`php artisan view:clear`**（必要なら **`php artisan view:cache`**）。ディスク満杯後に発生しやすい。運用は [`docs/manual.md`](docs/manual.md) も参照。
