Skip to content

feat(#207): SW notificationclick で trip / block へ deep link 遷移 - #209

Open
kuu13580 wants to merge 6 commits into
developfrom
feature/issue207_sw-notificationclick-deeplink
Open

feat(#207): SW notificationclick で trip / block へ deep link 遷移#209
kuu13580 wants to merge 6 commits into
developfrom
feature/issue207_sw-notificationclick-deeplink

Conversation

@kuu13580

@kuu13580 kuu13580 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #207

Summary

FCM 通知タップ時の挙動を Firebase デフォルトから明示ハンドラに置き換え、既存 PWA/tab を単一 tab のまま該当 block まで自動 scroll するようにする。#148 の P6b (別 issue 送り) の消化。

やったこと

  • SW (firebase-messaging-sw.js): notificationclick handler を追加。既存 client を focused > visible > 任意 の優先度で選び、postMessage で SPA navigate + focus()。無ければ clients.openWindow。別 origin URL は payload 経路含め弾く
  • Client (useFcmNavigationListener): SW からの FCM_NAVIGATE message を受けて React Router の navigate() を呼ぶ。App.tsx で 1 回登録
  • useFocusBlockOnMount: ?focusBlock={id}useBlock(id) で解決 → selectedPageId 切替 → data-block-id の DOM を rAF 待機 → scrollIntoView({ behavior: 'smooth', block: 'center' })replaceState で query 除去。404 / 非数値ゴミは黙って掃除
  • useBlocks populate: list fetch 時に個別 key /blocks/{id} に populate し useBlock の重複 fetch を回避
  • View block 2 種に data-block-id 付与 (edit モードは対象外)
  • test: 上記 hook 2 種の unit test を追加 (12 tests)

設計上の判断 (コードから読めない部分)

postMessagefocus() を選択 (navigate() ではなく)

WindowClient.navigate(url) はブラウザレベルのフル遷移になり atom / SWR / scroll 位置を全て失う。postMessage で client に指示を送って React Router で navigate すれば、同 trip 内の block 遷移は state を保ったまま ?focusBlock= の差し替えだけで済み、別 trip でも SPA 遷移で初期 fetch キャッシュを保持できる。

client 選択は focused > visible > 任意 に統一

PWA client を自己申告方式で明示的に優先する案 (client → SW への postMessage + IndexedDB 永続化) も検討したが、SW ライフサイクル管理のコストに対して「PWA / ブラウザ tab 両方 background + PWA 期待」という限定的なエッジしか救えないため MVP は見送り。iOS は PWA のみで判別問題不発生。

useBlocks populate は既存個別キャッシュを尊重

list revalidation が edit モードの 5s interval で走ると、useUpdateBlock の楽観更新値をサーバ古い値で上書きする race が発生する。cache.get(key)?.data === undefined の時のみ populate することで巻き戻し防止。以降の同期は useUpdateBlock 経由で行う。

View mode のみ対応 (edit は対象外)

通知タップの用途は閲覧目的が大半で、data-block-id は View 系 Block コンポーネントにのみ付与。編集は遷移後にユーザが自発的に入れば十分。

Test plan

  • type-check / lint / format
  • vitest (204 passed、うち今回追加 12 tests)
  • stg 実機 verify
    • Android Chrome (通常 tab / PWA install)
    • iOS Safari PWA (install 済)
    • Desktop Chrome (フォアグラウンド / バックグラウンド両方)
    • 通知タップで該当 trip の該当 block まで scroll する
    • 別 trip 見てる時にタップで単一 tab で SPA 遷移する

Summary by CodeRabbit

  • 新機能

    • 通知タップ時に、既存のアプリ画面へスムーズに遷移できるようになりました。
    • 対象画面が開いていない場合は、新しいウィンドウで開きます。
    • 通知から指定されたブロックへ自動スクロールし、対象を中央に表示します。
    • 通知に含まれる不正なリンクや外部リンクは安全に無視します。
    • 保留中の通知遷移を、アプリ再表示時にも処理できるようになりました。
    • デバッグ用ログの確認、コピー、削除が可能になりました。
  • ドキュメント

    • 通知からのディープリンク動作と実装状況の説明を更新しました。

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

FCM通知クリック時に、Service Workerが同一オリジンのクライアントへSPA遷移を通知します。保留URLはCache Storageでも処理します。TripPageはfocusBlockで対象ブロックを選択し、表示後にスクロールしてクエリを削除します。診断ログをIndexedDBに保存します。

Changes

通知ディープリンク

Layer / File(s) Summary
Service Workerの通知クリック処理
frontend/public/firebase-messaging-sw.js
通知リンクを検証し、保留意図を保存します。既存クライアントにはFCM_NAVIGATEを送信し、対象がない場合はopenWindowを実行します。
クライアント側のSPAナビゲーション
frontend/src/hooks/useFcmNavigationListener.ts, frontend/src/hooks/useFcmNavigationListener.test.tsx, frontend/src/App.tsx
Service Workerメッセージと保留意図を処理します。重複遷移、無効URL、別オリジン、未対応環境を処理します。
対象ブロックの選択とスクロール
frontend/src/hooks/useFocusBlockOnMount.ts, frontend/src/hooks/useBlocks.ts, frontend/src/pages/TripPage.tsx, frontend/src/components/blocks/view/*, frontend/src/hooks/useFocusBlockOnMount.test.tsx
focusBlockからブロックを取得し、ページ選択後にDOM要素を待機して中央へスクロールします。処理後にクエリを削除し、個別ブロックキャッシュを同期します。
診断ログの保存と表示
frontend/src/lib/debugLogger.ts, frontend/src/components/DebugLogPanel.tsx, frontend/src/App.tsx
IndexedDBに診断ログを最大500件保存します。debugパラメータでログ表示、コピー、削除を行います。
通知ディープリンク仕様の更新
docs/notifications.md, docs/memo/notification_roadmap.md
通知クリックからクライアント遷移、ブロックスクロール、クエリ削除までの処理を文書化します。

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FirebaseMessagingSW
  participant CacheStorage
  participant App
  participant TripPage
  participant BlockElement
  User->>FirebaseMessagingSW: notificationclick
  FirebaseMessagingSW->>CacheStorage: store pending intent
  FirebaseMessagingSW->>App: postMessage FCM_NAVIGATE
  App->>TripPage: navigate to pathname, search, hash
  TripPage->>TripPage: process focusBlock
  TripPage->>BlockElement: scrollIntoView center
Loading

Suggested reviewers: bobtaroh

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 通知遷移に直接必要な変更に加えて、DebugLogPanel と IndexedDB 診断ロガーを追加しています。 DebugLogPanel と診断ロガーを別PRへ分離するか、Issue #207 の要件として目的と必要性を明記してください。
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、Issue #207 に対応する Service Worker の通知クリックによる trip / block への deep link 遷移を明確に示しています。
Linked Issues check ✅ Passed [#207] の notificationclick、既存 client の再利用、openWindow、focusBlock 処理という実装要件を満たしています。
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue207_sw-notificationclick-deeplink

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit a6638e7):

https://tabi-share-8ef6b--pr209-feature-issue207-sw-n7jp3kfl.web.app

(expires Sat, 08 Aug 2026 07:36:31 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 9f2a87ede127df7673322845e34cf22c1372d720

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/public/firebase-messaging-sw.js`:
- Around line 40-53: Move the self.addEventListener('notificationclick', ...)
registration, including its custom deep-link validation and navigation logic,
before the Firebase importScripts, initializeApp, and onBackgroundMessage setup
in firebase-messaging-sw.js. Preserve the existing same-origin checks and click
behavior while ensuring the custom handler is registered first.

In `@frontend/src/hooks/useFocusBlockOnMount.ts`:
- Line 20: Update the consumedRef logic in useFocusBlockOnMount so it stores the
last processed focusBlock value rather than a boolean. Skip processing only when
the current value matches the stored value, then update the ref when handling a
new value while preserving React StrictMode duplicate-effect protection and
ensuring each new notification changes selectedPageId, scrolls, and calls
clearParam().
- Line 1: useFocusBlockOnMount の consumedRef を boolean の消費済みフラグから、直前に処理した
rawFocusBlock 値を保持する ref に変更し、値が変わった場合は再処理して selectedPageId
更新・scrollIntoView・query 掃除を実行できるようにする。同一マウント中に focusBlock=42 から 99
へ連続して変化した場合、2回目の処理結果を検証するテストを useFocusBlockOnMount の既存テストへ追加する。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56b64829-c350-4706-80b1-a943af8009c8

📥 Commits

Reviewing files that changed from the base of the PR and between e5527a0 and 6471a2b.

📒 Files selected for processing (12)
  • docs/memo/notification_roadmap.md
  • docs/notifications.md
  • frontend/public/firebase-messaging-sw.js
  • frontend/src/App.tsx
  • frontend/src/components/blocks/view/BlockScheduleView.tsx
  • frontend/src/components/blocks/view/BlockTransportationView.tsx
  • frontend/src/hooks/useBlocks.ts
  • frontend/src/hooks/useFcmNavigationListener.test.tsx
  • frontend/src/hooks/useFcmNavigationListener.ts
  • frontend/src/hooks/useFocusBlockOnMount.test.tsx
  • frontend/src/hooks/useFocusBlockOnMount.ts
  • frontend/src/pages/TripPage.tsx

Comment thread frontend/public/firebase-messaging-sw.js
Comment thread frontend/src/hooks/useFocusBlockOnMount.ts
Comment thread frontend/src/hooks/useFocusBlockOnMount.ts
@kuu13580
kuu13580 force-pushed the feature/issue207_sw-notificationclick-deeplink branch from 6471a2b to bb41626 Compare July 31, 2026 00:00
@kuu13580

Copy link
Copy Markdown
Contributor Author

@coderabbitai 指摘 3 件を fixup で反映しました。

  • SW handler 順序: notificationclick の addEventListener を importScripts / initializeApp / onBackgroundMessage より前に移動 (3259b71 にスカッシュ)
  • consumedRef の一度きりゲート問題: consumedKeyRef: useRef<string | null>(null) に変更し、直前に処理した rawFocusBlock の生値を保持。値が変わった時のみ再処理。deps に rawFocusBlock 追加
  • 連続 focusBlock のテスト追加: 同一マウント中に focusBlock=42 → 99 を SPA navigate 経由で切り替え、2 回目も selectedPageId 切替 + scrollIntoView + query 掃除が発火することを検証 (358897e にスカッシュ)

vitest 205 passed / type-check / lint / format 通過確認済み。

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Service worker outline =='
ast-grep outline frontend/public/firebase-messaging-sw.js --items all || true
echo '== Service worker event/Firebase ordering =='
rg -n -C 5 'notificationclick|importScripts|initializeApp|onBackgroundMessage' frontend/public/firebase-messaging-sw.js

echo '== Focus hook outline =='
ast-grep outline frontend/src/hooks/useFocusBlockOnMount.ts --items all || true
echo '== Focus hook consumption logic =='
rg -n -C 8 'consumed(Key)?Ref|rawFocusBlock|useEffect' frontend/src/hooks/useFocusBlockOnMount.ts

echo '== Regression test evidence =='
rg -n -C 10 '42|99|focusBlock|scrollIntoView|selectedPageId' frontend/src/hooks/useFocusBlockOnMount.test.tsx

Length of output: 12777


@kuu13580 確認しました。3 点とも修正内容と回帰テストが反映されています。

  • notificationclick は Firebase SDK の読み込み・初期化より前に登録されています。
  • consumedKeyRef は直前の rawFocusBlock 値を比較する方式となっており、別の値への SPA 遷移を再処理できます。
  • focusBlock=42 → 99 の連続遷移について、2 回目の page 切替・スクロール・query 除去を検証するテストも追加されています。

ご対応ありがとうございます。🐰


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

通知タップ時の挙動を Firebase デフォルトから明示ハンドラに置き換え、既存
PWA/tab を単一 tab のまま該当 block まで scroll できるようにする。

- SW: notificationclick で clients.matchAll → focused > visible > 任意
  の優先度で client を選び、postMessage で SPA navigate。無ければ
  clients.openWindow。別 origin URL は payload 経路含め弾く
- client 側: useFcmNavigationListener が SW message を受け React Router
  で navigate。WindowClient.navigate() のフルリロードを避けて atom /
  SWR / scroll 位置を保持
- useFocusBlockOnMount: ?focusBlock={id} を useBlock で解決 → pageId
  切替 → data-block-id の DOM を rAF 待機して scrollIntoView (center)
  → replaceState で query 除去。404 / 非数値ゴミは黙って掃除
- useBlocks: list fetch 時に個別 key を populate し useBlock の重複
  fetch を回避 (楽観更新値がある場合は skip して巻き戻し防止)
- View block 2 種に data-block-id を付与 (edit モードは対象外)
- useFcmNavigationListener: FCM_NAVIGATE 受信で navigate 呼び出し / 別
  origin & 非 http scheme & 型不正 payload の無視 / unmount cleanup /
  serviceWorker 未定義環境
- useFocusBlockOnMount: block ロード成功で selectedPageId 切替 +
  scrollIntoView + query 除去 / 404 → query 除去のみ / 非数値 → API
  も叩かず query 除去 / focusBlock 無 → no-op
- docs/notifications.md: §5 Deep link を実装済み記述に更新 (SW ハンドラ
  → SPA navigate → useFocusBlockOnMount → scrollIntoView の経路と、
  処理後の replaceState 除去まで明記)
- docs/memo/notification_roadmap.md: P6b を完了へ
@kuu13580
kuu13580 force-pushed the feature/issue207_sw-notificationclick-deeplink branch from bb41626 to c22d5ce Compare August 1, 2026 00:10
@kuu13580
kuu13580 marked this pull request as ready for review August 1, 2026 01:08
@kuu13580
kuu13580 requested a review from a team as a code owner August 1, 2026 01:08
@kuu13580
kuu13580 requested review from bobtaroh and koki-fore and removed request for a team August 1, 2026 01:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/hooks/useFocusBlockOnMount.ts`:
- Around line 41-44: Update the useFocusBlockOnMount hook so processing is
tracked per navigation event rather than only by rawFocusBlock, allowing a
second navigation to the same block ID to run page selection, scrolling, and
query removal again. Record the event as consumed only after its uncancelled
processing completes, and add a regression test covering 42 → query removal → 42
within the same mount.
- Around line 33-38: Update the focusBlock parsing in useFocusBlockOnMount so
only positive safe integers are accepted; treat 0, unsafe integers, and other
invalid values as invalid and remove the query parameter. Add a regression test
covering focusBlock=0, and keep useBlock’s existing falsy-key behavior
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fca4e6e-8b12-4300-80a1-a2793f56f227

📥 Commits

Reviewing files that changed from the base of the PR and between 6471a2b and c22d5ce.

📒 Files selected for processing (12)
  • docs/memo/notification_roadmap.md
  • docs/notifications.md
  • frontend/public/firebase-messaging-sw.js
  • frontend/src/App.tsx
  • frontend/src/components/blocks/view/BlockScheduleView.tsx
  • frontend/src/components/blocks/view/BlockTransportationView.tsx
  • frontend/src/hooks/useBlocks.ts
  • frontend/src/hooks/useFcmNavigationListener.test.tsx
  • frontend/src/hooks/useFcmNavigationListener.ts
  • frontend/src/hooks/useFocusBlockOnMount.test.tsx
  • frontend/src/hooks/useFocusBlockOnMount.ts
  • frontend/src/pages/TripPage.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • frontend/src/components/blocks/view/BlockScheduleView.tsx
  • docs/notifications.md
  • frontend/src/App.tsx
  • frontend/src/components/blocks/view/BlockTransportationView.tsx
  • docs/memo/notification_roadmap.md
  • frontend/src/hooks/useBlocks.ts
  • frontend/src/pages/TripPage.tsx
  • frontend/src/hooks/useFcmNavigationListener.ts
  • frontend/public/firebase-messaging-sw.js
  • frontend/src/hooks/useFcmNavigationListener.test.tsx

Comment thread frontend/src/hooks/useFocusBlockOnMount.ts
Comment thread frontend/src/hooks/useFocusBlockOnMount.ts
- focusBlock を正の safe integer のみ許可する parse に変更。regex を
  /^[1-9]\d*$/ + Number.isSafeInteger + >0 の三重チェックへ。0 が
  通ると useBlock(0) が SWR falsy key で fetch を止め ref も query
  も更新されずスタックしていた
- consumedKeyRef のセットを async の uncancelled 完了後に移動。
  StrictMode dev double-fire で最初の async が cancel された時に
  scroll が発火しなくなる問題を解消
- rawFocusBlock === null (query 掃除後) 時点で ref をリセット。同一
  block の再通知が同一マウント中に来ても再処理できるようにする
- 回帰テスト 2 種追加 (focusBlock=0 / 42 → clearParam → 42 再処理)
@kuu13580

kuu13580 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai 2 件の指摘を追加コミット (`bc92db0`) で反映しました。

  • `focusBlock=0` 弾き: parse を `/^[1-9]\d*$/` + `Number.isSafeInteger` + `>0` の三重チェックに変更。0 は `hasInvalidParam` パスに落ちて query 掃除だけ実行
  • 同 `focusBlock` 再通知の再処理:
    • `consumedKeyRef` セットを async の uncancelled 完了後 (`clearParam()` の後) に移動。StrictMode の double-fire で最初の async が cancel された時にスクロールが発火しない問題も同時に解消
    • `rawFocusBlock === null` (query 掃除完了後) の時点で ref をリセット。同一 block への再通知で 2 回目も page 切替・scroll・query 掃除を実行できる
  • 回帰テスト 2 種追加:
    • `?focusBlock=0`: API 未呼び出し + query 掃除のみ
    • `?focusBlock=42` → clearParam → 同 `?focusBlock=42` 再 navigate → 2 回目も `selectedPageId` 切替・`scrollIntoView`・query 掃除が発火

`vitest 207 passed` (新規 2) / `type-check` / `lint` / `format` 通過。

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@kuu13580 確認しました。

  • focusBlock=0 は無効な値として処理され、API 呼び出しをせずに query を削除します。
  • 同じ focusBlock の再通知でも、query 削除後に consumedKeyRef をリセットするため、2 回目の遷移・スクロールを実行できます。
  • StrictMode のキャンセル時は、完了していない処理を消費済みにしません。
  • 2 件の回帰テストも追加されています。

ご対応ありがとうございます。

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/hooks/useFocusBlockOnMount.test.tsx (1)

220-221: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

useBlocks の重複取得回避を検証してください。

Line 220 の MSW handler は同じ応答を複数回返します。リクエスト回数を記録していないため、2回目の focusBlock=42 処理が /blocks/42 を再取得しても、このテストは成功します。1回目の処理後の回数を保存し、2回目の処理後も増えないことを検証してください。

テスト修正案
+    let requestCount = 0;
     server.use(http.get('*/blocks/42', () => {
+      requestCount += 1;
       return HttpResponse.json(scheduleBlockJson(42, 7));
     }));
