Skip to content

コード全体評価・リファクタリング報告(2026-09) ​

実施日: 2026-09-04 / 対象: v6.2.1 時点の main(760fe55) / ブランチ: claude/code-refactor-evaluation-1otcto 目的: 「バグが起き得ないか」の観点でコード全体を評価し、確度の高いものは修正、残りは根拠付きで一覧化する。

このドキュメントは Code Responsibility Audit (2026-07) の後続にあたる。前回指摘のうち createProfile の型ドリフト / resolveImageHref 重複 / ProcessManager デッドコード / 勝率計算の共通化(src/utils/winRate.ts)/ normalizePlayerName 共通化 / timeUtils 新設 などは既に解消済みであることを確認した。本報告はその上で残っていた問題と、 新たに見つかった問題を扱う。


1. 進め方 ​

  1. ベースライン確認: tsc --noEmit(renderer / electron 両 tsconfig)・eslint --max-warnings 0・vitest(310 ファイル)が全て green であることを確認。
  2. サブシステム別レビュー: DB 層 / Recorder + 戦績 API + ブラウザ自動化 / Electron main・IPC・preload・windows / その他 Electron サービス / ログ解析コア / store・hooks・utils・screens / React タブ UI の 7 領域に分けて精読し、「file:line + 失敗シナリオ」が示せるものだけを採用。
  3. 横断的な機械チェック: ipcRenderer.invoke チャネル ⇔ ipcMain.handle 登録の突合、localStorage キーの書き手/読み手突合、 同名ヘルパの多重定義(grep ベース)。
  4. 確度 HIGH かつ影響が明確なものを修正し、テストを追加。判断が分かれるもの・影響範囲が広いものは §4 に残した。

2. 修正した不具合(確度 HIGH) ​

「症状」はユーザーから見える現象、「原因」はコード上の事実。すべて本 PR に修正とテストを含む。

2.1 Renderer / 設定まわり ​

#症状原因修正
R-1リアルタイム解析タブの「Starward 自動反映」を OFF にしても、BP のたびにオーバーレイのデッキ画像が上書きされるuseAutoDeckApply が旧キー logAnalysis_deckAutoApplyEnabled を読み、トグルは realtimeAnalysis.deckAutoApplyEnabled を書いていた(書き手ゼロの旧キーを参照)キーを src/constants/storageKeys.ts に定数化し両側で共有。旧キーは読み取りフォールバックのみ
R-2リアルタイム解析タブの「リセット」ボタンが何もしないmain には log-watcher-reset ハンドラがあるのに preload が db.logWatcherReset を露出しておらず、renderer のガードで常に no-oppreload / 型 / mock bridge に追加
R-3デバッグタブの autoStartBrowser が常に「未設定」migrateBrowserAutomationKeys が削除する旧キーを表示していた移行先 battle.autoOpenLoginOnRequired を表示
R-40 時をまたいだ試合の試合時間が DB で nullLogWatcherService / startupImport にあった private コピーの calculateDurationSeconds は 0 時跨ぎ補正・範囲検証を持たず、timeUtils の共有実装だけが正しかった共有実装に統一(extractDateFromLogFileName も集約)

