コンテンツへスキップ

多サイト一覧に「メンテ中・完了」を青脈動と緑枠で見せる — バックエンド改修ゼロでの状態可視化

複数サイトのメンテナンスを順番に流していると、一覧画面のどこに「いま処理中のサイト」がいて、「終わったサイト」がどれなのかが、文字情報だけだとパッと掴めません。クライアントから「メンテナンス中と完了サイトが一覧で視覚的にわかると良い」という素直な要望が来ました。

ぱっと見でわかる色付きの枠を付ければよさそうですが、決めることがいくつかあります。色は何にするか、状態はどこから取るか、終わったマークはいつ消すか、そして ── バックエンドを改修しないで実装できるか。今回はこの 4 つの判断と、最小限のフロント実装で済ませた話をまとめておきます。

色の選び方 — 「赤の点滅」は最初に却下した

実行中サイトをどう目立たせるか。直感的には「赤い点滅」が思い浮かびますが、これは早めに却下しました。多サイトメンテナンスは長時間の作業で、その間ずっと画面のどこかで赤が点滅していると、利用者の疲労源になります。

採用したのは「青の穏やかな脈動 + 緑の実線枠」の組合せです。

  • 実行中: 青 #2563eb の border + box-shadow による緩やかな脈動(2.2s ease-in-out)
  • 完了(24h 以内): 緑 #10b981 の実線 border + 薄い inset シャドウ
@keyframes site-running-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.4); }
  50%      { box-shadow: 0 0 0 6px rgba(37, 99, 235, 0); }
}
.site-running {
  border-color: #2563eb !important;
  animation: site-running-pulse 2.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .site-running { animation: none; }   /* アクセシビリティ配慮 */
}
.site-completed {
  border-color: #10b981 !important;
  box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.25);
}

prefers-reduced-motion: reduce の OS 設定(モーション軽減を有効にしているユーザー)では脈動が止まる ── これは前庭機能に過敏な利用者への配慮です。動きで気を引く設計を入れるなら、ほぼ自動で必須の対応になります。

バックエンド改修ゼロ — 既存のログストリーミングを再利用する

「いま処理中のサイトはどれか」を一覧 UI に伝えるためには、サーバー側から状態を流す必要があります。素直にやるなら /api/maintenance/status のような新規エンドポイントを立てる ── けれど、メンテナンスログはもう既にストリーミング配信されていました。[サイト名] メッセージ 形式の行が次々と流れてくるあのログです。

このログをフロント側で正規表現パースすれば、新規 API なしで「いま処理中のサイト」が取れることに気づきました。バックエンドに手を入れずに済む経路です。

function _detectRunningSiteFromLog(logText) {
  // ログは "2026-05-28 09:40:13 - [INFO] - [サイト名] メッセージ" 形式
  const lines = logText.split('\n');
  for (let i = lines.length - 1; i >= 0; i--) {  // 末尾から走査
    const matches = [...lines[i].matchAll(/\[([^\]]+)\]/g)];
    for (const m of matches) {
      const candidate = m[1];
      // log level の bracket は除外
      if (['INFO', 'WARNING', 'ERROR', 'DEBUG', 'TRACE'].includes(candidate)) continue;
      // allSites の site_name と照合
      const site = allSites.find(s => s.site_name === candidate);
      if (site) return site._id;
    }
  }
  return null;
}

末尾から走査するのは「最新のサイト名」を取りたいから。[INFO] のような log level の bracket も [...] でマッチするので、明示的に除外します。1 行に [INFO][サイト名] が両方あるケースに対応するため、行内のすべての bracket を順に試す形にしています。

サイト切替で前を完了マーク

_runningSiteId が変わったら、前のサイトを完了扱いにしてやれば、自然に「実行中 → 完了」の流れが UI に現れます。

function _setRunningSiteId(siteId) {
  if (_runningSiteId && _runningSiteId !== siteId) {
    _markSiteCompleted(_runningSiteId);  // 前のサイトは完了
  }
  _runningSiteId = siteId;
  filterSites();                          // 一覧再描画で枠色切替
}

