コンテンツへスキップ

「実行中サイトが弱く見える」と言われた日 — 3 状態の視覚階層を背景色で建て直した話

多サイト一覧にメンテ中・完了の色枠を入れた 後、運用に出してから「実行中サイトが、なんか弱く見える」というユーザー指摘が来ました。続けて「24 時間で残るはずの緑枠が出ない」も。

最初の指摘は意外でした。実行中サイトには青の脈動枠を付けてあって、ちゃんと目立つはずです。けれど画面を見直すと、確かに相対的に弱く見える。原因は「実行中の色そのもの」ではなく、3 つの状態の視覚階層が暗黙に逆転していたことでした。今回はこの「視覚階層の崩壊と再構築」の話と、ついでに踏んでいた「設計の揺れ戻し」を整理しておきます。

ぱっと見の感想が「実行中 < 実行待ち」になっていた

多サイト一覧には 3 つの状態があります:

  • 実行待ち(pending)── これから順に処理されるサイト
  • 実行中(running)── いま処理しているサイト
  • 完了(completed)── 24h 以内に終わったサイト

旧 CSS の塗り方はこうでした:

/* 旧: 実行中は背景なし・実行待ちには背景あり */
.site-running {
  border-color: #2563eb;
  /* background-color の指定なし */
  animation: pulse 2.2s ...;
}
.site-pending {
  border: 1px dashed #2563eb;
  background-color: rgba(37, 99, 235, 0.02);
}

「実行中は脈動で目立つから背景は要らない」「実行待ちは少しだけ控えめに塗る」── 一見筋が通った設計に見えます。が、運用の画面ではでした。

実行待ちが薄い破線枠 + 薄い青背景で「面で主張」しているのに対し、実行中は背景なしで「線で主張」している。面の主張は線より強いので、視覚階層が pending > running に逆転していたわけです。「実行中サイトが弱く見える」は錯覚ではなく、設計のミスでした。

視覚階層を背景 alpha で明示する

修正は背景色を 3 状態すべてに与えて、alpha 値で階層を作ることに集約しました。

.site-running {
  border-color: #2563eb;
  background-color: rgba(37, 99, 235, 0.08);  /* 最も濃い */
  animation: pulse 2.2s ease-in-out infinite;
  box-shadow: 0 0 0 6px rgba(37, 99, 235, 0.0);  /* 脈動も少し強化 */
}
.site-completed {
  border-color: #10b981;
  background-color: rgba(16, 185, 129, 0.05);  /* 中間 */
  box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.25);
}
.site-pending {
  border: 1px dashed #2563eb;
  background-color: rgba(37, 99, 235, 0.02);    /* 最も薄い */
}

3 状態の alpha が 0.08 > 0.05 > 0.02 の順で並ぶ。これで running > completed > pending の視覚階層が数値として保証されます。回帰テストにもこれを書きました ── 「実行中の背景 alpha は実行待ちの背景 alpha より大きい」を assert する 1 行で、将来誰かが値を入れ替えてしまった時に CI で落ちます。

// テスト擬似コード
function test_running_alpha_is_higher_than_pending() {
  const r = getComputedAlpha('.site-running');
  const p = getComputedAlpha('.site-pending');
  assert(r > p, '実行中は実行待ちより目立つはず');
}

色のトーンが感覚的に揃っていれば良い」と言いたくなる場面ですが、alpha 値という数字で階層を表現すると、視覚デザインの意図を機械検証できる範囲に持ち込めます。

もう一つの指摘 — 「24h 緑枠が出ない」副作用

同じ報告に「完了サイトの緑枠が出ない」も含まれていました。これは独立した別バグかと思ったら、実は前回の修正の副作用でした。

ストリーミングログから実行中サイトを当てる難しさ で書いた通り、ログの逆走で誤検出する問題を直すために「サイト切替時の完了マーク」を全面的に廃止して、終了時に一括マークする設計に振りました。これで誤検出は消えましたが、正常系で順次進行している間も「前のサイトが緑になる」現れ方が消えたわけです。