| R-5 | ログ解析タブでログを読み込むと、以後ずっと再レンダリングが止まらない(CPU 100%) | useStatisticsComputation の effect が毎レンダー新しい配列を受け取り、setStatistics → 再レンダー → … の無限ループになっていた | 配列を useMemo 化 | | R-6 | ブラウザ自動化タブの「テスト実行」「全試合取得」が、開発者の UID (1046101022) で他人の戦績を取得して自分の DB に混ぜる | localStorage.myUid(UI から書かれないキー)が無いときリテラルにフォールバックしていた | ユーザー設定から解決する resolveMyUid に統一し、未設定なら実行しない | | R-7 | 起動時インポートの進捗が「undefined / undefined」のまま | preload は (event, payload) で呼ぶのに args[0] を payload として読んでいた | 共通 extractIpcPayload に統一 | | R-8 | 未ログイン時にモーダルが 2 枚重なる | ControlPanel 側とライブ監視タブ側が同じイベントで別々のダイアログを開いていた | ControlPanel 側の 1 枚に集約 | | R-9 | ライブ監視とブラウザ自動化の両タブを開いていると、試合終了時に取得ワークフローが 2 回走る | 両タブが同じ IPC を購読していた(タブは keep-alive で常駐) | 購読を 1 箇所に集約 | | R-10 | ライブ監視タブを初めて開くと、ログ解析タブのガイドが重なって出る | タブ並び替え後もイベント購読が旧 index のままだった | TAB_INDEX 定数と共通フック useGuideOpenSignal に統一 | | R-11 | オンボーディング完了ボタンのラベルと遷移先が逆(viewer は無効化されたタブへ飛ぶ) | 完了時の分岐が反転していた | ラベルどおりの遷移先に修正 | | R-12 | プロファイルを素早く切り替えると、古い読み込み結果が新しい表示を上書きする | 5 箇所の非同期読み込みに順序トークンが無かった | 最新要求以外の結果を破棄 | | R-13 | 「初期レイアウトにリセット」で勝敗カウント・録画フィルタ・テーマ・アナウンス文まで初期化される | レイアウト以外も含む全既定値を流し込んでいた | レイアウト系キーのみに限定 | | R-14 | 非表示中のウィジェットが、編集モードを ON にすると座標 (0,0) で保存される | 非表示要素の測定値 (null → 0,0) を無条件に書き込んでいた | 当該処理を廃止(現在は座標がストア由来で測定不要) | | R-15 | renderer の Logger が dev でも一切出力されない(Logger.error 以外) | process.env に依存し、context isolation 下の renderer では常に本番扱いだった | Vite の import.meta.env.DEV を優先 | | R-16 | 編集モードで枠(GameFrame)を掴んで動かせない | pointerEvents: 'none' が常時適用されていた | 編集モード時のみ有効化 | | R-17 | キャラ画像で「イーネ」に別キャラ (rhine)、「ベータ」に longinus-beta の画像が出る | 部分一致検索がフォルダ単位で完全一致より先に走っていた | 全フォルダで完全一致を優先 | | R-18 | Normal 戦(BP なし)のキャラがほぼ取得できない | アセット名からの逆引きのみで、ROLE <n> の n がそのままキャラ ID である事実を使っていなかった | ID を直接利用し、名前解決はフォールバックに | | R-19 | 同じログをログ解析タブから保存すると、起動時インポート分と別 ID で二重登録される | 変換経路ごとに試合 ID の生成規則が違った | buildLogMatchId に統一 | | R-20 | 日付をまたいだ試合(0 時のログ分割)で BP データが失われ、種別も不正になる | 起動時インポートがファイルごとに新しいパーサを作っていた | ディレクトリ全体を 1 つのパーサに通す |

2.2 Recorder / 戦績 API 連携 ​

#症状原因修正
B-1同じ対戦カードで別の試合を戦うと、過去試合の kill/death/damage/MVP が新しい試合の値で上書きされ、試合終了のたびに繰り返されるfindMatchByOpponentUids が battle_id 済みの試合や日付の離れた試合も候補にし、1 件なら無条件に返していた(name フォールバックだけが ±5 分窓と未マージ条件を持っていた)未マージ行のみ・15 分窓・最近接に限定し、外れたら name フォールバック / api-only 登録へ
B-2Recorder の「直近 N 戦の勝敗」通知・トレイ表示が一度も出ないplayer: 疑似 UID だけで自分を判定しており、BP 付き Rank 戦(数値 UID)では常に '?'findMyPlayer(UID → myUids → player: → 名前)を新設して使用。単体テスト追加
B-3ログイン後にブラウザを閉じると、次の要ログイン時にログイン窓が開かない(通知だけ出る)disconnected 後も stale な BrowserAutomationService を保持し、navigateToUrl の失敗を握りつぶしていたisRunning() を見て破棄・再起動。navigate 失敗時も再起動
B-4常駐中にシーズンが切り替わると旧 season_code で取得し続けるSeasonResolver の cache が無期限now が全範囲外かつ 5 分以上古い cache は 1 回再取得
B-5Recorder 停止中に 1 ページ超の試合が溜まると、差分バックフィルで page 2 以降の穴が永久に埋まらない試合終了 WF が page 1 を先に保存 → 差分モードは page 1 の「新規ゼロ」で終了終了判定を page 2 以降に限定(テスト追加)
B-6同じ logDir を指す複数プロファイルで、試合が 2 回処理される(通知 2 回・カウント +2・ワークフロー二重起動)watcher をディレクトリごとに重複生成正規化して重複除去
B-7CLI / WS / 起動時オプションからバックフィルが同時に走ると API 二重呼び出し・listCount ずれ再入ガードなしbackfillProgress.running / fullSyncInFlight で直列化
B-8CLI / WS 起点の取得で 401 になっても何も起きないreason:'要ログイン' を返すだけで呼び出し側が捨てていた試合終了 WF と同じくログイン窓を開く(60 秒クールダウン)
B-9Manager / Viewer 起動プロセスの 'error'(実行不可等)が uncaughtException → app.crash テレメトリ + ダイアログ子プロセスの 'error' listener なしlistener 追加(テストの mock も更新)
B-10Recorder 終了時に直前のログイン cookie / token が保存されないことがあるstop() を待たずに app.quit()完了(上限 3 秒)を待ってから quit
B-11browser-automation-start が reject を返し renderer 側で通知できない兄弟ハンドラと違い try/catch なし{ success:false, error } に統一