この実装はこの記事の時点で「素朴」です。実際にはこの後、ログの非単調な出方(複数サイト名が前後に混ざる・初期化ループで全サイト名が先行出力される等)で3 つの落とし穴を踏むことになります。その顛末は ストリーミングログから処理中サイトを当てる難しさ にまとめました ── 結論だけ言うと、「サイト切替で前を完了」というルールはログの並びに対して脆く、後でマーカー方式(特定の 1 行だけを検出対象にする)に書き直すことになります。

最初の素朴な実装で気づけなかったのは、ログの並びを「常に単調に進む」と暗黙に仮定していたことでした。可視化機能を組み込むときは、ログという非同期データソースの揺れに対する耐性を最初から考えておくべき、という後付けの学びになっています。

完了マークを 24h で自動消失させる

緑枠を「永久に残す」と、利用者が「これ、いつメンテしたんだっけ」を読み取れなくなります(特に複数日にまたがる運用で混乱する)。逆に「ページリロードで消える」だと、それはそれで頼りない。

折衷案として 24 時間で自動消失 を入れました。完了タイムスタンプを localStorage に保存し、読み込み時に 24h を超えたエントリをフィルタ除外します。

function _loadCompletedSites() {
  try {
    const raw = localStorage.getItem(KEY_COMPLETED_SITES);
    if (!raw) return {};
    const parsed = JSON.parse(raw);
    if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
      return {};                                       // 型不正は捨てる
    }
    const cutoff = Date.now() - 24 * 60 * 60 * 1000;
    const filtered = {};
    for (const [siteId, ts] of Object.entries(parsed)) {
      if (typeof ts === 'number' && ts > cutoff) {     // 24h 以内のみ
        filtered[siteId] = ts;
      }
    }
    return filtered;
  } catch (e) {
    return {};                                         // JSON 不正・localStorage 無効
  }
}

3 段階のデータ防御を入れています。JSON パース失敗・型不正・古すぎるの 3 ケースで空辞書にフォールバック。localStorage が無効な環境(プライベートモード等)でも try/catch で落ちないようにします。「あれば嬉しい」種類のキャッシュは、壊れていても起動を妨げない方が原則です。

ちなみに、この 24h で消える完了マークの挙動は後で「キャッシュ寿命の設計」を巡るやり直しに発展して、ダッシュボードキャッシュの寿命を巡る 3 つのやり直し で TTL を 30 日に伸ばしたり、メンテ実行で部分消去にしたりと、何度も触ることになります。最初の 24h は「ひとまず合理的に見える初期値」でしかなく、運用に出てから「もっと長く残してほしい」「いや、メンテで全消ししないでほしい」と各方面から指摘が来ました。

まとめ — 「最小限で組んで、運用で揺らす」

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

  1. 新規 API を立てる前に既存ストリームを再利用できないか確認する — ログストリーミングが既に動いていれば、新規エンドポイントを生やすよりフロントでパースする方が、影響範囲も小さく素早く組める。バックエンド改修ゼロで状態可視化を実装できた
  2. アクセシビリティはアニメーション導入の自動条件prefers-reduced-motion: reduce の対応は、動きで気を引く設計を入れるなら必須。「赤点滅」を最初から却下したのも同じ系列の判断
  3. 「素朴な第一実装」は運用に出してから揺らす前提で組む — サイト切替で前を完了マークするルールは、ログの並びに対して脆い前提を含んでいた。後にマーカー方式に書き直すことになる。「最小限で動かして、運用で見えた揺らぎに対応する」進め方は、過剰に設計するより何が問題かを早く明らかにできる

可視化機能は「あると嬉しい」程度に思われがちですが、実際には運用者の負荷を地味に下げる価値があります。複数サイト × 長時間メンテ × 視覚情報ほぼゼロの組合せは、シンプルに疲労源だったので。色 1 つでも、思っているより効きます。