...
     expect(scrollIntoViewMock).toHaveBeenCalledTimes(1);
+    const requestCountAfterFirst = requestCount;
...
     await waitFor(() => {
       expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'center' });
     });
+    expect(requestCount).toBe(requestCountAfterFirst);

Also applies to: 254-278

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useFocusBlockOnMount.test.tsx` around lines 220 - 221,
Update the test around the MSW handler and the two focusBlock=42 flows to count
requests to /blocks/42, record the count after the first processing, and assert
that the count remains unchanged after the second processing. Preserve the
existing response setup while explicitly verifying useBlocks avoids the
duplicate fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/src/hooks/useFocusBlockOnMount.test.tsx`:
- Around line 220-221: Update the test around the MSW handler and the two
focusBlock=42 flows to count requests to /blocks/42, record the count after the
first processing, and assert that the count remains unchanged after the second
processing. Preserve the existing response setup while explicitly verifying
useBlocks avoids the duplicate fetch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 89d43864-3e49-4939-bf9c-23a5b56bf77e

📥 Commits

Reviewing files that changed from the base of the PR and between c22d5ce and bc92db0.

📒 Files selected for processing (2)
  • frontend/src/hooks/useFocusBlockOnMount.test.tsx
  • frontend/src/hooks/useFocusBlockOnMount.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/hooks/useFocusBlockOnMount.ts

