はじめに
Jira Cloudのタイムライン(ロードマップビュー)、使っていますか?
チームのスプリントや施策の進捗を俯瞰できて便利なのですが、1つ困ったことがあります。土日も祝日も平日も、全部同じ白いマス目なんです。
「この週のここに積んでるタスク、祝日が2日あるな…」という判断を、カレンダーと見比べながら頭の中でやっていました。視覚的に一目でわかれば、スプリント計画の精度が上がるのに。
まず公式設定を探しました。Jira設定 > システム > 勤務カレンダー という項目はあるのですが、これはあくまでSLAや期限計算に使う内部設定で、タイムラインの見た目には反映されません。
設定では解決できない。ならば、と Chrome拡張機能を自作することにしました。
AIと72往復の対話でDOM構造を解析し、「今日マーカーを基準点にする」という逆転の発想でようやく安定した実装にたどり着いた話を書きます。
Chrome拡張を作る決断
Chrome拡張はシンプルな構成で作れます。最低限必要なのは2ファイルです。
jira-holiday-extension/
├── manifest.json # 拡張機能のメタ情報・権限定義
└── content.js # 対象ページに注入するスクリプト
content.js はJiraのタイムラインページが開かれると自動で実行され、DOMを直接操作できます。
最終的に完成したものの機能はこちらです。
| 機能 | 内容 |
|---|---|
| 祝日セルを色付け | 日本の祝日を自動判定してハイライト |
| 土日セルを色付け | 月〜金と視覚的に区別 |
| カラーピッカーUI | ツールバーから色・透明度を変更可能 |
| 祝日データ自動更新 |
holidays-jp.github.io から週1回取得 |
デフォルトは祝日・土日ともに赤・透明度35% で統一しました(チームに配布するため)。
SPA特有のDOM解析という壁
拡張の仕組みは単純です。「タイムラインの各マス目を表すDOM要素を特定して、日付を判定して、CSSを当てる」だけです。
ところが、JiraはReact製のSPAです。ここに最初の壁があります。
Reactで生成されたDOM要素のクラス名は、こんな感じになっています。
<span class="_11c8wadc _1bsb14zn _y3gn1h6o _1i4q1hna ...">21</span>
ハッシュ化された短いクラス名の羅列で、ビルドのたびに変わります。クラス名でセレクタを書くと、Jiraのアップデートで即死します。
安定したセレクタとして使えるのは data-testid 属性 です。これはテスト用に付与された識別子で、エンジニアが意図的に変える場合以外は維持されます。
DevToolsで探すと、タイムラインの日付セルにはこんな data-testid がありました。
timeline.ui.timeline-table-kit.header.chart.calendar-cells.week.day-0
timeline.ui.timeline-table-kit.header.chart.calendar-cells.week.day-1
...
timeline.ui.timeline-table-kit.header.chart.calendar-cells.week.day-6
day-0 が月曜、day-6 が日曜の固定割り当てです。この命名規則を見つけたとき、「これだ」と思いました。
// 安定して使えるセレクタ
document.querySelectorAll('[data-testid*="calendar-cells.week.day-0"]')
ちなみにこの data-testid を見つけるまでに、こんなコマンドを実行しました。
// どんな data-testid が存在するか全列挙する
const testids = new Set();
document.querySelectorAll('[data-testid*="timeline"][data-testid*="header"]').forEach(el => {
testids.add(el.getAttribute('data-testid').replace(/\d+/g, 'N'));
});
console.log([...testids].join('\n'));
出力の中に calendar-cells.week.day-N を発見したときは小さくガッツポーズしました。
最初のアプローチ:月ヘッダーで日付を割り出す
day-0 〜 day-6 で「週の中の曜日」はわかりました。次の問題は**「何月何日か」の特定**です。セルのテキストは日付の数字("21" など)だけで、月・年の情報がありません。
DevToolsで調べると、月が変わる週にだけ月名ラベルが現れることがわかりました。
<span class="css-6cu6fo">Apr '25</span>
この要素のX座標と、各週コンテナのX座標を比較することで「この週は何月か」を計算する方式を実装しました。
しかし、動作確認してみると祝日じゃない日が色付いていました。
原因を調べると、問題が2つありました。
-
span.css-6cu6foは月の最初の週にしか現れない。スクロールすると消える - X座標ベースの月判定は、スクロール位置によってずれる
「月ヘッダーのX座標でどの週が何月かを割り出す」という設計自体が、SPAの動的なレンダリングと相性が悪かったのです。
ブレイクスルー:今日マーカーを基準点にする
月ヘッダーに頼るアプローチを捨てて、別の基準点を探していたとき、data-testid の一覧の中に気になる文字列を見つけました。
roadmap.timeline-table.main.scrollable-overlay.today-marker.container
「今日マーカー」 です。Jiraのタイムラインには、現在日付の位置に縦線が表示されます。これを表すDOM要素です。
この要素には重要な性質があります。
-
今日の日付は常に
new Date()で取得できる(JavaScriptの基本) - 今日マーカーのX座標は、ページ上で「今日」の位置を示している
つまり、このマーカーが今日の位置を「物理的に」教えてくれます。
発想を逆転させました。
月ヘッダーから日付を「推定」するのではなく、今日マーカーから日付を「確定」して、他の週はそこからオフセットで計算する
実装のポイントはこうです。
function buildDateMap() {
// 今日マーカーの位置を取得
const todayMarker = document.querySelector(
'[data-testid="roadmap.timeline-table.main.scrollable-overlay.today-marker.container"]'
);
const todayX = todayMarker.getBoundingClientRect().left;
// 今日の実際の日付(JavaScriptから確実に取得)
const today = new Date();
today.setHours(0, 0, 0, 0);
// 今日が属する週コンテナを特定し、その月曜日の日付を計算
const allWeeks = document.querySelectorAll('[data-testid*="calendar-cells.week.day-0"]');
let anchorMonday = null;
let anchorWeekIndex = -1;
allWeeks.forEach((dayEl, i) => {
const rect = dayEl.parentElement.getBoundingClientRect();
if (todayX >= rect.left && todayX < rect.right) {
// 今日はこの週にいる
const dayOfWeek = today.getDay(); // 0=日, 1=月, ..., 6=土
const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
anchorMonday = new Date(today);
anchorMonday.setDate(today.getDate() - daysFromMonday);
anchorWeekIndex = i;
}
});
// アンカー週から他の週の月曜日を計算
const dateMap = new Map();
allWeeks.forEach((dayEl, i) => {
const weekOffset = i - anchorWeekIndex;
const monday = new Date(anchorMonday);
monday.setDate(anchorMonday.getDate() + weekOffset * 7);
// その週の7日分の日付を登録
for (let d = 0; d < 7; d++) {
const date = new Date(monday);
date.setDate(monday.getDate() + d);
const testid = `calendar-cells.week.day-${d}`;
dateMap.set(/* ... */ testid + weekOffset, date);
}
});
return dateMap;
}
月ヘッダーのX座標もクラス名も一切使っていません。今日マーカーとJavaScriptの Date だけで確実に日付が割り出せます。
この方式に変えた瞬間、祝日のズレがなくなりました。
日本の祝日データの取得
日付が特定できたら、その日が祝日かどうかの判定が必要です。
holidays-jp.github.io が公開しているAPIを使いました。JSONで日本の祝日一覧を返してくれます。毎回取得するのは非効率なので、chrome.storage.local にキャッシュして週1回だけ更新する設計にしています。
async function getHolidays() {
const cache = await chrome.storage.local.get(['jp_holidays', 'jp_holidays_updated']);
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
if (cache.jp_holidays && cache.jp_holidays_updated > oneWeekAgo) {
return cache.jp_holidays; // キャッシュが新鮮なら使い回す
}
const res = await fetch('https://holidays-jp.github.io/api/v1/date.json');
const holidays = await res.json(); // { "2025-04-29": "昭和の日", ... }
await chrome.storage.local.set({ jp_holidays: holidays, jp_holidays_updated: Date.now() });
return holidays;
}
デバッグ中に1つはまりました。Chromeの開発者ツールのコンソールで chrome.storage.local を直接操作しようとしたところ、こんなエラーが出ました。
Uncaught TypeError: Cannot read properties of undefined (reading 'local')
chrome.storage はコンテンツスクリプトや拡張機能のコンテキストでしか動作しません。ページのコンソールからは触れないのです。デバッグ時はキャッシュの状態を chrome://extensions/ の「サービスワーカー」コンソールから確認する必要があります。
AIとの72往復DOM解析プロセス
この拡張を1日で完成させられたのは、AIと対話しながらDOM解析を進めたからです。セッションのメッセージ数は72往復でした。
ただ、順風満帆ではありませんでした。いくつか印象的なはまりポイントを紹介します。
「allow pasting」問題
ChromeのDevToolsコンソールは、初回は貼り付けが制限されています。AIに「コンソールに allow pasting と入力してEnterを押すと有効になります」と言われたので実行すると…
VM2040:1 Uncaught SyntaxError: Unexpected identifier 'pasting'
allow pasting はChromeが表示する警告テキストそのもので、コマンドではありません。コンソールに何かを入力した時点でペーストが有効になる仕組みでした。AIも間違えることがあります。
undefined しか返ってこない謎
長いJavaScriptを実行しても undefined しか表示されない現象に悩みました。原因の1つはコンソールのフィルターです。検索バーに文字が入っていると、console.log の出力が全部フィルタリングされて見えなくなります。「フィルターバーの × をクリックしてクリアしてください」という回答でようやく解決しました。
もう1つの原因は console.log 自体の戻り値が undefined であること。配列を直接返す形に変えると確認しやすくなりました。
// コンソールに直接結果を返す書き方
[...document.querySelectorAll('[data-testid*="roadmap"]')]
.map(el => el.getAttribute('data-testid').replace(/\d+/g, 'N'))
.filter((v, i, a) => a.indexOf(v) === i) // ユニーク化
.slice(0, 20)
AIが探索を構造化してくれた
最も助かったのは、「今どこを探していて、何がわかっていて、何が足りないか」をAIが整理しながら進めてくれた点です。人間が DevTools を手動で探索するだけでは見落としそうな data-testid の命名規則や、div[role="columnheader"] の構造(月ヘッダーと週コンテナが同居している点)も、AIが「次はこれを確認してください」と誘導することで発見できました。
機能追加:土日対応&カラーピッカー
祝日の色付けが安定したところで「土日も同じように色付けできる?」と確認したところ、day-5 と day-6 が常に土日に対応しているのですぐに実装できました。
さらに、チームに配布することを考えて、色をカスタマイズできるUIを追加しました。
manifest.json に popup 追加
↓
popup.html でカラーピッカーと透明度スライダーを配置
↓
popup.js で設定を chrome.storage.local に保存
↓
content.js で設定を読み込んで CSS の rgba() に反映
ポイントは chrome.storage.onChanged を使って、ポップアップで「保存」を押した瞬間にページ側が再描画されるようにしたことです。リロード不要でリアルタイムにプレビューできます。
メンテナンスの考え方
最後に正直に書いておきます。この拡張機能はJiraのアップデートで動かなくなる可能性があります。
使っているセレクタの安定度はこんな感じです。
| セレクタ | 安定度 | 変わりやすい理由 |
|---|---|---|
data-testid*="calendar-cells.week.day-N" |
中 | テスト設計の変更で消える可能性あり |
data-testid*="today-marker" |
中 | 同上 |
span.css-6cu6fo |
低 | CSSモジュールのハッシュはビルドで変わる |
data-testid は css-* よりは安定していますが、Jiraが内部実装を変えれば消えます。
壊れたときの対処法は「DevToolsで data-testid を再探索する」です。
// 動かなくなったら、まずこれで現在の testid 構造を確認
[...new Set(
[...document.querySelectorAll('[data-testid*="timeline"]')]
.map(el => el.getAttribute('data-testid').replace(/\.\d+/g, '.N').split('.').slice(0, 8).join('.'))
)].join('\n')
このコマンドをコンソールで実行すると、現在のタイムラインDOMにある data-testid の一覧が出ます。calendar-cells や today-marker が見つかれば、セレクタを更新するだけで復活します。
まとめ
この開発で得た一番の知見は「動的SPA上の日付特定は、安定した基準点から逆算する」という設計思想です。
月ヘッダーのテキストや位置から日付を「推定」しようとすると、DOMの動的な変化に弱くなります。対して「今日マーカー」は常に存在し、JavaScriptの new Date() と1対1で対応する唯一無二の基準点です。これを軸にすることで、月またぎの週や年またぎのケースも計算で正しく処理できます。
この手法は Jira 以外の SPA にも応用できます。「変わらない基準点はどこか?」を探すことが、動的DOM攻略の第一歩です。
また、AIとの72往復のDOM解析は「AIに実際のDOMを見せながら一緒に探索する」というスタイルでした。AIが「次はこのコマンドを実行してください」→ 結果を貼り付け → AIが分析して次の手を提案、という繰り返しです。自分一人でDevToolsと睨めっこするより圧倒的に速く、確実に構造を把握できました。
Jiraのタイムラインで同じ悩みを持っている方の参考になれば幸いです。