順次進行: サイト A → サイト B と切り替わった瞬間、「サイト Aが緑」になるのがユーザーの期待値。今は終了時まで誰も緑にならないので、「実行中の青脈動」しか視覚情報がない状態でした。

修正: 「前方移動時のみ完了マーク」

逆走防御は維持したいが、正常系の途中緑化も復活させたい。両立は、実行順インデックスで前方移動かどうかを判定することで達成できました。

function _setRunningSiteId(siteId) {
  const newIdx = _runningSiteIdOrder.indexOf(siteId);
  const curIdx = _runningSiteIdIndex;
  const isForwardMove = newIdx === -1 || newIdx >= curIdx;

  if (isForwardMove && _runningSiteId && _runningSiteId !== siteId) {
    _markSiteCompleted(_runningSiteId);  // 前方移動時のみ前を完了マーク
  }
  // 逆走(newIdx < curIdx)の場合は何もしない(前回の修正は維持)

  _runningSiteIdIndex = newIdx;
  _runningSiteId = siteId;
}

実行予定にない site_id(部分実行・想定外サイト)は newIdx === -1 で「前方移動扱い」にフォールバック。これで:

  • 正常順次進行(サイト A → サイト B): サイト Aが緑になる ✅
  • ログ逆走(サイト Aが再出現): 無視される ✅

両立しました。「逆走防御を入れる」と「前方移動時の表示変化を取り戻す」が、最初は二者択一に見えていたのが、進行方向の判定を 1 行足すだけで両立できたのは、設計の振り戻しとしては綺麗な収束でした。

振り返り — 「設計の揺れ戻し」をネガティブに捉えない

時系列で見ると、この機能はこういう揺れ方をしました:

  1. 初版(V33): サイト切替で前サイトを完了マーク
  2. V43 の修正: ログ逆走の誤検出を直すため、切替時の完了マークを全廃 → 終了時に一括マーク
  3. 本ラウンド: 「24h 緑枠が見えない」副作用が発覚 → 前方移動時のみ完了マークを復活させて両立

「最初から正解にたどり着けなかった」とも言えますが、別の見方をすれば、運用で見えた問題に対して最小限の修正を入れ、副作用が出たらまた最小限で修正する、という進め方そのものは健全でした。最初に「逆走防御 + 前方移動判定」を全部入れた完璧版を設計するのは、運用の声を聞く前ではほぼ不可能です。

学び — 視覚階層は alpha 値で機械検証する

このラウンドから取り出せる原則は次の 3 つでした。

  1. 線より面の方が強く主張する — 「脈動枠で目立っている」と思っていた実行中サイトが、背景塗りで主張している実行待ちサイトに視覚的に負けていた。3 状態がある UI では、最も重要な状態に背景塗りを与える方が線で勝負するより強い
  2. 視覚階層は数値(alpha 値)で表現すると機械検証できる0.08 > 0.05 > 0.02 のような順序を CSS に書いて、テストで running > completed > pending を assert すれば、将来の改修で値を入れ替えても CI が落ちる。デザインを「機械が守れる形式」に翻訳する
  3. 「揺れ戻し」は健全な設計の進み方 — 最初の修正で副作用が出たら、次の修正で副作用を吸収する。両立できると思わなかった選択肢(逆走防御 vs 順次緑化)も、判定軸を 1 つ足すだけで両立できることが多い。完璧版を最初から目指すより、揺らしながら収束させる方が運用の声に追随しやすい

目立つはずなのに目立たない」種類の体感的な指摘は、原因が「色そのもの」ではなく「他要素との相対関係」にあることが多いです。3 状態を同時に並べる UI を作る時は、最初から「どれが最も目立つべきか」を alpha 値の大小で明示しておくと、後で建て直す手間が減ると思います。