2.3 Electron main / ウィンドウ / IPC ​

#症状原因修正
M-1プロファイル設定の「フォルダ選択」でパス欄が [object Object] になり、キャンセルしても選択扱いになる。監視開始も d.trim is not a function で失敗open-directory ハンドラだけが { success, path } を返し、preload 型・renderer ラッパー・全呼び出し側は string | null を前提にしていたハンドラを string | null に統一。ラッパーも文字列以外を通さない
M-2オーバーレイを非表示にした状態でサイズを変更すると、次回起動以降オーバーレイを二度と表示できない非表示中の座標 (x = -9999) がそのまま保存され、復元位置も画面外になっていた画面外座標は保存しない。復元時も画面外・全ディスプレイ外の保存位置は無視
M-3Electron オーバーレイの「直近 N 戦」「本日 MVP」「フェーズ表示」が試合終了後も更新されないライブイベントが Control ウィンドウにしか転送されていなかった(OBS ブラウザソースは WS 経由なので無事)オーバーレイ窓にも配信
M-4Viewer で戦績ウィンドウを閉じるとトレイ常駐ごと終了するwindow-all-closed が全モードで app.quit() していたトレイ常駐モードでは終了処理をスキップ
M-5アップデートの「今すぐ再起動」でインストーラだけ起動しアプリが終了しないトレイ最小化の close 抑制が app.quit() 経路も潰していたbefore-quit で明示終了扱いにする
M-6トレイに最小化した状態で二重起動しても画面が出てこないrestore() + focus() だけで show() していなかった非表示なら show() してから focus
M-7BP ウィンドウの表示切替後、BAN/PICK が更新されなくなる閉じた旧ウィンドウの closed が非同期で発火し、新しいウィンドウの参照を null にしていた(4 ウィンドウクラス共通)インスタンス同一性チェック
M-8ログのリプレイ(デバッグ)が「開始しました」と出るのに何も起きない。resume を繰り返すとログ注入が多重化debug-replay-log が実行状態にせず、ポーリング中のループを止めずに新ループを開始していた開始処理を共通化し、既存タイマーを解除
M-9正常終了なのに app.crash テレメトリが記録されるログ stream を閉じた後に DB 終了ログを書いていた(write-after-end)DB close → log close の順に変更し、終了済み stream への書き込みをガード
M-10モニタを外すと BP ウィンドウが画面外に表示され、掴んで戻せない保存 bounds を検証せず復元していたisOnScreen 判定を共通化して既定位置へフォールバック
M-11試合終了通知が常に「Result: Unknown」自分の判定が player: 疑似 UID 前提だったfindMyPlayer に統一

2.4 Electron サービス層 ​