useBlock(id) 単発 fetch → selectedPageId 切替の後、対象 page の
useBlocks(pageId) list fetch が終わるまで ViewTripLayout は Skeleton を
返し、data-block-id DOM がまだ存在しない。Cloud Run コールドスタート等で
list fetch が 3 秒を超えると rAF ポーリングが空振りして scroll が
発火しないケースがあった。

- rAF ポーリングを MutationObserver に変更。Skeleton が Timeline に
  差し替わった瞬間に即発火するようになり無駄回転をなくす
- timeout を 3s → 8s に延長 (staging 実測でも 3s 超えるケースを確認)
- 要素発見後に 1 rAF 挟んでから scrollIntoView。Timeline 差し替え直後は
  layout がまだ確定していないタイミングで scroll してもズレることがあった

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/hooks/useFocusBlockOnMount.ts`:
- Around line 22-43: Update the useFocusBlockOnMount flow, including
waitForBlockElement and scrollIntoViewOnNextFrame, to use cancellable cleanup
state that explicitly disconnects the MutationObserver, clears the timeout, and
cancels any pending requestAnimationFrame when focusBlock changes or the
component unmounts. Preserve the 42→99 transition behavior and add regression
coverage for that transition and unmount during waiting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 859de847-11cb-4e67-a98e-40088cc1ea8d

📥 Commits

Reviewing files that changed from the base of the PR and between bc92db0 and cf02eda.

📒 Files selected for processing (2)
  • frontend/src/hooks/useFocusBlockOnMount.test.tsx
  • frontend/src/hooks/useFocusBlockOnMount.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/hooks/useFocusBlockOnMount.test.tsx

Comment on lines +22 to +43
const waitForBlockElement = (blockId: number, maxMs: number, isCancelled: () => boolean): Promise<HTMLElement | null> =>
new Promise(resolve => {
const selector = `[data-block-id="${blockId}"]`;

const found = document.querySelector<HTMLElement>(selector);
if (found) return resolve(found);

let timeoutId: ReturnType<typeof setTimeout> | null = null;
const finish = (el: HTMLElement | null) => {
observer.disconnect();
if (timeoutId !== null) clearTimeout(timeoutId);
resolve(el);
};

const observer = new MutationObserver(() => {
if (isCancelled()) return finish(null);
const el = document.querySelector<HTMLElement>(selector);
if (el) finish(el);
});
observer.observe(document.body, { childList: true, subtree: true });
timeoutId = setTimeout(() => finish(null), maxMs);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching hook:"
git ls-files | rg '(^|/)useFocusBlockOnMount\.(ts|tsx|js|jsx)$|useFocusBlockOnMount' || true

file="$(git ls-files | rg '(^|/)useFocusBlockOnMount\.(ts|tsx|js|jsx)$' | head -n 1 || true)"
if [ -n "$file" ]; then
  echo
  echo "File: $file"
  wc -l "$file"
  echo
  sed -n '1,180p' "$file" | cat -n
fi

Repository: tabi-bit/tabi-share

Length of output: 5538


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate tests:"
git ls-files | rg '(^|/)useFocusBlockOnMount\.test\.(tsx|ts|jsx|js)$' || true

test_file="$(git ls-files | rg '(^|/)useFocusBlockOnMount\.test\.(tsx|ts|jsx|js)$' | head -n 1 || true)"
if [ -n "$test_file" ]; then
  echo
  echo "File: $test_file"
  wc -l "$test_file"
  echo
  sed -n '1,260p' "$test_file" | cat -n
fi

echo
echo "Behavioral probe of current resource lifecycle based on static source:"
python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/hooks/useFocusBlockOnMount.ts')
s = p.read_text()
checks = {
    'cleanup_only_sets_cancelled': 'return () => {\n      cancelled = true;\n    }' in s,
    'observer_disconnected_in_finish': 'observer.disconnect()' in s,
    'observer_callback_calls_isCancelled': 'if (isCancelled()) return finish(null)' in s,
    'timeout_cleared_in_finish': 'clearTimeout(timeoutId)' in s,
    'raf_returns_id': 'const ' in s and 'requestAnimationFrame' in s and 'frameId' in s,
    'cleanup_releases_obs_or_timeout_or_raf': any(x in 'return () => {\n      cancelled = true;\n    }' for x in ['observer.disconnect','clearTimeout','clearAnimationFrame','frameId']),
    'has_cancel_handle_param_to_async_path': 'AbortSignal' in s or 'oncancel' in s or 'cancel' in s,
    'tests_mention_could_retain_rare': bool((p.parent / f'{p.stem}.test.tsx').read_text() if (p.parent / f'{p.stem}.test.tsx').exists() else ''),
}
# Inspect exact lines by counting before/after.
lines = s.splitlines()
for i, line in enumerate(lines, 1):
    if 'function waitForBlockElement' in line or 'const waitForBlockElement' in line:
        print('waitForBlockElement starts at line', i)
        for n,l in enumerate(lines[max(1,i-5):min(len(lines),i+25)], i-5):
            if n <= len(lines)+5:
                print(f'{n}: {l}')
        break
for k,v in checks.items():
    print(f'{k}: {v}')
PY

Repository: tabi-bit/tabi-share

Length of output: 11761


cleanup で待機中の observer、timer、rAF を解放してください。

cleanupcancelled = true までの設定だけで、待機中の MutationObserver と 8 秒タイマーは明示的に解放されません。scrollIntoViewOnNextFramerequestAnimationFrame の ID を保持しないため、focusBlock が変わると旧ブロックの scroll 処理が残る可能性があります。AbortSignal など cancel 可能なフローへ移し、cleanup で observer、timer、rAF を停止してください。対応後の 42 → 99 遷移・アンマウント中の回帰テストも追加してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useFocusBlockOnMount.ts` around lines 22 - 43, Update the
useFocusBlockOnMount flow, including waitForBlockElement and
scrollIntoViewOnNextFrame, to use cancellable cleanup state that explicitly
disconnects the MutationObserver, clears the timeout, and cancels any pending
requestAnimationFrame when focusBlock changes or the component unmounts.
Preserve the 42→99 transition behavior and add regression coverage for that
transition and unmount during waiting.

