minedia-www 変更点
インタビュールーム(WebRTC 機能)を新基盤(core)へ切り出したあと、minedia-www(調査ドメイン)側で何がどう変わるか。GitHub Issue #5317。
3 行サマリ
① minedia-www 側で何が変わる?
Room・録画・文字起こし・加工動画の正本が core へ移る。www には薄い参照テーブルを 4 つ新設し、既存 3 モデルは FK を uuid 参照に繋ぎ直す。旧 Room/OtArchive 系は read-only 化 → 廃止。
② 新基盤との結合点は?
www → core の REST(命令・取次ぎ・参照)と core → www の Webhook 5 種の 2 方向だけ。core 側に www 専用の公開 API は追加しない。
結合点 →③ 追加で必要な開発は?
4 フェーズ。Phase 0(統一 IF)と Phase 1(スキーマ)は core 非依存で今すぐ着手可。Phase 2 は core の OpenAPI 公開待ち、Phase 3 のみ core 稼働待ち。
実装フロー →全体像 — 何が core へ移り、何が残るか
core へ移るもの
WebRTC 接続・録画・文字起こし・加工動画・ルーム内ログは、新基盤が単一の事実根拠(source of truth)を持つ。
minedia-www は uuid 参照と REST API / Webhook で受ける。
minedia-www に残るもの
調査業務ロジックと、オペレーター / 従業員向けの管理画面(振り返り)は残す。録画・ログは core API から取得し、同一 UI で描画する。
個人情報保護加工の依頼管理(Slack 依頼・人手待ち)と録画の外部共有(OtArchiveAccess)も、調査側の関心事として残す。
① エンティティの変化
変化は 3 パターンに畳める。
- 移管 外部化テーブルは「core の正本」と「www の薄い参照」に分かれる(4 組)。ログ類はコピーを持たず都度取得。
- 変更 www に残る事後処理モデルは、物理 FK
belongs_to :ot_archiveをrecording_uuid(文字列参照)へ繋ぎ直す。 - 新設 前身のない同期基盤(outbox・Webhook 冪等受信・エンジン切替フラグ)を足す。
図 1:エンティティ変化マップ。行が対応関係(左の 1 テーブル → core 正本 + www 薄い参照)。
新設テーブルは 5 つ。Project 系の中心は 01 のアンカー、即席は 02 の実体テーブルが同じ役を担い、03〜05 は core uuid を介してこの 2 つにぶら下がる。
外部化 Slot ごとに 1 行。「統一 IF のアダプタ先」「一覧用キャッシュ」「詳細 API 用 uuid の保持先」「Webhook の更新先」の 4 役を兼ねる。
索引:unique [slot_id] / unique [core_session_uuid]。本テーブルは Project 系(Slot)専用——即席は 02 の extemporary_sessions が実体を兼ねるため二股 FK は存在しない。OpenTok legacy 案件はこの行を持たず slot.room で従来どおり。エンジン選択は projects.webrtc_engine(default: opentok)で持ち Slot が継承する。即席は extemporary_folders.webrtc_engine(フォルダ単位・枠単位の混在なし)。
設計メモ:なぜ「薄い参照モデル」か
- Slot への列直付けは、uuid・キャッシュ・同期鮮度の置き場が散らかる。1:1 付帯テーブルに隔離するほうが移行の継ぎ目が明確。
- core の完全ローカルミラーは「事実は core が正」(B-DEC-02)と矛盾し、整合性責務が重い。採らない。
- 薄い参照は「事実は core が正」を保ちつつ、一覧性能だけローカルで担保する最小の方法。
Project に紐付かない実査(即席)は、新エンジンでは ExtemporarySlot を作らない。枠の実体(時間枠・RTC 構成)は core の Session / Room に置き、www 側は本テーブルがフォルダ紐付けと参照だけを持つ。external_ref は {"ExtemporarySession", id}。
| カラム | 型 | 説明 |
|---|---|---|
extemporary_folder_id | FK NOT NULL | フォルダ紐付け(www 固有の管理単位。営業先・担当・検索・集計) |
memo | text | www 固有の運用メモ(core に送らない) |
core_session_uuid / core_room_uuid | string | core 参照(unique [core_session_uuid]・作成 2 段階目で書き戻し) |
access_token | string | 入室 URL 用トークン(core 発行の書き戻しコピー・unique) |
name / start_time / end_time | string / datetime | ライトスルーキャッシュ(正は core Session。一覧表示・フォルダ検索・URL 開示可否判定用) |
first_entered_at | datetime nullable | W1 初回受信で set。フォルダ一覧の Room 数バッジのカウント材料(B-DEC-14) |
cancelled_at | datetime nullable | キャンセル事実(www 起点操作のためライトスルーで自明に記録) |
last_synced_at | datetime | Webhook / リコンサイルの鮮度 |
動き — 作成は 2 段階・設定は core へ封入
- 行 INSERT(id 確定)→ core
POST /rooms(nestedsession{}・external_ref={"ExtemporarySession", id}。archive_*・文字起こし構成・入室文言はフォルダ設定から解決して Room 直下に封入)→ uuid / token 書き戻し。失敗時はcore_session_uuid IS NULLの行をリトライ / 掃除ジョブで回収。 - RTC・録画・文字起こし設定は www に保存しない(二重管理の消滅)。編集は core
PATCH /sessions成功後にローカル更新(ライトスルー)。 - キャンセルは core
DELETE /sessions(Room close にカスケード)→cancelled_at記録。行は消さない(録画・集計の親)。
設計メモ:なぜ ExtemporarySlot を残さないか
- ExtemporarySlot 13 列のうち www が正本として持つ固有情報はフォルダ紐付けと memo だけ。実体を残すと RTC 設定が www と core の二重管理になる。
- Slot(Project 系)には募集・割付・謝礼という www 台帳が残るが、即席には存在しない——この非対称が「Project 系=薄い参照・即席=直参照」の根拠。
- 既存データは移行しない(即席枠は使い捨て)。OpenTok フォルダの ExtemporarySlot はレガシー経路で従来どおり、新エンジンフォルダの新規枠から本テーブル。
- 提示物(handout)は www に行を作らず core へ直接登録(external_ref なし)。書類置き場(
extemporary_folder_files)は www 完結のまま無変更。
一覧・集計用の録画メタ。録画 1 行=1 レコードで持ち、合計値には畳み込まない。SUM / GROUP BY は読み取り時に行う(B-DEC-11)。
| カラム | 型 | 説明 |
|---|---|---|
interview_room_ref_id / extemporary_session_id | FK nullable(どちらか一方) | 親。Project 系=01、即席=02。Webhook の room uuid 突合は親テーブルの core_room_uuid 解決を経由 |
slot_id / project_id / extemporary_folder_id | bigint nullable | 非正規化コピー。集計 JOIN を短縮(Room ホップ除去)。Project 系は slot / project、即席はフォルダ |
recording_uuid | string | core Recording 参照(unique) |
status | string | available / failed 等。一覧の「録画あり/失敗」表示ヒント |
duration_sec | integer | 録画時間(core Recording.duration_sec のコピー) |
recorded_at | datetime | 録画日時(月次集計のバケット) |
動き — Webhook 契機の同期(B-DEC-12)
- 録画 Webhook 受信 → payload を直書きせず core
GET /rooms/{id}/recordingsを read-back → room 単位で冪等 upsert(消えた行は削除)→last_synced_at更新。 - Webhook 欠落は日次リコンサイルで補足。
- 録画削除(www 起点
DELETE /recordings/{id})も同一フローで行を削除。
読み手 — どの画面がこの表を見るか
即席フォルダ一覧の録画時間合計・ダッシュボード月次統計・Room#movie_duration_sec 後継。Slot 詳細ページは読まない(core API on demand・B-DEC-03)。
設計メモ:なぜ集約列ではなく行キャッシュか(B-DEC-11)
ダッシュボードの月次録画時間統計は「録画ごとの duration × 録画日時」がないと再現できない。合計 1 値の集約列では月別に割れないため、行のまま持つ。「薄い参照」から半ミラーへ一歩踏み込むが、正は core のまま・用途は表示/集計ヒントに限る。
文字起こしのコア化で本文正本(VTT)は core へ移り非公開になる。Employee ジョブ一覧の「文字起こし内容」横断検索と派生機能(字幕 DL・AI チャットボット・要約)の材料を残すため、Webhook 契機で本文を取得しローカル保存する(B-DEC-06)。core に検索 API は足さない。
| カラム | 型 | 説明 |
|---|---|---|
transcription_uuid | string NOT NULL | core Transcription 参照(unique)。legacy 受け入れ列は初期スキーマに仕込まない(B-DEC-09) |
recording_uuid | string | 消去整合の突合キー(index) |
slot_id / project_id | bigint | ローカル JOIN 用。検索 JOIN は project→slots→本テーブルの 2 hop に短縮 |
kind | string | transcription / translation。全 kind 保存=現行の日英区別なしヒットを踏襲 |
language | string | BCP-47 |
status | string | done / error。検索は常に status: :done 条件(B-DEC-13) |
text | mediumtext nullable | VTT から抽出した本文(fulltext index)。error 行は null |
raw_vtt | mediumtext nullable | VTT 原文(cue 表示・字幕 .docx/.txt/.csv の材料) |
動き — Webhook 契機の取得・消去整合
transcription.completed受信 →GET /transcriptions/{id}でresult_url(TTL 3600 秒)→ VTT をフェッチし保存。error 時も失敗行を保存(ダッシュボード表示用)。- 依存機能はすべて本テーブルを読む:ジョブ横断検索 / 字幕 DL / cue 表示 / AI・要約の入力 / ダッシュボード「直近アウトプット」。
- 消去整合(B-DEC-07):www の録画削除フロー内で当該
recording_uuidの行を同時削除。削除系 Webhook は新設しない。
設計メモ:1 文字起こし=1 行(チャンク分割をやめる)/並行運用期の 2 系統検索(B-DEC-09)
チャンク分割の廃止:現行 TranscriptionAnalyzer の 65,535 バイト分割は TEXT 型上限の産物。InnoDB の FULLTEXT は MEDIUMTEXT(16MB)にも張れるため踏襲しない。チャンク境界をまたぐフレーズがヒットしない現行欠点も解消。実装時は現行 index の実 DDL(parser 指定の有無)を SHOW CREATE TABLE で確認し、同一構成にする。
並行運用期:OpenTok 案件は現行 transcription_request_analyses、新エンジン案件は本テーブル(dual-write なし・書き込み経路は独立)。search_text はアプリ層で 2 系統を別クエリ実行し、research_project id の和集合で絞る。1 ジョブ=1 エンジンのためマージはクリーンに成立。
完全移行での一本化:旧経路の新規発生が止まったら、旧 analyses → 本テーブルへ一括バックフィル(チャンク連結)し 2 系統検索を退役。legacy 参照キーは、データ移行範囲(過去録画・チャット・入退室の core 移送 or read-only 残置)の決定と併せて確定。
core は加工の「ファイルの事実」だけ持ち、加工内容(ブラー / 音声変換)のメタは持たない。現行 ProcessedOtArchive が混載していた個人情報保護加工の依頼管理(Slack 依頼 → 人手待ち → 手動アップロードで完了)の後継を www に新設する。
| カラム | 型 | 説明 |
|---|---|---|
ot_archive_id / 将来 recording_uuid | FK / string | 加工元録画への参照(二重運用期は engine 分岐) |
processes | json | 依頼内容 ["blur", "voice_change"] |
status | enum | requested / completed / cancelled の 3 値のみ |
processed_recording_uuid | string nullable | 成果物=core ProcessedRecording 参照。完了で埋まる |
requested_at / completed_at | datetime | 依頼・完了時刻 |
動き — 依頼から完了まで
① オペレーターが保護加工を選択 → レコード作成(requested)+ Slack 依頼(現行踏襲) → ② 人手加工後、core へ手動アップロード(署名 PUT → complete) → ③ processed_recording.status_changed(available)受信で completed 化。
設計メモ:持ち込まないもの
トリムのみの依頼はレコードを作らない(core のトリム指示 API 直行・現行 auto? 分岐の消滅)。unprocessed/started/expired のような曖昧な中間状態も廃止する。
残留モデルの繋ぎ直し — 物理 FK → recording_uuid
| モデル | 現状 | To-Be |
|---|---|---|
TranscriptionRequest | belongs_to :ot_archive | core へ移管 調査側からは廃止。本文は interview_transcripts が後継 |
ProcessedOtArchive | belongs_to :ot_archive | core へ移管 依頼管理のみ recording_processing_requests が後継 |
StatementSummaryRequest(要約) | belongs_to :ot_archive | 変更 recording_uuid+transcription_uuid+自前 slot_id/project_id を保持 |
OtArchiveSummary | belongs_to :ot_archive | 変更 recording_uuid 保持 |
OtArchiveAccess(外部共有) | belongs_to :ot_archive | 変更 recording_uuid 保持 |
動画再生 URL:ot_archive.video_s3_key → 新エンジンは core GET /recordings/{id}/download-url(短命の署名 S3 URL)。旧 OpenTok 案件は ot_archive のまま(二重運用)。
① 読み取り経路の変化 — 統一インターフェース
二重運用期(OpenTok 既存+新基盤)の分岐は Slot#interview_room が吸収する(B-DEC-01)。呼び出し側はエンジンを知らない。
Slot#interview_room
├─ engine == opentok → OpentokRoom.new(room) # 既存ローカル Room をラップ・無変更
└─ engine == livekit → LivekitRoom.new(interview_room_ref, core_api_client)
共通インターフェースは 6 メソッド:#chat_messages(usage:) / #entry_histories / #recordings / #playback_events / #access_url(role:) / #movie_duration_sec
各アダプタの実装イメージ
OpentokRoom — 既存のローカル Room / OtArchive をそのまま読む薄いラッパ。挙動は無変更。
class OpentokRoom
def initialize(room) = @room = room
def chat_messages(usage:) = @room.room_chat_messages.where(usage:)
def entry_histories = @room.room_entry_histories
def recordings = @room.ot_archives
def playback_events = @room.playback_events
def access_url(role:) = @room.room_access_token(role)
def movie_duration_sec = @room.ot_archives.sum(:file_duration_sec) # DB集計
end
LivekitRoom — 一覧・集計は recording_refs で即返し、詳細はその場で core API を叩く。API 障害時はデグレード値を返す。
class LivekitRoom
def initialize(ref, client)
@ref, @client = ref, client
end
# 一覧・集計:API を叩かずローカル行キャッシュで即返し
def movie_duration_sec = @ref.recording_refs.sum(:duration_sec)
# 詳細:core API on demand。障害時は「一時的に取得できません」+uuid
def chat_messages(usage:)
@client.get("/rooms/#{@ref.core_room_uuid}/chat-messages", channel: usage)
rescue Core::ApiError
Core::Degraded.new(@ref.core_room_uuid)
end
def entry_histories = @client.get("/rooms/#{@ref.core_room_uuid}/entry-histories")
def recordings = @client.get("/rooms/#{@ref.core_room_uuid}/recordings")
def playback_events = @client.get("/rooms/#{@ref.core_room_uuid}/room-events", type: "handout")
def access_url(role:) = Core::RoomAccessToken.sign(room: @ref.core_room_uuid, role:) # 自己署名(core への往復なし)
end
主な読み取り書き換え
| 箇所 | 現状 | To-Be |
|---|---|---|
| 振り返り画面 | @room.room_chat_messages 等を直接 | @interview_room.chat_messages(usage:)。Livekit は core API on demand |
| ダッシュボード「直近アウトプット」 | TranscriptionRequest / ProcessedOtArchive を Room 経由 JOIN | 材料を差し替え(B-DEC-13):interview_transcripts+recording_processing_requests を自前 slot_id/project_id でローカル JOIN |
| ダッシュボード月次録画時間統計 | OtArchive.movie_duration_sec_per_month | recording_refs を recorded_at の月別 GROUP BY(B-DEC-11) |
| 一覧 N+1 prefetch | includes(room: :ot_archives) | prefetch ごと削除——表示に使われていない dead code |
| 即席フォルダ一覧(録画時間・Room 数) | joins(room: :ot_archives).sum / Room 行数 | recording_refs.duration_sec の SUM(extemporary_folder_id 絞り・JOIN レス)/ extemporary_sessions.first_entered_at IS NOT NULL をカウント(B-DEC-14) |
| チャット CSV ダウンロード(3 画面) | room_chat_messages を DB 直読み → CSV | BFF が core 履歴 API を全ページ取得し CSV 整形(B-DEC-10)。添付は attachment_name のみ・スタンプは www 側 I18n 辞書 |
API on demand の劣化設計:core API 障害時は、チャット / 録画セクションのみ「一時的に取得できません」+ uuid のデグレード表示。ページ全体は調査データで描画を継続する。
② 新基盤との結合点
結合は www → core の REST(①命令 ②取次ぎ ③参照)と core → www の Webhook(④通知)の 2 方向だけ。core が www を同期呼び出しすることはなく、core 側に www 専用の公開 API も追加しない。
図 2:結合点マップ。実線= www → core の REST、破線= core → www の Webhook。
権威の分界(B-DEC-05)— 衝突時の勝者を先に決めておく
| 軸 | 権威 | 乖離時の是正 |
|---|---|---|
| ライフサイクル(キャンセル・中止・時間枠) | 調査 | core へ DELETE / PATCH を再送 |
| 事実(入退室・録画 / 文字起こし status・ban) | core | GET でローカルコピー(recording_refs 等)を上書き |
| 出欠・謝礼・合否 | 調査 | core へ書き戻さない(調査内で完結) |
「GET が真実の源泉」は事実系にのみ適用され、ライフサイクル系は逆向き。この非対称の明文化が勝者規則の本体。LWW(タイムスタンプ比較)は採らない。
www → core(命令・取次ぎ・参照)
| 契機 | API | 備考 |
|---|---|---|
| Slot 作成 | POST /sessions | outbox +リトライ。失敗しても Slot 保存は成功させる(入室 URL は「準備中」) |
| キャンセル・枠削除 | DELETE /sessions/{id} | outbox 経由・404=成功扱いの冪等。teardown_state で伝達状態を追跡 |
| 録画削除 | DELETE /recordings/{id} | Transcription 連鎖削除。同一フローで interview_transcripts も削除(B-DEC-07) |
| 退会 → 個人情報消去 | erasure(匿名化) | core 契約への追加依頼(要調整)。完了は erasure.completed Webhook |
| 入室(毎回) | —(自己署名・core 呼び出しなし) | BFF が tenant 0 の秘密鍵で参加 JWT を署名し、ブラウザへ返す(下記の認可要件) |
| 緊急発行(force の後継) | —(自己署名・時間窓チェックを www 側でスキップ) | generate_force_access_token を置換。nbf / exp の導出はテナント裁量(検証側は上限強制のみ) |
| 詳細画面の表示 | GET /rooms/{id}/… 各種 | on demand・保存しない。出欠・評価の材料(在室時間)も entry-histories を都度参照 |
core → www(Webhook 5 種)
| Webhook | www 側の処理 |
|---|---|
participant.entered(W1) | Interview.attend_at(panelist 初回・冪等)+ first_entered_at 記録(Project 系=01・即席=02。Room 数カウント材料・B-DEC-14) |
recording.status_changed | core を read-back して recording_refs を room 単位 full sync(B-DEC-12)→ 事後処理のトリガー |
transcription.completed | 本文(VTT)を取得し interview_transcripts へ保存 → 要約 / 集計ジョブのトリガー |
processed_recording.status_changed | available → 加工依頼を完了化。failed → オペ向け通知(現行 Slack 失敗通知の後継) |
erasure.completed | 消去の完了確認(契約合意後) |
Session の終了に push はない。終了検知は GET /sessions(status は時刻からの導出値)。
整合性担保の三層防御
FK を持たない uuid 境界では、削除・同期の整合を DB が保証しない。以下の三層で担保する(🧩 タスクとして実装フローに割り付け済み)。
| 層 | 仕組み | 割り付け先 |
|---|---|---|
| ① 削除伝達(一次防御) | ローカル削除と同一 Tx で outbox に書く → ワーカーが冪等 DELETE → teardown_state: confirmed | Phase 1(schema)/ Phase 2(実装) |
| ② 突合ジョブ(二次防御・GC) | 日次 reconciliation=死参照掃除+孤児掃除。grace period 24h・検知→是正の段階有効化 | Phase 3(実 core 前提) |
| ③ 権威表(衝突の決着) | 上記「権威の分界」をフィールド単位の定数にし、乖離時の勝者を機械的に決定 | Phase 2(定数化・是正ロジック) |
core 契約への追加依頼(要調整):(a) erasure API(external_ref 単位の匿名化+erasure.completed)——退会時に core 側へ残る display_name 等の個人情報を消すために必須。(b) 突合用一覧 GET /sessions?updated_since=。erasure 合意前に二重運用を始めると、livekit 案件の退会処理に個人情報残留ギャップが生じる。
即席調査の入室 To-Be — ロール偽造の封じ込め
即席実査は extemporary_sessions(① §02)で実体化するが、入室トークンの持ち方だけは As-Is の穴を引き継がないよう作り替える。即席は User / Answer ドメインを持たず、パネリスト・モデレーター・見学者・運用が同じ枠へ入るため、ロールを詐称されやすい。
As-Is の穴:1 枠に単一 room_access_token、ロールは ?interview_role= クエリ(lib/tb.rb の 4 値)で申告する。CSV は同じトークンで 4 本の URL を出し、違いはクエリだけ。panelist の URL を受け取った人が ?interview_role=minedia_observer に書き換えれば、UI 経由でそのまま権限昇格できる(生 API も申告ロールを信用)。
To-Be — ロール別トークン + サーバー確定署名
単一トークン+ロールクエリをやめ、ロールごとに別トークンを発行する。?interview_role= は廃止。入室時、www(tenant 0 = 発行者)がトークンからロールを引き当て、ロールを participant JWT のクレームに封入して署名する(Model B)。基盤コアは署名を検証するだけで、ロールの申告は下流に一切流れない。参加者はロールを選べず、署名後は改ざんできない。
実装は extemporary_sessions に紐づく www 側のロール別トークン表で持つ。ロールの権威は www に閉じ、core は関与しない(core は JWT の role クレームを検証するだけ)。
| カラム | 型 | 説明 |
|---|---|---|
extemporary_session_id | FK NOT NULL | 枠紐付け(extemporary_sessions) |
role | enum | panelist / moderator / observer / minedia_observer(基盤定義の閉じた語彙) |
access_token | string | 配布 URL 用のロール別トークン(has_secure_token・unique) |
revoked_at | datetime nullable | ロール単位の個別失効・再発行用 |
索引:unique [extemporary_session_id, role] / unique [access_token]。枠作成時(webrtc_engine=livekit)に 4 ロール分を一括生成。単一 access_token は本表に置き換える——「トークンが当たれば申告ロールを信じる」構造を、トークン自体がロールを担う構造へ。
入室フロー(認証ページ「即席調査」タブに対応)
- 配布 URL(ロール別トークン)クリック → www が
access_tokenから(session, role)を確定。revoked_at/ 時間窓を検証。?interview_role=は読まない。 - www が core Room uuid を確定し、participant JWT
{tenant:0, room, participant, role, exp, jti}を tenant 0 の秘密鍵で署名(基盤への往復なし)。 - フラグメント付きリダイレクト → room SPA が JWT を回収。
- core が署名・クレーム・越境・失効を検証 → RoomPolicy から grants 導出 → LiveKit トークン発行。
- 入室待機画面で表示名のみ入力(即席は identity 不要の形式)。
ロールはラベルであって能力ではない。JWT に載るのはロールラベルのみで、canPublish 等の能力は基盤の RoomPolicy が専有する(認可設計)。テナントがどんな JWT を署名しても、メディア層の能力は基盤の導出結果に限定される。
移行とスコープ
- エンジン選択は
extemporary_folders.webrtc_engine(フォルダ単位・default opentok)。opentokフォルダの即席枠は現行 legacy 経路(単一トークン+ロールクエリ)のまま——手を入れず OpenTok 撤去まで併存する。livekitフォルダの新規枠から本方式に乗る。既存配布 URL は壊れない。 - 本 To-Be のスコープはロール偽造(権限昇格)の防止のみ。
name(入室待機画面で入力する表示名)とuuid(接続識別用の派生 ID)は身元を主張せず、即席は identity 不要の形式のため、identity なりすましの残余リスクは実質存在しない。 - 入室時、www はトークン照合(現行の入室ページ認可の継承)を経てから署名する。ロール別トークン化はその上に「申告ロールを信じない」構造を足すもの——トークンが当たっても、名乗ったロールではなくトークンに紐づくロールで署名される。
③ 追加で必要な開発 — 実装フロー
主軸は A主軸ハイブリッド:既存を壊さない継ぎ目(統一 IF)を先に完結させ、新エンジン部は契約モックで先行 TDD し、core を待つ。core 依存度で 4 フェーズに割れる。
統一 IF Slot#interview_room を導入し、既存 OpenTok Room を OpentokRoom でラップ。この段階では常に OpentokRoom を返す(engine 分岐なし)。
OpentokRoomクラスを追加し、共通 IF 6 メソッドを既存Room/OtArchive委譲で実装Slot#interview_roomを追加(暫定で常にOpentokRoom.new(room)を返す)- 振り返り画面と
ot_archives_controllerを@interview_room.*経由へ書き換え - OpentokRoom の IF 単体テスト+既存リグレッションテストを通す
受け入れ:挙動完全無変更・全テスト緑(純リファクタ)。
エンジン分岐・薄い参照モデル・繋ぎ直しのスキーマを用意する。新列は未使用(default/null)でも既存集計が壊れない状態にする。
- migration:
projects.webrtc_engine/extemporary_folders.webrtc_engine(default opentok) - migration:
interview_room_refs(slot_id単独・uuid 群・first_entered_at・unique 索引)/extemporary_sessions(即席の実体)+モデル/関連定義 - migration:
recording_refs(録画メタの行キャッシュ) - migration:
interview_transcripts(textmediumtext + fulltext index) - migration:
recording_processing_requests(加工依頼管理) - migration: 事後処理モデルへ
recording_uuid/ 自前slot_id/project_idを追加 - 物理 FK
belongs_to :ot_archiveを nullable 化(廃止は Phase 3) - ローカル JOIN / 集計経路を engine 分岐で用意。dead な一覧 N+1 prefetch は削除
- 🧩 migration:
core_outbox/interview_room_refs.teardown_state/core_webhook_events(同期基盤)
受け入れ:migration 可逆・既存 OpenTok 経路は無変更で緑。
LivekitRoom 経路一式を core 契約に対して mock で完成させる。契約逸脱を mock で検出できる状態にする。
- core 公開 OpenAPI を vendoring、
openapi_parser+WebMock で契約テスト土台を用意 Core::ApiClient実装(ApiKey 認証・Idempotency-Key・outbox リトライ)LivekitRoomを共通 IF で実装(cache 即返し+ API on demand +障害時デグレード)- Webhook receiver +同期ハンドラ 4 種(録画 full sync/入室/文字起こし保存/加工完了化)
search_textの 2 系統マージ(B-DEC-09)と録画削除フローのコピー行同時削除(B-DEC-07)- 入室 BFF に認可検証(
room_access_token照合 or セッション+Ability・B-DEC-08) - 🧩 Webhook 冪等化(event id 重複排除)・削除伝達の outbox 化・権威表の定数化+是正ロジック
- 🧩 退会 → erasure 連携 client(core 契約に入り次第)
Slot#interview_roomの engine 分岐を有効化
受け入れ:mock 相手に「作成 → 入室 URL → 読み取り → Webhook 同期」が契約テストで緑。
mock を実 core へ差し替え、二重運用を経て完全移行する。core 稼働でアンブロックされる唯一のフェーズ。
- mock → 実 core へ差し替え、1 案件を新エンジンで e2e(作成→配布→入室→録画→振り返り→事後処理)
- 二重運用投入:opentok / livekit の案件割り当てルールを確定
- 🧩 突合ジョブ投入(grace period 24h・検知のみ → 乖離ゼロ確認後に自動是正 ON)
- 🧩 erasure e2e・整合性メトリクス計装(乖離検知・自動是正件数)
- 完全移行:物理 FK 廃止、legacy
Room/OtSession/OtArchiveを read-only 化 - 過去データ(録画・チャット・入退室)の core 移送 or read-only 残置の範囲を確定
依存関係
Phase 0 (seam) ──┐
├─► Phase 2 (contract-first, mock) ──► Phase 3 (core 結合) ★core稼働待ち
Phase 1 (schema)─┘
- Phase 0 / 1 は core 非依存で先行完了できる。
- Phase 2 は core の OpenAPI 公開が着手条件=稼働サービスは不要だが契約公開は必要(minedia-www は pure consumer)。
- Phase 3 のみ core 稼働がブロッカー。
付録 — 確定 14 論点(B-DEC-01〜14)
現行画面の実査(Operator 実ログイン・Employee 管理画面・ダッシュボード / 即席フォルダ)で確定した設計分岐の一覧。根拠と詳細は本文の各セクションに移したため、ここは一行の決定記録のみ。
| B-DEC-02 | 事後処理の繋ぎ直しは物理 FK を廃止し recording_uuid(文字列参照)へ | → ① |
| B-DEC-11 | 録画メタは集約列でなく録画 1 行=1 レコードの recording_refs(月次統計が集約列では割れない) | → ① |
| B-DEC-12 | recording_refs の書き込みは Webhook payload を信用せず read-back の room 単位 full sync+日次リコンサイル | → ① |
| B-DEC-13 | ダッシュボード「直近アウトプット」は縮退を受容:完了 / 失敗のみ表示(進行中は再現不可) | → ① |
| B-DEC-14 | 即席フォルダの Room 数バッジは W1 契機の extemporary_sessions.first_entered_at IS NOT NULL カウント(「実際に使われた部屋数」を踏襲) | → ① |
| B-DEC-06 | ジョブ横断検索は Webhook 契機で本文を取得し interview_transcripts にローカル保存(core に検索 API は足さない・全 kind 保存) | → ① |
| B-DEC-07 | 本文コピーの消去は www の録画削除フロー内で同時削除(削除系 Webhook は新設しない) | → ① |
| B-DEC-09 | 並行運用期はアプリ層で旧・新 2 系統を検索し id をマージ(dual-write なし)。完全移行時にバックフィルで一本化 | → ① |
| B-DEC-01 | 二重運用は統一 IF Slot#interview_room(OpentokRoom / LivekitRoom アダプタ)で吸収 | → ① |
| B-DEC-03 | 読み取りは混合:詳細=core API on demand / 一覧・集計の hot 列だけローカルキャッシュ | → ① |
| B-DEC-04 | 振り返り管理画面は www に残し core API 取得。二重運用期も同一 UI で両エンジンを見る | → ① |
| B-DEC-10 | チャット CSV は www 側 BFF が core 履歴 API から生成(core に CSV API は追加しない)。列・TZ・Excel 書式・ファイル名は現行踏襲 | → ① |
| B-DEC-05 | 権威の分界:Room 解放 / 入室可否 / 録画=core、オファー / 確定 / 謝礼 / 出欠=調査。Webhook で双方向同期 | → ② |
| B-DEC-08 | 入室 BFF は取次ぎ前の本人検証を必須化(room_access_token 照合 or セッション+Ability)。推測可能な id のみによる発行を構造的に禁止 | → ② |
本ページは設計ドキュメントであり、本番コード(app/, db/)は変更しない。GitHub Issue #5317。