#症状原因修正
S-1任意の Web ページから Recorder のライブ WebSocket に接続でき、戦績データの閲覧や resident:enable / login 等のコマンド送信が可能だったLiveEventWsServer に Origin 検証が無い(ObsOverlayServer にはある)判定を共通化し verifyClient で拒否。Origin 無しの Node クライアントは従来どおり許可
S-21 日 20 MB を超えた日のテレメトリがサーバへ一切送信されないローテーション後の -2 が辞書順で本体より前に並び、カーソルが到達しなかった日付 + 連番の比較関数に統一。終了時 flush と定期 tick の二重送信も防止
S-3ログが無制限に増え続ける(14 日保持が効かない)JsonlWriter.cleanup() がどこからも呼ばれていなかった初期化時 + 24 時間ごとに実行
S-4クラッシュ後に UID・OBS ポート等の設定が消える設定ファイルを直接上書きしており、書き込み途中で壊れると次回 {} として読み、次の保存で確定していた.tmp → rename の原子的保存 + .bak フォールバック
S-5ウイルス対策等でログファイルが一時的にロックされると app.crash テレメトリが出て fd も漏れるopenSync / readSync が未ガードtry/catch + finally で close。日付フォールバックもローカル日付へ
S-6デバッグタブのログイン確認サマリが常に「成功率 0.0% / 平均 0 ms」集計側が emitter の書かないプロパティ名を読んでいた実際のキー (loggedIn / durationMs) を読む
S-7OBS が固まっていると Manager の終了・ポート変更が最大 30 秒ブロックgraceful close の待ちterminate()
S-8Recorder 起動時に Manager 側の接続が一度失敗すると、以後ライブ表示が復活しないhello 遅延 1 回で永久停止していた再接続へ
S-9カスタムフォルダにインストールした環境で自動更新がダウンロードされないportable 判定が 2 系統混在yml ベースの判定に統一

2.5 DB / 集計 ​

#症状原因修正
D-1対面分析の「相手の所持キャラ」に、2v2 の味方の所持キャラが混ざるgetOpponentAvailableCharacterStats の JOIN に team 条件が無く、uid != 自分 だけで相手としていた味方を除外(getOpponentStats と同じ規約に統一)
D-2同じ対戦カードの api-only 試合が 2 件あると、同一ログ試合へ両方マージされ片方の戦績が消えるreconcile に排他が無く、同点時の優先順位も「ログ終了時刻 < API 開始時刻」を考慮していなかったclaimed セットで排他し、時刻の前後関係を優先
D-3ON DELETE CASCADE 導入前に作られた DB で、試合削除・reconcile が FOREIGN KEY constraint failed で失敗するCREATE TABLE IF NOT EXISTS は既存テーブルの FK 定義を更新できない子行を明示削除してから親を消す(トランザクション化)
D-4勝率推移の期間下限が日付境界で 1 日ずれるSQLite の date('now')(UTC)をローカル日付の列と比較していたJS 側でローカル日付を計算して束縛
D-5API 由来の自分の行が名前空欄のまま残るbattle_detail マージの UPDATE が name を埋めていなかった空のときだけ API 名で補完

2.6 試合種別(Rank / Casual)判定の是正 — 影響が最も広い変更 ​

実ログ 12 ファイル・342 試合で検証したところ、BP データが揃っているのに Normal_2v2 として記録されていた試合が 142 件(42%) あった。原因は 2 つ:

  1. TYPE: マーカーを一度も見なかった試合に、パーサが無条件で 'Normal_2v2' を割り当てていた(ゲームが種別行を出さないセッションが実在する)。
  2. 直前の試合が「結果行なしで終了」した場合の後始末で、次の試合用に読み込み済みの TYPE: を捨てていた。

さらに TYPE:None(チーム解散時のノイズ行)が種別 'None' として記録され、2v2 の集計から丸ごと消えていた。

修正後の方針:

  • 明示された TYPE:(Rank_* / Normal_* / League_* / Custom / Ai_*)は常に尊重する
  • TYPE:None は種別として扱わない
  • TYPE: 未検出なら人数から Unknown_1v1 / Unknown_2v2 とし、BP の有無から Rank を推測しない(現在のゲームは Casual でも BP ログを出すため、旧ロジックは Casual を Rank として記録していた)
  • BP 中断 → 再マッチング(BattleID 変更 / 5 人目の出現)を検知して、古い BAN/PICK を次の試合に持ち越さない

Unknown_* は Rank 集計には入らないが 2v2 の表示・集計には従来どおり乗る。**「Rank と断定できない試合を Normal と断定しない」**のが本修正の要点で、Rank 戦績の精度を上げるには §4 の O-1(API 由来 battle_type からの種別復元)とセットで進めるのが望ましい。

3. 実施したリファクタリング(動作は同一、テストで担保) ​