現状は SW → client の postMessage 1 本に依存しており、以下 2 パターンで
navigate が発火せず PWA が前面化したまま URL が変わらない事故になる:

- Android で PWA が freeze/kill され、matchAll は phantom client を
  返すが event loop で message が drain されない
- cold start で client 側 message listener 登録より前に SW が
  postMessage を送出

Cache Storage を safety net にして両方救う。

- SW: 通知タップの target URL を Cache に put してから postMessage/
  openWindow へ進む
- Client: useFcmNavigationListener を mount + visibilitychange で
  Cache 内 intent を消化する経路に拡張。fast path (message) と safety
  net (cache) の二重発火を lastNavigatedRef で dedupe
- 現在 URL と一致するときは navigate せず、履歴汚染を避ける
- Cache API 非対応環境 / QuotaExceeded は best effort として無視

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/src/components/DebugLogPanel.tsx (1)

59-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

不要なコメントを削除してください。

Line 59 の説明は、直後の window.prompt('コピーしてください', logs) と分岐条件から判断できます。

コーディングガイドラインに基づき、**/*.{js,ts,jsx,tsx} では「Add comments in Japanese, but only when the intent cannot be read from the code itself, keeping them to a minimum」です。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/DebugLogPanel.tsx` at line 59, DebugLogPanel
のクリップボード拒否時のフォールバック処理にある冗長な日本語コメントを削除し、直後の window.prompt
呼び出しと分岐条件だけで意図が伝わる状態にしてください。

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/DebugLogPanel.tsx`:
- Around line 71-75: DebugLogPanel の onCopy および onClear
ボタンに表示しているユーザー向け英語テキストを日本語へ変更し、UI 表記を日本語に統一してください。

In `@frontend/src/hooks/useFcmNavigationListener.ts`:
- Around line 69-100: Update navigateOnce in useFcmNavigationListener so
lastNavigatedRef does not permanently suppress later notifications for the same
target. Clear the reference shortly after navigate(target) completes, or reset
it when the current URL matches the target, while preserving duplicate
suppression for the immediate message/cache delivery.

In `@frontend/src/lib/debugLogger.ts`:
- Around line 108-115: Update frontend/src/lib/debugLogger.ts lines 108-115 in
clearAllLogs to await the IndexedDB transaction’s completion handler and reject
on error or abort, resolving only after clear succeeds; update
frontend/src/components/DebugLogPanel.tsx lines 64-67 in onClear to show the
success toast only when clearAllLogs completes successfully, while preserving
failure handling.

---

Nitpick comments:
In `@frontend/src/components/DebugLogPanel.tsx`:
- Line 59: DebugLogPanel のクリップボード拒否時のフォールバック処理にある冗長な日本語コメントを削除し、直後の
window.prompt 呼び出しと分岐条件だけで意図が伝わる状態にしてください。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dedebb1b-c48e-403b-957a-f2165449e419

📥 Commits

Reviewing files that changed from the base of the PR and between cf02eda and 71cb265.

📒 Files selected for processing (7)
  • frontend/public/firebase-messaging-sw.js
  • frontend/src/App.tsx
  • frontend/src/components/DebugLogPanel.tsx
  • frontend/src/hooks/useFcmNavigationListener.test.tsx
  • frontend/src/hooks/useFcmNavigationListener.ts
  • frontend/src/hooks/useFocusBlockOnMount.ts
  • frontend/src/lib/debugLogger.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/App.tsx
  • frontend/src/hooks/useFocusBlockOnMount.ts

Comment on lines +71 to +75
<button type='button' onClick={onCopy} className='cursor-pointer rounded bg-teal-600 px-2 py-1 hover:bg-teal-500'>
Copy Logs
</button>
<button type='button' onClick={onClear} className='cursor-pointer rounded bg-red-600 px-2 py-1 hover:bg-red-500'>
Clear

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

ボタン表示を日本語にしてください。

Line 72 の Copy Logs と Line 75 の Clear はユーザー向け UI テキストです。日本語表示に統一してください。

修正例
-        Copy Logs
+        ログをコピー
...
-        Clear
+        ログをクリア

コーディングガイドラインに基づき、**/*.{json,md,ts,tsx,js,jsx} の UI テキストは日本語で記述してください。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button type='button' onClick={onCopy} className='cursor-pointer rounded bg-teal-600 px-2 py-1 hover:bg-teal-500'>
Copy Logs
</button>
<button type='button' onClick={onClear} className='cursor-pointer rounded bg-red-600 px-2 py-1 hover:bg-red-500'>
Clear
<button type='button' onClick={onCopy} className='cursor-pointer rounded bg-teal-600 px-2 py-1 hover:bg-teal-500'>
ログをコピー
</button>
<button type='button' onClick={onClear} className='cursor-pointer rounded bg-red-600 px-2 py-1 hover:bg-red-500'>
ログをクリア
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/DebugLogPanel.tsx` around lines 71 - 75,
DebugLogPanel の onCopy および onClear ボタンに表示しているユーザー向け英語テキストを日本語へ変更し、UI
表記を日本語に統一してください。

Source: Coding guidelines

Comment on lines +69 to +100
export const useFcmNavigationListener = () => {
const navigate = useNavigate();
// fast path (message) と safety net (cache) が同一 URL に対して二重発火するのを防ぐ。
// 直近の遷移先を保持し、同値なら skip する。
const lastNavigatedRef = useRef<string | null>(null);

useEffect(() => {
let cancelled = false;

const navigateOnce = (target: string, source: string) => {
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (target === current) {
void debugLog('CL', 'navigateOnce skip (== current)', { target, source });
return;
}
if (lastNavigatedRef.current === target) {
void debugLog('CL', 'navigateOnce skip (== last)', { target, source });
return;
}
lastNavigatedRef.current = target;
void debugLog('CL', 'navigate() called', { target, source });
navigate(target);
};

const applyIntent = async () => {
const url = await readAndClearPendingIntent();
void debugLog('CL', 'applyIntent', { url });
if (cancelled || !url) return;
const target = parseSameOriginPath(url);
if (!target) return;
navigateOnce(target, 'intent');
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

lastNavigatedRefが永続的に残るため、同一URLへの再通知を処理できません。

navigateOncelastNavigatedRef.current === targetのとき常にnavigateをスキップします。この参照はフックのライフタイム全体で保持され、成功したnavigate以外でクリアされません。

以下の手順で再通知が失われます。

  1. 通知Aで/trip/xyz?focusBlock=7へnavigateし、lastNavigatedRefに同じ値がセットされる。
  2. TripPage側でクエリが除去され、現在URLは/trip/xyzになる(lastNavigatedRefは更新されない)。
  3. ユーザーが他ページへ移動する。
  4. 後日、同じblockに対する通知が再送され、同じURL(/trip/xyz?focusBlock=7)がmessageまたはcache intentとして届く。
  5. target !== currentだがlastNavigatedRef.current === targetが真のため、navigate()が呼ばれない。

PR概要には「query除去後に状態をリセットし、同じblockへの再通知を再処理」という要件がありますが、この重複排除ロジックが本フックの段階でnavigate自体を止めてしまうため、下流のblock選択ロジックまで再通知が到達しません。

navigate()呼び出し後に短時間でlastNavigatedRefをクリアするか、currenttargetが一致した時点でlastNavigatedRefもリセットしてください。

🐛 修正案
     const navigateOnce = (target: string, source: string) => {
       const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
       if (target === current) {
         void debugLog('CL', 'navigateOnce skip (== current)', { target, source });
+        // 現在地と一致した時点で dedupe 参照もクリアし、将来の同一URLへの再通知を処理可能にする
+        lastNavigatedRef.current = null;
         return;
       }
       if (lastNavigatedRef.current === target) {
         void debugLog('CL', 'navigateOnce skip (== last)', { target, source });
         return;
       }
       lastNavigatedRef.current = target;
       void debugLog('CL', 'navigate() called', { target, source });
       navigate(target);
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useFcmNavigationListener.ts` around lines 69 - 100, Update
navigateOnce in useFcmNavigationListener so lastNavigatedRef does not
permanently suppress later notifications for the same target. Clear the
reference shortly after navigate(target) completes, or reset it when the current
URL matches the target, while preserving duplicate suppression for the immediate
message/cache delivery.

