minedia-www 変更点
インタビュールーム(WebRTC機能)を新基盤(core)へ切り出した後、minedia-www(調査ドメイン)側で何がどう変わるかの設計方針と実装フロー。GitHub Issue #5317。
全体像 — 何が core へ移り、何が残るか
core へ移るもの
WebRTC 接続・録画・文字起こし・ルーム内ログは新基盤が単一の事実根拠(source of truth)を持つ。minedia-www は uuid 参照と REST API / Webhook で受ける。
minedia-www に残るもの
調査業務ロジックと、オペレーター/従業員向けの管理画面(振り返り)は残す。録画・ログは core API から取得して同一 UI で描画する。
設計方針案 — 5つの論点
下表は現時点の方針案であり、最終決定は明日の協議で行う。(2026-06-19 の /dig セッションで Operator 管理画面・コードを実査して導いた叩き台)
「Operator として実ログインし、Room/録画をどう読んでいるか」を実査したうえで、5つの設計分岐の方針案をまとめた。これらは互いに整合し、「外部化 Slot ごとの薄いローカル参照モデル」へ一意に収束する想定。
| ID | 論点 | 方針案 |
|---|---|---|
| DEC-01 | 二重運用期(OpenTok 既存+新基盤)の既存コード呼び出し | 統一インターフェースで吸収。Slot#interview_room が OpentokRoom か LivekitRoom アダプタを返し、呼び出し側をエンジン非依存にする |
| DEC-02 | 事後処理(要約/分析/閲覧認可)の Recording への繋ぎ直し | recording_uuid(文字列参照)へ。物理 FK belongs_to :ot_archive を廃止 |
| DEC-03 | UI/ジョブの読み取り経路 | 混合:詳細=core API on demand / 一覧・集計の hot 列だけローカル非正規化キャッシュ |
| DEC-04 | 振り返り管理画面(チャット履歴/入退室/録画一覧/再生/発言録)の所在 | minedia-www に残し core API 取得。二重運用期も同一 UI で両エンジンを見る |
| DEC-05 | 状態同期の権威 | 役割分担:Room 解放/入室可否/録画=core、オファー/確定/謝礼/出欠=調査。Webhook で双方向同期 |
アンカーモデル — 薄いローカル参照モデル InterviewRoomRef
5つの方針案は 外部化 Slot ごとの薄いローカル参照モデルに収束する。これが「統一IF のアダプタ先」「一覧用キャッシュ」「詳細 API 用 uuid 保持先」「Webhook 更新先」を兼ねる。新テーブル interview_room_refs(仮称・別名 LivekitRoom)を追加する。
索引:unique [slot_id] / unique [extemporary_slot_id] / unique [core_session_uuid]。OpenTok legacy 案件はこの行を持たず slot.room で従来どおり。新エンジン案件のみ1行持つ。エンジン選択は projects.webrtc_engine(opentok/livekit・default opentok)で持ち、Slot が継承する。
なぜ「薄い参照モデル」か
- Slot に列直付けだけでは、詳細 API キャッシュ・録画 uuid 配列・同期鮮度の置き場が散らかる。1:1 付帯テーブルに隔離するほうが移行の継ぎ目が明確。
- core の完全ローカルミラーは「事実は core が正、調査は uuid 参照」(DEC-02)と矛盾し整合性責務が重い。採らない。
- 薄い参照モデルは「事実は core が正」を保ちつつ一覧性能だけローカルで担保する最小の方法。
録画・事後処理の繋ぎ直し(DEC-02)
現状、録画後加工の各モデルが belongs_to :ot_archive(物理 FK)。外部化後は recording_uuid 参照へ。
| モデル | 現状 | To-Be |
|---|---|---|
TranscriptionRequest | belongs_to :ot_archive | core へ移管。調査側からは廃止。必要なら transcription_uuid 参照 |
StatementSummaryRequest(要約) | belongs_to :ot_archive | recording_uuid + transcription_uuid を保持。自前 slot_id/project_id も保持(local JOIN 用) |
ProcessedOtArchive | belongs_to :ot_archive | recording_uuid 保持 |
OtArchiveSummary | belongs_to :ot_archive | recording_uuid 保持 |
OtArchiveAccess(録画閲覧認可) | belongs_to :ot_archive | recording_uuid。認可主体は 未決(DEC-08) |
動画再生URL:ot_archive.video_s3_key → 新エンジンは core の GET /recordings/{id}/download-url(短命の署名S3 URL)を取得。旧 OpenTok 案件は ot_archive のまま(二重運用)。
統一インターフェース(DEC-01)と読み取り書き換え
Slot#interview_room がエンジンに応じてアダプタを返し、呼び出し側を非依存にする。
Slot#interview_room
├─ engine == opentok → OpentokRoom.new(room) # 既存ローカル Room をラップ・無変更
└─ engine == livekit → LivekitRoom.new(interview_room_ref, core_api_client)
両アダプタが共通インターフェースに応答する:
#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 — 一覧・集計は interview_room_refs の cache 列で即返し、詳細はその場で core API を叩く(on demand)。API 障害時はデグレード値を返す。
class LivekitRoom
def initialize(ref, client)
@ref, @client = ref, client
end
# 一覧・集計:API を叩かず cache 列で即返し
def movie_duration_sec = @ref.cached_total_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:) = @client.post("/room-access-tokens", room: @ref.core_room_uuid, role:)
end
主な読み取り書き換え
| 箇所 | 現状 | To-Be |
|---|---|---|
| 振り返り画面 (B) | @room.room_chat_messages 等を直接 | @interview_room.chat_messages(usage:)。Livekit は core API on demand |
| ダッシュボード集計 (C) | joins(ot_archive→ot_session→room→slot) | Room ホップ除去:local slot_id/project_id で JOIN、duration/status は cached_* |
| 一覧 N+1 prefetch (C) | includes(room: :ot_archives) | includes(:interview_room_ref) でキャッシュ列を読む |
| 録画時間集計 (C) | joins(room: :ot_archives).sum | cached_total_duration_sec を集計 |
API on demand の劣化設計:詳細 (B) で core API が落ちている場合、チャット/録画セクションのみデグレード表示(「一時的に取得できません」+ uuid 表示)。ページ全体は調査データで描画を継続する。
同期 — Webhook 役割分担(DEC-05)
状態を2軸に分け、衝突しないよう権威を分界する。
| 軸 | 権威 | 同期方向 |
|---|---|---|
| Room 解放 / 入室可否 / 締切 / 録画 | core(Session.status) | core → 調査へ Webhook(事実通知) |
| オファー / 確定 / 出欠 / 謝礼 | 調査(AnswerSlot.status) | 調査内で完結。退室イベントは core → 調査がトリガー |
連携インターフェース
| 連携点 | 方向 | minedia-www 側の実装 |
|---|---|---|
| Session/Room 作成 | 調査 → core(命令) | Slot 作成後に POST /sessions。失敗は outbox + リトライ |
| 入室トークン発行 | 調査 → core(命令) | 配布/入室時に POST /room-access-tokens で participant JWT 払い出し |
| 緊急発行(force の後継) | オペ → core(命令) | generate_force_access_token を core 緊急発行 API に置換 |
| キャンセル → Room 解放 | 調査 → core(命令) | AnswerSlot キャンセル時に DELETE /sessions/{id} |
録画完了 recording.status_changed | core → 調査(Webhook) | recording_uuid/duration/status を cached_* と事後処理へ反映 |
退室 participant.left | core → 調査(Webhook) | 謝礼/コメント表示トリガー・出欠 |
session.ended | core → 調査(Webhook) | cached_session_status 更新 |
文字起こし完了 transcription.completed | core → 調査(Webhook) | 要約/集計ジョブのトリガー |
障害・整合性:作成時 core ダウン → Slot は保存成功させ Session 作成は outbox で非同期リトライ(入室URLは「準備中」表示・調査業務は止めない)。Webhook 欠落 → 定期 polling fallback でリコンサイル(last_synced_at で鮮度監視)。cached_* は一覧用ヒントに留め、課金根拠は常に core を正として詳細時に再取得。
実装フロー
主軸は 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(二股FK・uuid群・cached列・unique 索引)+ モデル/関連定義 - migration: 事後処理4モデルへ
recording_uuid/ 自前slot_id/project_idを追加 - 物理FK
belongs_to :ot_archiveを nullable 化(廃止は Phase 3) - (C) のローカルJOIN / cache 集計経路を engine 分岐で用意(ダッシュボード・一覧・録画時間)
受け入れ: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 +同期ハンドラ(録画完了/退室/
session.ended/文字起こし完了 →cached_*・事後処理) Slot#interview_roomの engine 分岐を有効化(livekit 案件はLivekitRoomを返す)
受け入れ:mock 相手に「作成→入室URL→(B)読み取り→Webhook 同期」が契約テストで緑。
mock を実 core へ差し替え、二重運用を経て完全移行する。core 稼働でアンブロックされる唯一のフェーズ。
- mock → 実 core へ差し替え、1案件を新エンジンで e2e(作成→配布→入室→録画→振り返り→事後処理)
- 二重運用投入:どの案件を opentok/livekit で回すかの運用ルールを確定
- 完全移行:物理FK
belongs_to :ot_archiveを廃止、legacyRoom/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 公開が着手条件(gating)=core の稼働サービスは不要だが契約公開は必要(minedia-www は pure consumer)。
- Phase 3 のみ core 稼働がブロッカー。
本ページは設計ドキュメントであり、本番コード(app/, db/)は変更しない。GitHub Issue #5317。