#内容効果
X-1extractIpcPayload を src/utils/electronAPI/ipcPayload.ts に集約(useAutoDeckApply / useLogWatcherIPC のコピペ解消。後続で useStartupImport / LogSimulator / bp-opponent / recorder-stats 各 hook も同ヘルパへ)preload の (event, payload) 形を 1 箇所で扱う
X-2formatDuration を src/utils/formatters.ts に一本化(四捨五入・負数 0 丸め・padMinutes)3 実装 → 1。"1:5.5" のような壊れた表示も防止
X-3WinDot / LoseDot を tabs/common/components/ResultDot.tsx へ2 実装 → 1
X-4勝率文字列生成を store の calculateWinRate に統一(WinRatePanel / useWinRateRecalc)片側だけ変更されるとオーバーレイと管理画面で表示がずれる問題を構造的に防止
X-5STEAM_LAUNCH_FLAG / AUTO_START_FLAG / RECORDER_EXE_NAME を launchMode.ts / launchRecorderExe.ts の export に統一3 箇所の文字列リテラル重複を解消
X-6ログ時刻ヘルパ(parseLogClockToMilliseconds / calculateDurationSeconds / extractDateFromLogFileName)を timeUtils.ts に集約LogWatcherService / startupImport の private コピーを削除(= R-4 の修正そのもの)
X-7ブラウザ経由の戦績取得ワークフロー(約 1,900 行)を削除。workflowOrchestrator.ts / detailPageUrl.ts / summarizeProbeData.ts / probeAuthEndpoint / isLoggedInProbeResponse と BrowserAutomationService の 5 ファサードメソッド、形骸化した shouldTryDirect / shouldFallbackToBrowserdirect API 一本化(76779c4)以降到達不能だったコード。ブラウザはログイン専用に限定される
X-8到達不能な IPC ハンドラ db-save-battle-list-items / db-save-season-summary / db-save-season-show を削除preload から一度も invoke されていなかった
X-9死んだモジュール src/utils/configSync.ts(importer ゼロ)と空ファイル PositionAdjustmentView.tsx / SizeAdjustmentView.tsx を削除—
X-10.tmp-logs/(vitest 実行時に electron/logger.ts が書く退避先)を追跡対象から外し gitignoreテストのたびに作業ツリーが汚れていた

| X-11 | ホームパスのマスク正規表現を electron/utils/maskHomePath.ts に共通化(診断バンドル / テレメトリの重複を解消) | 2 実装 → 1 | | X-12 | ライブ WS と OBS サーバの Origin/Host 判定を liveEvents/localOrigin.ts に共通化 | S-1 の修正に必要だった共通化 | | X-13 | ガイド起動シグナルの effect(3 タブに重複)を useGuideOpenSignal(tabIndex) に集約 | 3 実装 → 1。R-10 の再発防止 | | X-14 | UID 解決を src/utils/resolveMyUid.ts に集約(4 種類の異なる実装があった) | R-6 の再発防止 | | X-15 | 試合 ID 生成を src/utils/logAnalyzer/matchId.ts に集約 | R-19 の再発防止 | | X-16 | RecorderStatsWindow の画面内判定を WindowStateStore.isOnScreen へ移し、BP / オーバーレイ窓でも利用 | 3 窓で同じ復元不具合を防ぐ | | X-17 | formatChangelogDate を src/utils/formatters.ts へ移動(コンポーネントからの関数 export を解消) | 日付整形の置き場を一本化 | | X-18 | 到達不能な IPC db-save-matches と preload の saveMatches を削除(再保存で API 由来の戦績列を消す実装だった) | IPC 面の縮小 | | X-19 | onConfigUpdated / onOverlaySizeChanged が解除関数を返すようにし、購読の積み上がりを解消 | listener リーク防止 |

4. 未対応の指摘(判断が必要 / 影響範囲が広い) ​

修正せず残したもの。いずれも「再現条件」と「推奨対応」を添える。深刻度は 🔴 高 / 🟠 中 / 🟡 低。

4.1 仕様判断が必要 ​

#深刻度内容推奨対応
O-1🟠API 由来試合の type が '2v2' 固定で Rank 判定から外れる。battleTypeMap.ts は 21/22 を '2v2' に潰すが、同系 API の roleListBattleType.ts は「一の位 2=ランク / 1=カジュアル」と定義しており、battle_list.json サンプルも 22 のみ。Recorder のみで記録した試合(ログ無し)は getLatestRankByUid / getSeasonRankStatsByUid / rankOnly 表示の対象外になる実データで 21 = カジュアルを 1 件確認したうえで 22 → 'Rank_2v2' / 21 → 'Normal_2v2'(1v1 も同様)へ変更。変更後は既存行の再ラベルも検討
O-2🟡TYPE:Ai_2v2(対 AI 戦)がカジュアル 2v2 として記録・集計される除外するなら isRecordableForRealtime / matchesMatchTypeGroup で Ai_* を弾く
O-3🟠checkLoginStatus の storage-state フォールバックが「token 失効 = 401」のケースでちょうど loggedIn:true を返す(8 秒以内に認証 API 応答が無く、storageState ファイルが存在する場合)。状態表示が「ログイン済み」になり、失効 token が login-confirmed として再保存されるフォールバックを loggedIn:false にするか、readCredentialsFromFile() で token が取れて かつ direct probe(loginValidation)が成功した場合のみ true にする。Manager 側の「未ログイン」表示への影響を確認してから
O-4🟡Recorder の headless 設定(CLI / UserSettings / WS ba:headless:*)は保存されるだけで、ログイン窓は常に headed設定を削除するか Manager 専用と明記