Comment thread frontend/src/lib/debugLogger.ts Outdated
Comment on lines +108 to +115
export const clearAllLogs = async (): Promise<void> => {
try {
const db = await getDb();
const tx = db.transaction(STORE_NAME, 'readwrite');
tx.objectStore(STORE_NAME).clear();
} catch {
// ignore
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Locate files:\n'
git ls-files | rg 'frontend/src/(lib/debugLogger\.ts|components/DebugLogPanel\.tsx)$|frontend/src/lib/debugLogger|frontend/src/components/DebugLogPanel' || true

if [ -f frontend/src/lib/debugLogger.ts ]; then
  printf '\nFile outline:\n'
  ast-grep outline frontend/src/lib/debugLogger.ts || true
  printf '\nRelevant debugLogger.ts lines:\n'
  sed -n '1,160p' frontend/src/lib/debugLogger.ts | nl -ba
fi

if [ -f frontend/src/components/DebugLogPanel.tsx ]; then
  printf '\nRelevant DebugLogPanel.tsx lines:\n'
  sed -n '1,110p' frontend/src/components/DebugLogPanel.tsx | nl -ba
fi

printf '\nSearch clearAllLogs usages:\n'
rg -n "clearAllLogs|toast\.success\(|toast\.error|onClear" frontend/src || true

# Deterministic/semantic behavior probe: parse the function and check whether any await/completion handlers occur after transaction start or whether transaction abort/error paths are reachable from clear().
python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/lib/debugLogger.ts')
if p.exists():
    text = p.read_text()
    start = text.find('export const clearAllLogs')
    end = text.find('\n};', start)
    body = text[start:end+3] if end != -1 else ''
    print('clearAllLogs contains clear():', 'clear()' in body)
    print('clearAllLogs contains tx.await/oncomplete/onabort/onerror wait:', any(s in body for s in ['oncomplete', 'onabort', 'onerror', 'addEventListener'])))
    print('clearAllLogs catches:', body.find('catch') != -1)
    print('clearAllLogs returns boolean-like indicators:', bool(set(('true', 'false')).intersection(body.split())) )
PY

Repository: tabi-bit/tabi-share

Length of output: 928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant debugLogger.ts lines:\n'
sed -n '1,150p' frontend/src/lib/debugLogger.ts | awk '{ printf "%4d\t%s\n", NR, $0 }'

printf '\nRelevant DebugLogPanel.tsx lines:\n'
sed -n '1,100p' frontend/src/components/DebugLogPanel.tsx | awk '{ printf "%4d\t%s\n", NR, $0 }'

printf '\nUsages:\n'
rg -n 'clearAllLogs|clear\(|transaction\(|onComplete|oncomplete|onabort|onerror|toast\.success|toast\.error' frontend/src || true

printf '\nStatic check of clearAllLogs body:\n'
python3 - <<'PY'
from pathlib import Path
text = Path('frontend/src/lib/debugLogger.ts').read_text()
start = text.find('export const清')
print('contains clearAllLogs:', 'export const clearAllLogs' in text)
# simple extraction until next exported const/function at same indentation is hard in TS, so just print signature/body markers
for marker in ['export const clearAllLogs', 'getDb()', 'db.transaction', 'objectStore(STORE_NAME).clear()', 'oncomplete', 'onabort', 'onerror', 'catch {']:
    print(f'{marker}: {marker in text}')
PY

Repository: tabi-bit/tabi-share

Length of output: 10464


削除完了を確認してから成功を表示してください。

clearAllLogsclear() を開始した後、トランザクションの完了結果を待たずに Promise<void> を解決します。IndexedDB が失敗・abort した場合でも DebugLogPanelonClear は成功トーストを表示します。clearAllLogs の完了ハンドラまで待機し、成功時だけ成功トーストを表示してください。

📍 Affects 2 files
  • frontend/src/lib/debugLogger.ts#L108-L115 (this comment)
  • frontend/src/components/DebugLogPanel.tsx#L64-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/lib/debugLogger.ts` around lines 108 - 115, Update
frontend/src/lib/debugLogger.ts lines 108-115 in clearAllLogs to await the
IndexedDB transaction’s completion handler and reject on error or abort,
resolving only after clear succeeds; update
frontend/src/components/DebugLogPanel.tsx lines 64-67 in onClear to show the
success toast only when clearAllLogs completes successfully, while preserving
failure handling.

kuu13580 added a commit that referenced this pull request Aug 1, 2026
実機ログで SW ハンドラが全く発火せず Firebase SDK デフォルト click 処理
だけが動く現象が発生していたが、原因は VitePWA が firebase-messaging-sw.js
を precache していたことにあった。register 時に Chrome が cache 経由で
古い版を fetch → 古い SW が active → いくら deploy しても新版に切り替
わらない。

- vite.config.ts の workbox に globIgnores + navigateFallbackDenylist で
  firebase-messaging-sw.js を除外
- firebase.json で /firebase-messaging-sw.js に Cache-Control: no-cache
  ヘッダを追加。Firebase Hosting のデフォルト max-age=3600 も抑える
- 診断: DEBUG_LOG_VERSION を v03 に bump、client 側で
  getRegistrations() を呼び出して全 SW の scope / active / waiting /
  installing の scriptURL をログ出力

feature branch (#209) に同等の修正を反映する前に、まず diagnostic
branch で実機挙動確認する。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

通知タップで該当 trip / page / block へ遷移させる (SW notificationclick)

1 participant