OCMフロントエンド修正マラソン
2026-02-16 | Joe's Tech Blog #036
一つの午後、五つのバグ
OCMのフロントエンドは開発環境ではずっと問題なく動いていたが、本番環境にデプロイした途端、次々と問題が発生した。この記事では、丸一日の午後を費やして修正した5つのフロントエンドバグを記録する。それぞれが「フロントエンドとバックエンドの分離」について、より深い理解を与えてくれた。
Bug 1:SPA Fallbackが静的リソースを飲み込む
これが最も不可解なバグだった。Nginxにデプロイした後、ページを開くと白い画面。DevToolsを開いてみると、main.jsとstyle.cssのリクエストが返すコンテンツが、なんとindex.htmlだった。
原因はすぐに特定できた——NginxのSPA fallback設定が過剰に積極的だった:
# 問題のある設定
location / {
try_files $uri /index.html;
}
このルールは「リクエストされたファイルが存在しなければindex.htmlを返す」という意味だ。問題は、ビルド後のJS/CSSファイル名にはハッシュが付いており(例:main.a3b2c1.js)、ファイルパスの設定が間違っていたりビルド成果物が正しい場所に配置されていないと、Nginxがファイルを見つけられずindex.htmlを返してしまうこと。ブラウザが受け取った「JavaScript」は実際にはHTMLなので、当然シンタックスエラーになる。
修正はシンプルで、静的リソースに対する正確なマッチングルールを追加した:
location /assets {
alias /path/to/build/assets;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
}
教訓:SPAのfallbackルールは、静的リソースのパスを必ず除外すること。これはよくある問題だが、遭遇するたびに時間を浪費してしまう。
Bug 2:APIレスポンス形式の不一致
フロントエンドが期待するデータ構造:
{ "data": { "nodes": [...], "total": 10 } }
バックエンドが実際に返すデータ:
{ "nodes": [...], "total": 10 }
dataのラッパーが一段足りないだけ。フロントエンドのコードではresponse.data.nodesと書いていて、取得されるのはundefinedになり、リスト全体が空でレンダリングされてしまう。
この種の問題はフロントエンドとバックエンドを並行開発している時に特に発生しやすい。私のアプローチは、APIレイヤーにレスポンスインターセプターを追加して統一的にラッピングすること:
// APIレスポンスインターセプター
app.use((req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (!body.data && !body.error) {
return originalJson({ data: body });
}
return originalJson(body);
};
next();
});
同時にフロントエンド側にも防御的なコードを追加し、両方の形式に対応した。両側で修正する、ダブルの安全策だ。
Bug 3:Reactコンポーネントのnull値の罠
一部のノードの詳細ページを開くと空白になるとユーザーから報告があった。調査の結果、問題はステータスの比較ロジックにあった:
// 問題のあるコード
if (node.status === 'online') {
return <OnlineView />;
}
// node.statusがnullの場合、ここでは何もレンダリングされない
ノードが登録直後でまだステータスを報告していない場合、statusフィールドはnullになる。コードはnullのケースを処理していなかったため、コンポーネントは何もレンダリングしない——ユーザーには空白ページが表示される。
修正方法はデフォルトステータスの処理を追加すること:
const status = node.status ?? 'unknown';
// 'unknown'ステータスに対してフレンドリーな案内画面を表示
所感:Reactでは、APIから取得するあらゆるフィールドがnullやundefinedになり得る。データが完全であると仮定してはならない。現在の私の習慣は、APIからデータを取得した後、最初のステップとしてnormalizeを行い、nullになり得るすべてのフィールドにデフォルト値を設定することだ。
Bug 4:Jackの応答の謎
これが最も興味深かった。ユーザーがOCMを通じてあるノードにコマンドを送信し、ノード上のagent(Jack)が応答した。しかし応答内容が奇妙だった——専用の管理agentではなく、main agentが回答しているようだった。
調査の結果、OpenClawのフォールバックメカニズムが作用していることが判明した。OCMがagents.listを問い合わせた際、返されるリストが空だった(そのノードにはmain agentのみが設定されており、別途管理agentが存在しなかったため)。OpenClawは一致するagentが見つからない場合、自動的にmain agentにフォールバックしてリクエストを処理する。
つまりJackの応答自体は正しかった——ただしmain agentとして応答していたため、応答のスタイルや専門性がユーザーの期待と異なっていただけだ。
解決策:OCMのノード登録フローで、ターゲットノードに対応する管理agentが存在するかを自動チェックするようにした。存在しない場合、UIで明確に「このノードではmain agentが管理コマンドを処理します」と表示する。
Bug 5:状態同期の根本的な問題
最後のバグはアーキテクチャレベルの問題を露呈した:物理的な操作とデータベースの状態が乖離していること。
例を挙げると:ユーザーがOCM上であるノードのOpenClawサービスを再起動した。OCMはデータベースのステータスを「restarting」に更新する。しかしもし再起動中にSSH接続が切れた場合、サービスは実際には正常に再起動されたかもしれないが、データベースのステータスは永遠に「restarting」のまま残る。
この問題には簡単な修正方法がないため、二つの戦略を採用した:
1. 定期的なヘルスチェック:60秒ごとに各ノードにSSHで接続して実際の状態を確認し、データベースと照合して修正
2. ステータスの有効期限メカニズム:中間状態(restarting、deletingなど)が5分以上更新されない場合、自動的に再チェックをトリガー
まとめ
一つの午後で5つのバグを修正した。個々のバグはそれほど難しくないが、まとめて対処するとマラソンのようだ。フロントエンド開発で最も頭を悩ませるのは、個々の問題の複雑さではなく、問題の多様性だ——Nginxの設定からAPIの形式、Reactのレンダリング、ビジネスロジックまで、範囲が極めて広い。
これらすべてを一人で解決できるのは、特定の分野の深さではなく、フルスタックの広さのおかげだ。これこそが個人プロジェクトで得られる最大の鍛錬なのかもしれない。