4.2 設計変更を伴うもの ​

#深刻度内容推奨対応
O-5🟠LogWatcherService の単一ファイル位置モデル。候補 .txt が 2 つ以上あると(前日ファイルの最終 flush と当日ファイル、または無関係な .txt)ファイルを切り替えるたびに EOF へ飛び、その間の行を取りこぼすファイルごとに {lastSize, pendingChunk} を持つ Map にし、ready 後に初めて見たファイルは 0 から読む。候補判定を Log_*.txt に絞るだけでも影響を縮小できる
O-6🟠同期モデルが「全状態の後勝ち」(broadcastSet は毎回 state 全体を送る)。get-config の古いスナップショットで新しいローカル変更が巻き戻る競合(今回は最小限のガードのみ)や、オーバーレイ側が起動時に書き手になる問題の根本原因changedKeys だけを送るキー単位マージにする(ObsOverlayServer の replay 用スナップショットはサーバ側でマージ保持)
O-7🟠終了処理が window-all-closed に紐づいている。Manager では app.quit() と競合して末尾(telemetry flush / DB close)が走らないことがあり、Recorder / Viewer(ウィンドウ無し)では一度も走らない。※ 本 PR では close 順序(M-9)と Viewer の即終了(M-4)のみ是正し、構造は据え置きbefore-quit で一度 preventDefault → cleanup を await → app.exit() に移す
O-8🟠試合終了ワークフローの renderer 側二重実装(useBrowserAutomation と useBrowserAutomationSession)と、browser-automation-status を 4 箇所が独立にポーリング。※ 二重購読による「ワークフローが 2 回走る」不具合(R-9)は本 PR で解消済みだが、フック自体の重複とポーラ 4 本は残っているControlPanel 直下の単一 owner hook(runMatchEnd/runFull/runTest + 1 本の status poller)に統合
O-9🟡3 つの MatchData→DB 変換(LogWatcherService.convertToDbFormat / startupImport / useMatchConverter)で team のフォールバック規則が異なる(Win→A vs idx<2→A)id は本 PR で統一。team も src/utils/logAnalyzer の共通関数へ
O-10🟡useDraggable が mousemove ごとに全状態を broadcast するmouseup で確定、または throttle
O-11🟡RecorderStateFile が 1 秒ごとに getRecentResults(最大 20 件の DB 照会)+ 設定ファイル 2 つを読むmatch-completed / match-detail-updated でキャッシュ無効化
O-12🟡Cloudflare Worker(telemetry-ingest)が gzip 本文を全展開してからサイズ検証する(gzip bomb で isolate の OOM)展開ストリームをバイト数で打ち切り 413
O-13🟡初回訪問時の画面ガイドが 3 タブ(試合一覧 / プレイヤー / ログ解析)で開かない。LazyTabPanel が hasMounted を useEffect で立てるため、タブ subtree のマウントは「アクティブになった次のコミット」になる。control-tab-selected はその前に発火するので、初めて開いたタブのリスナーは間に合わない(2 回目以降は「一度出したタブ」ガードで抑止される)。本 PR のタブ index 修正(R-10)で誤ったタブにガイドが出る問題は解消したが、この経路は残っているLazyTabPanel.tsx で useEffect をやめ、レンダー中に状態を進める React 公式パターンに変更する(検証済み):
const [hasMounted, setHasMounted] = useState(active);
if (active && !hasMounted) setHasMounted(true);
「訪問していないタブはマウントしない」という lazy mount の利点はそのまま。ただしタブ切替のクリック→描画に初回レンダーが含まれるようになるため、perf-baseline.json のタブ TTI で確認してから入れるのが望ましい

