複数の WordPress サイトを保守していると、サイト一覧に「最後のメンテナンス日」を出すのは自然な発想です。2026-05-21 のような日付が並ぶ。けれど現場で使っていると、これだけでは足りない場面があります。
クライアントからこんな要望が来ました。「最後のメンテナンス日とは別に、何日経っているかも表示できると良い。15 日 / 30 日 / 60 日で色が変わって危険度がわかると尚良い」。今回はこの「絶対日付 → 相対経過日数 + 色」への一歩を、設計の細部を含めて整理しておきます。
なぜ「日付」だけでは足りないのか
2026-05-21 という絶対日付は正確ですが、頭の中で「今日との差」を計算する負荷が利用者に乗ります。サイトが 5 件なら何とかなっても、メンテ対象が増えてくると「どれが放置気味か」を日付の羅列から読み取るのは難しい。
棚卸しの目的は「どのサイトが要対応か」を一目で掴むことです。だとすれば、見せるべきは絶対日付そのものより「最後のメンテから何日経ったか」という相対量で、しかも「何日経つと危ないか」を色で直感させたい。クライアントの要望は、まさにこの「絶対 → 相対 + 危険度」の転換を突いていました。
4 段階の色分け
採用したのは経過日数による 4 段階の色分けです。最後のメンテ日の直後に (15日前) のような小さなバッジを併記し、しきい値で色を変えます。
| 経過日数 | tier | 色 | 意味 |
|---|---|---|---|
| 0〜14 日 | fresh | 緑 | 最近メンテ済み・安心 |
| 15〜29 日 | normal | グレー | 標準 |
| 30〜59 日 | warn | amber | 要注意 |
| 60 日〜 | danger | 赤 | 要対応 |
緑 → グレー → amber → 赤 の遷移で、リストをスクロールするだけで「赤が多いな」「ここ最近触れてないサイトが固まってる」が視覚的に飛び込んできます。バッジには hover で「最後のメンテナンスから N 日経過しています」の tooltip も付けて、数値の意味を補足しています。
ヘルパー関数に集約する
表示ロジックは複数箇所(リストビュー・グリッドビュー)から呼ばれるので、インラインで日数計算を散らすと DRY 違反になります。ヘルパー関数群に集約しました。
// 経過日数を返す。空入力/無効入力は null、未来日は 0 にクランプ
function _daysSinceLastRun(lastRunStr) {
if (!lastRunStr) return null;
// Safari 互換: "YYYY-MM-DD" を "YYYY/MM/DD" に正規化してから Date へ
const normalized = lastRunStr.replace(/-/g, '/');
const d = new Date(normalized);
if (isNaN(d.getTime())) return null;
const diffMs = Date.now() - d.getTime();
const days = Math.floor(diffMs / 86400000);
return days < 0 ? 0 : days; // 未来日は時計ズレ防御で 0 にクランプ
}
// しきい値別の色オブジェクトを返す
function _daysAgoStyle(days) {
if (days <= 14) return { tier: 'fresh', color: '#15803d', bg: '#dcfce7' };
if (days <= 29) return { tier: 'normal', color: '#4b5563', bg: '#f3f4f6' };
if (days <= 59) return { tier: 'warn', color: '#92400e', bg: '#fef3c7' };
return { tier: 'danger', color: '#7f1d1d', bg: '#fee2e2' };
}
3 つの「地味だが効く」防御を入れています。
Safari 互換の日付正規化
new Date("2026-05-21") は Chrome では通りますが、Safari では環境によって Invalid Date になることがあります。YYYY-MM-DD のハイフン区切りを Safari がうまく解釈しない既知の挙動です。ハイフンをスラッシュに置換した 2026/05/21 形式に正規化してから Date に渡すと、Safari でも安定して解釈されます。クロスブラウザの日付パースは、ライブラリを入れないなら最初から正規化しておくのが安全です。
未来日のクランプ(時計ズレ防御)
サーバーとクライアントの時計がずれていたり、メンテ日が何らかの理由で未来日付になっていると、経過日数がマイナスになります。(-3日前) のような表示は意味不明なので、days < 0 ? 0 : days でゼロにクランプ。「本日」扱いに倒します。エッジケースですが、出ると一気に「壊れてる感」が出る種類のバグです。
i18n フォールバック
経過日数の文言は 本日 / 1日前 / N日前 を i18n 経由で生成します。ここで罠だったのが、i18n キーが読み込まれる前に描画が走ると、site_list.days_ago_n のような生キーがそのまま画面に出ること。
function _formatDaysAgoText(days) {
// _i18n を直接参照し、未登録なら日本語 fallback
if (days === 0) return _i18n?.days_ago_today ?? '本日';
if (days === 1) return _i18n?.days_ago_one ?? '1日前';
const tmpl = _i18n?.days_ago_n ?? '${days}日前';
return tmpl.replace('${days}', days);
}
?? '日本語 fallback' を挟むことで、i18n の読み込みタイミングに関わらず必ず意味のある文言が出ます。動的に生成する文言は、i18n の初期化レースを考えてフォールバックを必ず用意する、というのが教訓でした。
回帰テストで色しきい値を固定する
色分けのしきい値(14 / 29 / 59 / 60+)は仕様そのものなので、回帰テストで固定しました。tests/test_days_since_last_run.py に 18 件 ── 4 段階の境界値(14 と 15、29 と 30、59 と 60)が正しい tier を返すか・null / NaN / 未来日クランプの防御・Safari 正規化・リスト/グリッド両ビューでヘルパーが呼ばれているか・旧インライン計算が残っていないか(DRY 違反検出)。
特に境界値テストは大事です。「30 日は warn か normal か」のような off-by-one は、仕様を読んだだけでは見落としやすく、テストで固定しておかないと将来のリファクタで静かにずれます。
まとめ — 「絶対値」より「相対量 + 危険度」
このラウンドから取り出せる原則は次の 3 つでした。
- 棚卸し UI は絶対値より相対量を見せる — 「最後のメンテ日 2026-05-21」より「(15日前)」のほうが、利用者の脳内計算を肩代わりする。さらに色で危険度を直感させると、リストをスクロールするだけで要対応サイトが浮かび上がる
- 日付パースはクロスブラウザ防御を最初から入れる — Safari の
YYYY-MM-DD問題・未来日クランプは、出てから対処すると「環境依存で再現しない」と悩むことになる。正規化とクランプを最初から組み込む - 動的生成文言は i18n フォールバックを必ず添える — i18n の読み込みレースで生キーが画面に出る事故は、
?? '日本語'のフォールバックで構造的に防げる
「日付を出す」という素朴な機能の中に、相対時間の見せ方・クロスブラウザの日付パース・i18n レース・境界値テストと、地味な設計判断が複数詰まっていました。多サイトを横断して扱う管理画面では、「いつ」を「あと何日で危ないか」に翻訳して見せると、要対応サイトの見極めにかかる負荷が下がり、対応漏れを未然に防ぎやすくなると思います。