4.3 デッドコード・整理候補(動作影響なし) ​

  • getPlayerStats / getCharacterStats / getDashboardSummary / getWinRateTrend / getOpponentStats / getBanDebugInfo は DB → IPC → preload まで配線されているが renderer に消費者がいない(ダッシュボードはローカル集計)。削除するか配線し直す → 対応済(getWinRateTrend は勝率推移比較で使用中のため残し、他 5 つを削除)
  • MatchRepository.saveMatches は再保存時に API 由来の戦績列を消す挙動(players を DELETE → 基本列だけ INSERT)。IPC / preload 面は本 PR で削除済みだが、DB 層の関数自体は残っている(現在の呼び出し元は無い)→ 対応済(DB 層の関数も削除)
  • OverlayState の avatarX/Y/Size / cornerDecoration* / bottomRightText/FontSize は描画されない永続状態(同期・保存対象に残っている)。persist の version/migrate を導入して除去
  • scripts/log/analyze-log/parsers/LogParser.ts(792 行)は本体パーサと乖離した第 2 エンジンで、watch-log.ts --replay と scripts/debug/* だけが使う。src/utils/logAnalyzer に置き換えて削除
  • Aggregator.dedupeBans/dedupePicks/sortPlayers / LogParserCore.* / StreamFsm.canTransition はテストからしか呼ばれない
  • open-player-dashboard の window._openPlayerUid は書かれるだけで読まれない(試合詳細のプレイヤー名クリックがプロファイルを選ばない)→ 書き込みを削除済(タブ遷移は維持)

5. 維持すべき良い設計(レビューで確認できたもの) ​

  • DB 層: 単一 DatabaseConnection、リポジトリ間の循環依存なし、全 ALTER TABLE が PRAGMA table_info でガードされ二重起動で壊れない、複数行書き込みは全て db.transaction、返却行は AS 句 / 明示 map で camelCase 化済み。SQL はすべてパラメータ化
  • ログ解析コア: 正規表現は LineParser.ts に単一ソース、名前正規化は結合の両側に適用、BAN/PICK の dedupe キーは実データで一致、4 枠デッキ抽出は回帰フィクスチャで固定。batch / realtime とも StreamLogParserV2 一本
  • 同期: applyRemoteState の self-filter / 再入カウンタ / timestamp 破棄 / キー allowlist はテストで固定。WSAdapter の指数バックオフと停止時の再接続抑止も正しい
  • Electron 境界: preload に Node 漏れなし、イベント allowlist、全 webContents.send に isDestroyed() ガード、setWindowOpenHandler deny + will-navigate origin 検証、ブラウザ自動化 URL の allowlist は IPC 層とサービス層の二重防御
  • 起動: StartupSequencer のタスク単位 fault isolation、startup import の冪等性(既存 ID スキップ)、単一インスタンス制御(Manager = Electron lock / Recorder = process.lock)
  • DI: backfillWorkflow / MilestoneService / BattleRecordApiClient(FetchLike) は副作用を完全注入しており、今回の修正でもテストがそのまま書けた

6. 検証 ​

チェック結果
npx tsc --noEmit(renderer)エラー 0
npx tsc -p tsconfig.electron.json --noEmit(main)エラー 0
corepack yarn lint(--max-warnings 0)0 errors / 0 warnings
corepack yarn test:unit320 ファイル / 3,135 テスト すべて pass

修正には回帰テストを追加している。主なもの:

  • 試合照合の時刻窓と未マージ限定(BattleListRepository)
  • 0 時跨ぎのログ分割で BP データと種別が保持されること(startupImport.parseFiles)
  • レイアウトリセットで「レイアウトは戻り、勝敗・フィルタ・テーマは残る」ことの両方向
  • TYPE: 未検出時の Unknown_1v1 / Unknown_2v2 割り当てと、明示 TYPE の尊重
  • 対面分析で味方の所持キャラを数えないこと
  • テレメトリのローテーションファイル順とカーソル継続
  • 設定ファイルの原子的保存と .bak フォールバック
  • 旧スキーマ(CASCADE なし)での試合削除

動作確認について ​

本作業はディスプレイの無いヘッドレス環境で行ったため、yarn dev:electron による起動確認は実施していない。 UI に見える変更(ダイアログの重複解消 / オンボーディングの遷移先 / タブガイド / オーバーレイの勝率表示 / フォルダ選択)はマージ前に実機で一通り触ることを推奨する。