Skip to content

課金機能の実装#203

Merged
ryuuhei0729 merged 37 commits into
mainfrom
stripe
Apr 8, 2026
Merged

課金機能の実装#203
ryuuhei0729 merged 37 commits into
mainfrom
stripe

Conversation

@ryuuhei0729
Copy link
Copy Markdown
Owner

@ryuuhei0729 ryuuhei0729 commented Mar 14, 2026

…imatch to version 10.2.4, and add undici and esbuild with minimum versions. Enhance vitest configuration for shared components by adding React aliases to prevent multiple instances. Introduce new authentication hooks and types for shared use across applications. Refactor calendar data hooks to remove unnecessary user authentication checks. Update SettingsClient to handle Stripe session verification after checkout. Add a link to the specific commercial transaction law in the About page and footer for compliance.

概要

変更内容

  • [ ]
  • [ ]
  • [ ]

テスト

  • 単体テストが通る
  • E2Eテストが通る
  • カバレッジが基準を満たしている

チェックリスト

  • コードの品質チェックが完了している
  • ドキュメントの更新が必要な場合は更新済み
  • 破壊的変更がある場合は適切にマークしている
  • セキュリティ上の問題がないか確認済み

スクリーンショット

関連Issue

Closes #

Summary by CodeRabbit

Release Notes

  • New Features

    • 記録と練習ログへの動画アップロード・再生機能を追加
    • Premium機能による分割時間、練習時間、画像・動画アップロード制限を実装
  • Bug Fixes

    • フリープランユーザー向けの機能制限を改善
  • Chores

    • アプリバージョンを1.3.0から1.4.0に更新
    • ワークフロータイムアウト設定を調整

…imatch to version 10.2.4, and add undici and esbuild with minimum versions. Enhance vitest configuration for shared components by adding React aliases to prevent multiple instances. Introduce new authentication hooks and types for shared use across applications. Refactor calendar data hooks to remove unnecessary user authentication checks. Update SettingsClient to handle Stripe session verification after checkout. Add a link to the specific commercial transaction law in the About page and footer for compliance.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

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

このプルリクエストは、モバイルアプリにビデオアップロード・再生機能を追加し、プレミアム機能ゲーティング(画像/ビデオアップロード、スプリットタイム、練習タイム数制限)を実装。スキーマ更新によりvideo_urlvideo_path/video_thumbnail_pathに置き換え、複数のスクリーン・フォームコンポーネントを更新して新機能を統合します。

Changes

Cohort / File(s) Summary
CI/CDワークフロー更新
.github/workflows/ci.yml, .github/workflows/supabase.yml
Supabaseスタートアップコマンドに--exclude edge-runtimeフラグを追加、タイムアウトを5分から8分に延長。停止ステップにcontinue-on-error: trueを設定し、失敗時も継続するように変更。
ビデオプレイヤー基盤
apps/mobile/components/shared/VideoPlayer.tsx, apps/mobile/components/shared/VideoUploader.tsx, apps/mobile/hooks/useVideoPresignedUrl.ts
ビデオ再生・アップロード機能の新規実装。VideoPlayerは事前署名URLを取得・再生し、タムネイル表示と全画面対応。VideoUploaderは選択・アップロード・削除フロー、プレミアムゲーティング。useVideoPresignedUrlは署名URL取得と自動更新。
プレミアム機能ゲーティング
apps/shared/constants/premium.ts, apps/shared/utils/premium.ts, apps/shared/utils/validators.ts, apps/mobile/components/shared/PremiumBadge.tsx
プレミアム購読チェック、プラン制限定数、ゲーティングロジック実装。PremiumBadgeコンポーネントで制限メッセージを表示。フル/コンパクト表示レイアウト対応。
ビデオアップロード基盤
apps/mobile/utils/videoUpload.ts
R2へのビデオ・タムネイル署名付きアップロード、URL生成、削除、確認API統合。進捗コールバック、エラーハンドリング対応。
スキーマ・型定義更新
apps/shared/types/supabase-schema.ts, apps/shared/types/record.ts, apps/shared/types/practice.ts, apps/shared/types/auth.ts, apps/shared/types/ui.ts, apps/mobile/components/calendar/DayDetailModal/types.ts
video_urlvideo_path/video_thumbnail_pathに統一。認証基本型BaseAuthState/BaseAuthActions追加、UserPlan型導入。SubscriptionInfo.planUserPlanに変更。
レコード表示スクリーン更新
apps/mobile/screens/RecordDetailScreen.tsx, apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx
ビデオ再生セクション追加、VideoPlayer統合。スプリットタイム表示条件をDB提供分に変更。Supabaseクエリにvideo_path/video_thumbnail_path含む。
フォームスクリーン - ビデオ対応
apps/mobile/screens/RecordFormScreen.tsx, apps/mobile/screens/RecordLogFormScreen.tsx, apps/mobile/screens/PracticeLogFormScreen.tsx
VideoUploader統合、ビデオ状態管理、プレミアムゲーティング。スプリットタイム制限、編集モード対応、フォーム保存時の処理更新。
フォームスクリーン - プレミアムゲーティング
apps/mobile/screens/PracticeFormScreen.tsx, apps/mobile/screens/CompetitionBasicFormScreen.tsx, apps/mobile/screens/PracticeTimeFormScreen.tsx
プレミアム判定に基づき、画像アップロードUI/練習タイム数上限UI を条件付き表示。非プレミアムユーザーにPremiumBadge表示。
練習ログ表示
apps/mobile/components/practices/PracticeLogItem.tsx
ビデオセクション条件付きレンダリング、VideoPlayer統合。
共有フック・インデックス
apps/shared/hooks/useAuthState.ts, apps/shared/hooks/index.ts
認証状態管理フック新規実装、セッション/ユーザー/ローディング状態管理。Supabase認証イベントリスナー及びフォールバック。
画像アップロード機能更新
apps/mobile/utils/imageUpload.ts
プレミアム判定パラメータ追加、プレミアムゲーティング制御フロー実装。
テスト更新
apps/shared/__mocks__/supabase.ts, apps/shared/__tests__/api/records.test.ts, apps/shared/__tests__/hooks/queries/records.test.ts, apps/shared/__tests__/hooks/useRecords.test.tsx, apps/shared/__tests__/premium.test.ts, apps/web/__tests__/components/forms/RecordForm.test.tsx, apps/web/__tests__/contexts/CalendarProvider.test.tsx, apps/web/__tests__/hooks/useRecordForm.test.ts
スキーマ変更対応(video_urlvideo_path/video_thumbnail_path)、プレミアム検証テスト新規追加、モック更新、テスト依存パラメータ更新。
共有テスト設定
apps/shared/vitest.config.ts
defineConfig構成に変更、グローバルテスト設定追加、React/ReactDOMエイリアス設定、esbuildターゲット指定。
パッケージ・構成更新
apps/mobile/package.json, apps/mobile/app.config.js, apps/mobile/app.json, .gitignore
expo-video/expo-video-thumbnails依存追加(v3.0.16/v10.0.8)、React 19.1.4更新、RevenueCat iOS APIキー設定、バージョン1.3.0 → 1.4.0、iOSビルド番号・Androidバージョンコード削除、.turbo//node_modules/をgitignore追加。

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant VideoUploader as VideoUploader<br/>Component
    participant Selector as ImagePicker<br/>(Expo)
    participant Validator as Validator
    participant API as Upload API
    participant R2 as R2 Storage
    participant Supabase as Supabase DB

    User->>VideoUploader: Select/Record Video
    VideoUploader->>Selector: Pick from camera/library
    Selector-->>VideoUploader: Return video URI + metadata
    VideoUploader->>Validator: Check file size & auth
    alt isPremium = false & uploadState = idle
        Validator-->>VideoUploader: Show PremiumBadge
    else Valid & id exists
        Validator-->>VideoUploader: Proceed to upload
        VideoUploader->>API: Request presigned URLs<br/>(video + thumbnail)
        API-->>VideoUploader: Return signed URLs
        VideoUploader->>Selector: Generate thumbnail (0s frame)
        Selector-->>VideoUploader: Thumbnail URI
        par
            VideoUploader->>R2: PUT video to signed URL<br/>(with progress)
            VideoUploader->>R2: PUT thumbnail to signed URL
        and
            R2-->>VideoUploader: Upload complete
        end
        VideoUploader->>API: Confirm upload<br/>(videoPath, thumbnailPath)
        API->>Supabase: Update record/practice_log<br/>with video paths
        Supabase-->>API: Success
        API-->>VideoUploader: onUploadComplete callback
        VideoUploader->>VideoUploader: Render VideoPlayer<br/>with saved paths
    else id missing
        VideoUploader->>VideoUploader: Store pendingVideoUri<br/>Show "upload on save"
    end
Loading
sequenceDiagram
    participant User
    participant VideoPlayer as VideoPlayer<br/>Component
    participant URLHook as useVideoPresignedUrl<br/>Hook
    participant API as Get Presigned URL API
    participant Auth as Auth System
    participant R2 as R2 Storage
    participant ExpoVideo as expo-video<br/>Library

    User->>VideoPlayer: Mount with videoPath
    VideoPlayer->>URLHook: Call hook(videoPath)
    URLHook->>Auth: Get access_token
    Auth-->>URLHook: Return token
    URLHook->>API: getPresignedUrl<br/>(videoPath, access_token)
    API->>R2: Get signed streaming URL
    R2-->>API: Presigned URL
    API-->>URLHook: Return {videoUrl, thumbnailUrl}
    URLHook-->>VideoPlayer: {videoUrl, thumbnailUrl, isLoading}
    
    alt isLoading
        VideoPlayer->>VideoPlayer: Render ActivityIndicator
    else error exists
        VideoPlayer->>VideoPlayer: Render error + retry button
    else URLs ready
        VideoPlayer->>VideoPlayer: Create expo-video player
        VideoPlayer->>ExpoVideo: useVideoPlayer(videoUrl)
        ExpoVideo-->>VideoPlayer: Player instance
        VideoPlayer->>VideoPlayer: Show thumbnail overlay<br/>+ play button
        
        User->>VideoPlayer: Tap play button
        VideoPlayer->>ExpoVideo: player.replay()
        ExpoVideo-->>VideoPlayer: Playback started
        VideoPlayer->>VideoPlayer: Hide thumbnail
        
        User->>VideoPlayer: Tap fullscreen
        VideoPlayer->>ExpoVideo: enterFullscreen()
        ExpoVideo-->>VideoPlayer: Fullscreen active
        
        User->>VideoPlayer: Exit fullscreen
        VideoPlayer->>ExpoVideo: Pause & seek back<br/>to thumbnail state
        ExpoVideo-->>VideoPlayer: Paused, reset position
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #136: モバイルのDayDetailModal/RecordDetailコンポーネント、関連型・エクスポートを同じファイルで変更しており、コード層で直結している。
  • PR #94: PracticeLogFormScreen.tsxを同じく修正—主PRはビデオアップロード・プレミアムゲーティング追加、該当PRはswim_category追加—同一ファイルでのコード関連性あり。
  • PR #65: モバイルスキャフォルド導入後、apps/mobile/apps/sharedコンポーネント・フック・型を複数拡張しており、コード層で関連性が深い。

Poem

🐰 ビデオ飛ぶ、録画ポンッ!プレミアムの扉も開く
スプリット数も制限めく、でもプレミアムなら無制限ぴょん
R2へアップロード、署名URLでセキュア、再生すればスムース〜ん

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR説明はテンプレートの英語記述を含みテンプレート形式は維持されていますが、チェックリスト項目がすべて未入力で、実装内容の詳細説明が不足しており、変更内容・テスト結果・確認項目が具体的に記載されていません。 PR説明のチェックリスト項目を詳細に埋め、変更内容(ビデオ機能、プレミアム機能ゲーティング、Stripe統合、スキーマ変更など)、テスト実行結果、セキュリティ確認を具体的に記載してください。
Docstring Coverage ⚠️ Warning Docstring coverage is 66.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed タイトル「課金機能の実装」はPRの主要な変更内容を正確に反映しており、プレミアム機能(ビデオアップロード、分割タイム制限など)と課金システム(Stripe統合)の実装を簡潔に表現しています。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch stripe

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 and usage tips.

ryuuhei0729 and others added 9 commits March 15, 2026 00:50
The lockfile had a missing entry for workerd@1.20260310.1 after merging
main into stripe, causing pnpm install --frozen-lockfile to fail in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…date tests for authentication failure scenarios. The initial calendar items and monthly summary now default to empty states if not provided. Test cases updated to reflect API call failures when user authentication is lost.
- Integrated premium checks for image uploads and practice time limits in CompetitionBasicFormScreen, PracticeFormScreen, and RecordFormScreen.
- Added PremiumBadge components to notify users of premium features in various forms.
- Implemented validation logic for split time limits in RecordLogForm and RecordForm, ensuring free users are informed of their limits.
- Updated image upload utility to enforce premium restrictions.
- Enhanced user experience by providing clear messaging for premium features across the application.
…ser handling and loading states

- Updated PracticeDetails to use userId instead of user directly for filtering practice logs in team practices.
- Enhanced SettingsClient to refresh subscription on mount and added loading state handling for subscription settings.
- Improved user experience in SubscriptionSettings by displaying a skeleton loader while subscription data is being fetched.
- Mocked useAuth in RecordForm tests to simulate user authentication without AuthProvider.
- Updated useRecordForm tests to include isPremium flag, allowing for testing without free restrictions on split time additions.
…ring Supabase stack startup

- Modified the Supabase stack startup command in both ci.yml and supabase.yml to exclude edge-runtime, enhancing stability for end-to-end tests.
- Increased timeout for the startup process to 8 minutes to accommodate potential delays.
- Updated PremiumBadge component to link directly to the subscription page instead of settings.
- Added DAILY_TOKEN_LIMIT to free plan limits for better token management.
- Refined subscription type definitions to include a UserPlan type for better clarity.
- Adjusted logic in premium utility functions to enforce stricter limits on split and practice times for free users.
- Enhanced subscription status handling in the SubscriptionSettings component to display payment failure messages.
- Updated Stripe webhook handlers to improve customer ID management and session validation.
- Added a new pricing page with detailed plans and features for free and premium users.
- Improved sitemap to include the new pricing page for better SEO.
@ryuuhei0729 ryuuhei0729 changed the title Update dependencies in package.json and pnpm-lock.yaml, including min… 課金機能の実装 Mar 22, 2026
ryuhei.hosoi and others added 4 commits March 22, 2026 18:32
コンフリクト解決: page.tsxはstripeの料金カードを採用、
PracticeMenuItemとRecordLogEntryはmainのレスポンシブクラスを採用しつつ
stripeのisSplitTimeLimitReachedによるプレミアム制限を維持。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xml-parser to >=5.5.7 and flatted to >=3.4.2 for improved compatibility; modify path-expression-matcher to version 1.2.0. Enhance CI workflows by adding continue-on-error for stopping Supabase stack.
… version

- Modified billing.spec.ts to create a new browser context for unauthenticated requests, ensuring accurate testing of subscription status API.
- Updated the expected response status for invalid priceId in checkout API to allow for both 400 and 500 responses in CI environments.
- Enhanced visibility checks in settings page tests to include subscription-related text.
- Updated Stripe webhook handler to use the latest API version (2025-02-24.acacia) for improved compatibility.
@ryuuhei0729
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/web/app/page.tsx (1)

139-145: ⚠️ Potential issue | 🟡 Minor

sm:whitespace-nowrap が中間幅で本文をはみ出させます

日本語の長文を sm から折り返し禁止にしているので、640px 前後の幅で各行がコンテナに収まらず横スクロールや見切れが起きやすいです。通常の折り返しのままにするか、必要なら text-balance などで整えた方が安全です。

📱 提案差分
-            <p className="text-lg text-gray-600 sm:whitespace-nowrap">
+            <p className="text-lg text-gray-600">
-            <p className="text-lg text-gray-600 sm:whitespace-nowrap">
+            <p className="text-lg text-gray-600">
-            <p className="text-lg text-gray-600 sm:whitespace-nowrap">
+            <p className="text-lg text-gray-600">

As per coding guidelines, 'Implement responsive design in UI components'.

Also applies to: 193-201, 249-253

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/page.tsx` around lines 139 - 145, The paragraph element using
className "text-lg text-gray-600 sm:whitespace-nowrap" prevents wrapping at
small/medium screens and causes overflow; remove the responsive nowrap utility
(sm:whitespace-nowrap) from the <p> element(s) in page.tsx (and the other
occurrences noted) or replace it with a responsive-friendly alternative such as
a balancing utility (e.g., add a "text-balance" class) so the Japanese copy
wraps naturally and doesn't overflow the container.
apps/mobile/screens/CompetitionBasicFormScreen.tsx (1)

1-1: ⚠️ Potential issue | 🟡 Minor

"use client" ディレクティブは React Native ファイルでは不要です。

"use client" は Next.js App Router のディレクティブであり、React Native/Expo モバイルアプリには適用されません。このディレクティブは誤って追加された可能性があります。

🔧 修正案
-"use client";
-
 import React, { useState, useEffect, useCallback, useRef } from "react";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/screens/CompetitionBasicFormScreen.tsx` at line 1, Remove the
unnecessary Next.js directive "use client" from the top of
CompetitionBasicFormScreen.tsx; this is not applicable to React Native/Expo
files—open the CompetitionBasicFormScreen component file, delete the standalone
"use client" line (the exact string shown in the diff), and ensure no other
Next.js-only directives remain at the top of the file.
apps/mobile/screens/PracticeTimeFormScreen.tsx (1)

190-215: ⚠️ Potential issue | 🟡 Minor

無料枠の最後の入力が next のままです。

無料ユーザーで上限より多いセルがある場合、最後に編集できるセルでも returnKeyType"next" のままで、focusNextInput() は次の disabled セルへ進もうとします。上限到達時の Enter でフォーカス遷移が詰まるので、最後の編集可能セルは "done" にしてください。

💡 修正例
-                  const isLastInput = globalIndex === setCount * repCount - 1;
+                  const lastEditableIndex = isPremium
+                    ? setCount * repCount - 1
+                    : Math.min(setCount * repCount, FREE_PLAN_LIMITS.PRACTICE_TIMES_PER_LOG) - 1;
+                  const isLastInput = globalIndex === lastEditableIndex;
@@
                         onSubmitEditing={() => {
                           handleTimeConfirm(timeEntry.id, timeEntry.displayValue || "");
-                          focusNextInput(globalIndex);
+                          if (!isLastInput) {
+                            focusNextInput(globalIndex);
+                          }
                         }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/screens/PracticeTimeFormScreen.tsx` around lines 190 - 215, The
returnKeyType is computed only by isLastInput, so for free users the last
editable cell (before disabled cells) still shows "next" and focusNextInput()
moves into disabled inputs; compute the last editable index and use that to set
returnKeyType and whether to call focusNextInput. Specifically, derive
totalCount = setCount * repCount, compute lastEditableIndex = isPremium ?
totalCount - 1 : Math.min(totalCount, FREE_PLAN_LIMITS.PRACTICE_TIMES_PER_LOG) -
1, replace use of isLastInput in returnKeyType with (globalIndex ===
lastEditableIndex) and guard the onSubmitEditing call to only call
focusNextInput when globalIndex < lastEditableIndex (or when not
isDisabledByLimit).
apps/web/components/forms/record-log/hooks/useRecordLogForm.ts (1)

106-116: ⚠️ Potential issue | 🟠 Major

編集初期化で既存の videoThumbnailPath を落としています。

editData.videoPath は引き継いでいるのに、サムネイルだけ常に null で初期化されています。これだと編集画面で既存サムネイルが表示されず、再保存時に video_thumbnail_path を消してしまう可能性があります。

差分案
           note: editData.note || "",
           videoPath: editData.videoPath ?? null,
-          videoThumbnailPath: null,
+          videoThumbnailPath: editData.videoThumbnailPath ?? null,
           reactionTime: editData.reactionTime?.toString() || "",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/record-log/hooks/useRecordLogForm.ts` around lines
106 - 116, The form initialization is clearing existing thumbnails by always
setting videoThumbnailPath to null; update the setFormDataList initialization to
preserve editData.videoThumbnailPath when present (use
editData.videoThumbnailPath ?? null) instead of unconditionally null so the
existing thumbnail is shown and not removed on save; locate the setFormDataList
call in useRecordLogForm and change the videoThumbnailPath assignment to read
from editData (same pattern used for videoPath) while leaving null fallback.
🟠 Major comments (31)
apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx-131-135 (1)

131-135: ⚠️ Potential issue | 🟠 Major

チーム練習時に userId 未確定だと他メンバーのログを表示してしまいます。

Line 131 の分岐で、isTeamPractice かつ userId 未取得時に全ログへフォールバックしており、一時的でも表示漏えいが起きます。userId がない間は空配列にするか、読み込み継続にしてください。

修正案(フォールバックを空配列に変更)
-        const filteredLogs =
-          isTeamPractice && userId
-            ? (practiceData.practice_logs || []).filter(
-                (log: PracticeLogFromDB) => log.user_id === userId,
-              )
-            : practiceData.practice_logs || [];
+        const allLogs = practiceData.practice_logs || [];
+        const filteredLogs = isTeamPractice
+          ? userId
+            ? allLogs.filter((log: PracticeLogFromDB) => log.user_id === userId)
+            : []
+          : allLogs;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx
around lines 131 - 135, The current ternary in PracticeDetails that uses
isTeamPractice && userId ? ... : practiceData.practice_logs returns all logs
when isTeamPractice is true but userId is falsy; change the fallback so that
when isTeamPractice is true and userId is not present you return an empty array
(or a loading state) instead of practiceData.practice_logs to avoid showing
other members' logs—update the expression that derives the filtered logs
(referencing isTeamPractice, userId, and practiceData.practice_logs /
PracticeLogFromDB inside PracticeDetails) to explicitly check for userId and
return [] when missing.
apps/web/app/pricing/page.tsx-43-47 (1)

43-47: ⚠️ Potential issue | 🟠 Major

比較表の可否がスクリーンリーダーで判別できません

CellContent の boolean 分岐がアイコンだけを返しているので、料金比較の主情報が読み上げでは空セルになります。sr-only の文言を添えて可否をテキストでも持たせたいです。

♿ 提案差分
 function CellContent({ value }: { value: string | boolean }) {
   if (typeof value === "string") {
     return <span className="text-sm text-gray-700">{value}</span>;
   }
-  return value ? CHECK : CROSS;
+  return (
+    <span className="inline-flex items-center justify-center">
+      <span className="sr-only">{value ? "利用可能" : "利用不可"}</span>
+      {value ? CHECK : CROSS}
+    </span>
+  );
 }

As per coding guidelines, 'Set aria-label attributes appropriately for accessibility'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/pricing/page.tsx` around lines 43 - 47, CellContent returns only
icons for boolean values, which is inaccessible to screen readers; update the
boolean branch in CellContent to include an accessible text node (e.g., a span
with className "sr-only" containing a localized status like "Available" / "Not
available") and ensure the visual icon constants CHECK and CROSS are aria-hidden
(or the wrapper element carries an appropriate aria-label) so screen readers
announce the status while sighted users see the icon; modify the boolean branch
to render the icon plus the sr-only span (or add aria-label on the wrapper) and
keep string handling unchanged.
apps/web/app/page.tsx-363-375 (1)

363-375: ⚠️ Potential issue | 🟠 Major

Freeプランの不可項目が肯定アイコンで表示されています

画像・動画アップロード: 不可 まで同じチェックアイコンで描画しているので、一覧を流し見したときに利用可能と誤認されます。/pricing 側の比較表とも表現がズレています。

💳 提案差分
-              <ul className="space-y-3 mb-6 flex-1">
-                {[
-                  "Split-time: 最大3個/記録",
-                  "PracticeTime: 最大18個/練習ログ",
-                  "画像・動画アップロード: 不可",
-                ].map((feature) => (
-                  <li key={feature} className="flex items-start gap-2 text-sm text-gray-700">
-                    <svg className="h-5 w-5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
-                      <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
-                    </svg>
-                    {feature}
-                  </li>
-                ))}
-              </ul>
+              <ul className="space-y-3 mb-6 flex-1">
+                {[
+                  { label: "Split-time: 最大3個/記録", available: true },
+                  { label: "PracticeTime: 最大18個/練習ログ", available: true },
+                  { label: "画像・動画アップロード: 不可", available: false },
+                ].map(({ label, available }) => (
+                  <li key={label} className="flex items-start gap-2 text-sm text-gray-700">
+                    <svg
+                      className={`h-5 w-5 shrink-0 ${available ? "text-gray-400" : "text-gray-300"}`}
+                      fill="none"
+                      viewBox="0 0 24 24"
+                      stroke="currentColor"
+                      strokeWidth={2}
+                      aria-hidden="true"
+                    >
+                      {available ? (
+                        <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
+                      ) : (
+                        <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
+                      )}
+                    </svg>
+                    {label}
+                  </li>
+                ))}
+              </ul>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/page.tsx` around lines 363 - 375, The list shows an availability
check icon for every entry including the "画像・動画アップロード: 不可" item; update the map
so unavailable items render a negative icon instead. Replace the plain string
array in page.tsx with objects (e.g. {label, available}) or detect "不可" in the
mapped value, and inside the map render a different SVG/class when available is
false (or label includes "不可") so the "画像・動画アップロード: 不可" list item shows an
X/disabled icon and/or muted color while other items keep the check icon.
apps/web/app/pricing/page.tsx-293-318 (1)

293-318: ⚠️ Potential issue | 🟠 Major

料金ページに法務導線付きフッター(StaticFooter)が欠けています

検証結果:/pricing ページは StaticFooter をインポート・描画していません。apps/web/app/pricing/page.tsx はホームページの apps/web/app/page.tsx のような StaticFooter が見当たらず、ページが CTA で終わっています。

親レイアウトの確認:

  • apps/web/app/pricing/ には layout.tsx がない
  • apps/web/app/layout.tsx(ルートレイアウト)には StaticFooter がない
  • pricing ページは root layout のみを継承するため、フッターが描画されていない

対応方法apps/web/app/pricing/page.tsx の最後に <StaticFooter /> をインポート・追加して、特定商取引法へのリンクを含むフッターを表示してください。期待値は「料金ページでも法務リンクに到達できること」です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/pricing/page.tsx` around lines 293 - 318, The pricing page is
missing the StaticFooter component; import StaticFooter at the top of
apps/web/app/pricing/page.tsx and render <StaticFooter /> at the end of the page
JSX (e.g., immediately after the closing </section> / before the outer </div>),
ensuring the footer with legal/terms links appears on the /pricing page;
reference the StaticFooter symbol in the import and insertion so the component
is included in the page output.
apps/web/components/forms/practice-log/types.ts-22-23 (1)

22-23: ⚠️ Potential issue | 🟠 Major

動画フィールドが送信型まで届いていません。

PracticeMenuPracticeLogEditData には動画パスを持たせていますが、onSubmit の契約である PracticeLogSubmitData にはまだありません。このままだと videoPath / videoThumbnailPath が送信直前で型から脱落し、保存処理へ渡せません。

🛠️ 最小修正案
 export interface PracticeLogSubmitData {
   style: string;
   swimCategory: "Swim" | "Pull" | "Kick";
   distance: number;
   reps: number;
   sets: number;
   circleTime: number | null;
   note: string;
   tags: Tag[];
   times: TimeEntry[];
+  videoPath?: string | null;
+  videoThumbnailPath?: string | null;
 }

Also applies to: 49-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/practice-log/types.ts` around lines 22 - 23,
PracticeLogSubmitData is missing the optional video fields so
videoPath/videoThumbnailPath are dropped before onSubmit; update the
PracticeLogSubmitData type to include videoPath?: string | null and
videoThumbnailPath?: string | null (matching PracticeMenu and
PracticeLogEditData) so the onSubmit contract carries the video fields through
to save, and verify the same additions are applied where PracticeLogSubmitData
is referenced (e.g., form submit handlers).
apps/web/components/forms/record-log/types.ts-29-30 (1)

29-30: ⚠️ Potential issue | 🟠 Major

RecordLogEditDatavideoThumbnailPath フィールドを追加し、フォーム初期化時に既存サムネイルパスを保持してください。

RecordLogFormState / RecordLogFormData には videoThumbnailPath が定義されていますが、編集入力データ型の RecordLogEditData に欠けています。useRecordLogForm.ts の初期化(115行目)で videoThumbnailPath: null と硬定義されているため、既存動画を再アップロードせず編集保存する場合、サムネイルパスが失われます。

修正が必要な場所:

  1. types.tsRecordLogEditDatavideoThumbnailPath?: string | null; を追加
  2. useRecordLogForm.ts 115行目を videoThumbnailPath: editData.videoThumbnailPath ?? null, に変更

同じ問題は useRecordForm.ts(競技記録編集)にも存在します。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/record-log/types.ts` around lines 29 - 30,
RecordLogEditData is missing the optional videoThumbnailPath field which causes
existing thumbnail paths to be dropped during form initialization in
useRecordLogForm; add videoThumbnailPath?: string | null to RecordLogEditData in
types.ts and update the initializer in useRecordLogForm (where form fields are
set) to use editData.videoThumbnailPath ?? null instead of a hard-coded null;
apply the same change to the analogous useRecordForm implementation to preserve
thumbnails for competition records as well.
apps/shared/hooks/useAuthState.ts-24-50 (1)

24-50: ⚠️ Potential issue | 🟠 Major

getSession() のフォールバックが最新の認証状態を上書きできます。

Line 27-32 の onAuthStateChange と Line 35-50 の getSession() が独立に同じ state を更新しているので、その間に SIGNED_OUT / SIGNED_IN が起きると、後から解決した fallback が古い session を戻せます。初回取得結果は「まだ別イベントが反映されていないときだけ」適用するガードが必要です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shared/hooks/useAuthState.ts` around lines 24 - 50, The fallback
getSession() can overwrite a newer auth state because both onAuthStateChange
(supabase.auth.onAuthStateChange) and getSession() independently call
setSession/setUser/setLoading; fix by introducing a small guard flag (e.g.,
authEventHandledRef) that you set to true inside the onAuthStateChange callback
and check before applying the initialSession in the getSession().then/catch
handlers so you only apply the fallback when no auth event has already fired;
ensure the flag is a ref or similar persistent value (not local variable) so
onAuthStateChange and the getSession handlers see the same state.
apps/shared/hooks/useAuthState.ts-1-3 (1)

1-3: ⚠️ Potential issue | 🟠 Major

'use client' ディレクティブを追加してください。

この shared hook は useState / useEffect を直接使用しているため、Line 1 に 'use client' ディレクティブが必須です。App Router で使用する際の境界が曖昧になるため、必ず追加してください。

修正例
+"use client";
+
 import { useState, useEffect } from "react";
 import type { SupabaseClient, User, Session } from "@supabase/supabase-js";
 import type { BaseAuthState } from "../types/auth";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shared/hooks/useAuthState.ts` around lines 1 - 3, Add the React client
directive to the top of the module: insert 'use client' as the very first
statement in apps/shared/hooks/useAuthState.ts (before any imports) because this
file uses useState/useEffect and defines the useAuthState hook; this ensures the
hook runs on the client and clarifies App Router boundaries.
supabase/migrations/20260318000000_drop_video_url.sql-1-2 (1)

1-2: ⚠️ Potential issue | 🟠 Major

video_url のドロップ前に backfill と残存参照の処理が必須です。

検査結果:

  • 20260317 移行ファイルはカラム追加のみで、video_url → 新カラムへのデータコピーがありません
  • test-data-2025.sqlvideo_url をまだ参照しているため、このドロップ後は INSERT が失敗します
  • 本番環境にデータが存在する場合、アップデート処理なしでは永続的に失われます
必要な修正
  1. 20260317 と 20260318 の間に backfill 移行を追加:UPDATE records SET video_path = video_url ... WHERE video_url IS NOT NULL
  2. test-data-2025.sql の INSERT ステートメントから video_url 参照を削除
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260318000000_drop_video_url.sql` around lines 1 - 2,
Add a backfill migration between the 20260317 and 20260318 migrations that
copies data from the old column to the new one (e.g., run UPDATE records SET
video_path = video_url WHERE video_url IS NOT NULL) so existing production rows
are preserved before the drop; then remove all references to video_url from
test-data-2025.sql INSERTs so tests won’t fail after the drop (keep the existing
ALTER TABLE ... DROP COLUMN IF EXISTS video_url migration as-is but only apply
it after the backfill and after the Step 7 code deploy).
apps/mobile/screens/PracticeFormScreen.tsx-594-604 (1)

594-604: ⚠️ Potential issue | 🟠 Major

プレミアム制御が UI 非表示だけで、送信経路はまだ通ります。

Line 594 の表示切り替えで uploader を隠しても、handleSave / handleContinueToLog 側は newImageFiles.length > 0 ならそのまま uploadImages(...) を呼びます。購読状態が画面表示中に変わった場合や state が残った場合に無料会員でも送信できるので、送信直前でも isPremium を再判定してキューを破棄/ブロックした方が安全です。

🔒 対応イメージ
+ useEffect(() => {
+   if (!isPremium) {
+     setNewImageFiles([]);
+     setDeletedImageIds([]);
+   }
+ }, [isPremium]);
+
  const handleSave = async () => {
+   if (!isPremium && newImageFiles.length > 0) {
+     Alert.alert("プレミアム限定", PREMIUM_MESSAGES.image_upload, [{ text: "OK" }]);
+     return;
+   }
    // 二重送信防止
    if (isSubmittingRef.current) return;
    ...
  };

  const handleContinueToLog = async () => {
+   if (!isPremium && newImageFiles.length > 0) {
+     Alert.alert("プレミアム限定", PREMIUM_MESSAGES.image_upload, [{ text: "OK" }]);
+     return;
+   }
    // 二重送信防止
    if (isSubmittingRef.current) return;
    ...
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/screens/PracticeFormScreen.tsx` around lines 594 - 604, The UI
currently hides ImageUploader when isPremium is false but handleSave and
handleContinueToLog still call uploadImages when newImageFiles.length > 0;
modify those save/continue paths (functions handleSave and handleContinueToLog)
to re-check isPremium immediately before any upload step and if not premium,
skip/abort the image upload (e.g., set/clear newImageFiles or prevent calling
uploadImages) and proceed without images (or return early), ensuring the upload
queue cannot be triggered when isPremium is false even if state lingers;
reference isPremium, newImageFiles, uploadImages, ImageUploader in your changes.
supabase/functions/revenucat-webhook/index.ts-1-3 (1)

1-3: ⚠️ Potential issue | 🟠 Major

RevenueCat のウェブフック設定の綴りを統一してください。

ディレクトリ名とコード内で revenucat を使用していますが、公式サービスは「RevenueCat」(revenuecat.com)です。Supabase Edge Functions はディレクトリ名がそのままエンドポイント名になるため、RevenueCat の外部ダッシュボード側で revenuecat-webhook を設定している場合、現在の revenucat-webhook エンドポイントへのリクエストが 404 になります。以下の箇所をマージ前に統一してください:

  • ディレクトリ名: supabase/functions/revenucat-webhook/revenuecat-webhook/
  • インポートパス、ファイル名、データベース migration の provider 値も合わせて統一
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/revenucat-webhook/index.ts` around lines 1 - 3,
ディレクトリ名とインポートで綴りが間違っているためエンドポイントが一致しません;rename the function directory from
revenucat-webhook to revenuecat-webhook, update the import path string
("../_shared/revenucat-webhook-handler.ts") to use revenuecat spelling, and
rename any shared file names and migration `provider` values that use
"revenucat" to "revenuecat" so createRevenueCatWebhookHandler and its handler
file, import, and DB migration all use the same correct "revenuecat" spelling.
apps/mobile/package.json-63-63 (1)

63-63: ⚠️ Potential issue | 🟠 Major

reactreact-test-renderer のバージョンを揃えてください。

Line 63 で react19.1.4 に上げていますが、Line 85 の react-test-renderer19.2.4 のままです。Renderer と core の minor がずれると、@testing-library/react-native 配下で peer 警告や不安定なテスト失敗の原因になります。react-test-renderer19.1.4 に更新してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/package.json` at line 63, package.json の依存不整合を修正してください: 現在
"react" が "19.1.4" に上がっている一方で "react-test-renderer" が "19.2.4"
のままなので、package.json の dependencies/devDependencies 内の "react-test-renderer"
エントリを "19.1.4" に揃えて更新し、依存整合後にロックファイルを再生成して(npm/yarn/pnpm のいずれかで)テストを再実行してください.
package.json-113-114 (1)

113-114: ⚠️ Potential issue | 🟠 Major

expo パッケージのバージョンが大幅に古い状態です。更新を検討してください。

expo-video@3.0.16expo-video-thumbnails@10.0.8 は npm に存在する有効なバージョンですが、現在の最新バージョンは両方とも 55.0.10 です。これらのパッケージは 50+ メジャーバージョン以上遅れており、セキュリティ更新やバグ修正が失われている可能性があります。最新バージョンへの更新を検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 113 - 114, The expo-video and
expo-video-thumbnails dependencies are pinned to very old versions; update the
package.json entries for "expo-video" and "expo-video-thumbnails" from "~3.0.16"
and "~10.0.8" to the current release "55.0.10", then run your package manager
(npm/yarn/pnpm) to install and rebuild; after updating, run the app and relevant
tests to verify there are no breaking API changes from the upgraded "expo-video"
and "expo-video-thumbnails" packages.
apps/mobile/screens/PracticeLogFormScreen.tsx-64-66 (1)

64-66: ⚠️ Potential issue | 🟠 Major

動画 state が menu 単位になっていません。

VideoUploader は各メニュー内で描画されているのに、保持している existingVideoPath / existingThumbnailPath は画面全体で 1 組だけです。これだと 2 件目のメニューでアップロード/削除した内容が他のメニューにも波及しますし、新規作成時はどの menu の動画かも区別できません。menu.id ごとに動画 state を持たせて、保存後に対応する practice log へ紐付ける形にしないと誤表示・誤保存が起きます。

💡 方向性の例
-  const [existingVideoPath, setExistingVideoPath] = useState<string | null>(null);
-  const [existingThumbnailPath, setExistingThumbnailPath] = useState<string | null>(null);
+  const [videosByMenuId, setVideosByMenuId] = useState<
+    Record<string, { videoPath: string | null; thumbnailPath: string | null }>
+  >({});
...
-                <VideoUploader
+                <VideoUploader
                   type="practice-log"
-                  id={practiceLogId}
-                  existingVideoPath={existingVideoPath}
-                  existingThumbnailPath={existingThumbnailPath}
+                  id={isEditMode && menu.id === practiceLogId ? practiceLogId : undefined}
+                  existingVideoPath={videosByMenuId[menu.id]?.videoPath ?? null}
+                  existingThumbnailPath={videosByMenuId[menu.id]?.thumbnailPath ?? null}
                   isPremium={isPremium}
                   onUploadComplete={(vPath, tPath) => {
-                    setExistingVideoPath(vPath);
-                    setExistingThumbnailPath(tPath);
+                    setVideosByMenuId((prev) => ({
+                      ...prev,
+                      [menu.id]: { videoPath: vPath, thumbnailPath: tPath },
+                    }));
                   }}
                   onDelete={() => {
-                    setExistingVideoPath(null);
-                    setExistingThumbnailPath(null);
+                    setVideosByMenuId((prev) => ({
+                      ...prev,
+                      [menu.id]: { videoPath: null, thumbnailPath: null },
+                    }));
                   }}
                 />

Also applies to: 767-783

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/screens/PracticeLogFormScreen.tsx` around lines 64 - 66, The
current global existingVideoPath/existingThumbnailPath state in
PracticeLogFormScreen causes cross-menu interference; replace them with a
per-menu map keyed by menu.id (e.g. existingVideoByMenu: Record<string,
string|null> and existingThumbnailByMenu: Record<string, string|null>), update
the setters (setExistingVideoByMenu/setExistingThumbnailByMenu) to only change
the entry for the target menu.id, and pass the menu-specific values and setters
into VideoUploader instead of the single global values; finally, when saving the
form ensure you iterate menus and attach each menu.id's video/thumbnail from
your maps to the corresponding practice log entry so uploads/deletes are
isolated per menu.
apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx-341-345 (1)

341-345: ⚠️ Potential issue | 🟠 Major

動画付き記録の編集データが不完全です。

ここで動画を表示するようになった一方で、同ファイルの onEditRecordcalendarItem.metadata.record には video_path / video_thumbnail_path を詰めていません。いまのままだと動画付き記録を編集したときに既存動画がフォームへ渡らず、再保存で外れる可能性があります。

💡 修正例
                         metadata: {
                           record: {
                             time: fullRecord.time,
                             is_relaying: fullRecord.is_relaying || false,
                             reaction_time: fullRecord.reaction_time ?? null,
+                            video_path: fullRecord.video_path ?? null,
+                            video_thumbnail_path: fullRecord.video_thumbnail_path ?? null,
                             style: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx`
around lines 341 - 345, The edit flow doesn’t include existing video fields into
calendarItem.metadata.record, so when a user edits a video record the
video_path/video_thumbnail_path are lost; update the onEditRecord logic that
constructs calendarItem.metadata.record to copy record.videoPath -> video_path
and record.videoThumbnailPath -> video_thumbnail_path (ensure naming matches
backend shape) so the VideoPlayer-backed record retains its paths when opening
the edit form and resaving.
apps/web/e2e/src/tests/billing.spec.ts-37-42 (1)

37-42: ⚠️ Potential issue | 🟠 Major

500 を成功扱いにしないでください。

ここで 500 を許容すると、Stripe 設定漏れやサーバー例外でもテストが通ってしまい、課金 API の回帰を検知できません。CI で前提環境が揃わないケースを考慮するなら、この spec 自体を skip するか、Stripe をモックした上で期待値を 4xx に固定した方が安全です。

Also applies to: 61-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/e2e/src/tests/billing.spec.ts` around lines 37 - 42, The test
TC-BILLING-002 currently accepts a 500 status which masks server/configuration
errors; update the test to not treat 500 as a valid success: either skip the
test when Stripe env vars are missing (detect missing env in the test setup and
call test.skip) or replace the live call with a mocked Stripe interaction and
assert only 4xx responses (e.g., expect(res.status()).toBe(400)). Apply the same
change to the related test at the other occurrence (the block around lines
61-65) so neither test accepts 500.
apps/shared/types/ui.ts-95-96 (1)

95-96: ⚠️ Potential issue | 🟠 Major

shared UI 型の命名がまた分岐しています。

apps/shared/types/ui.tsvideo_path / video_thumbnail_path を追加すると、同じ PR で増えている videoPath 系と型契約が食い違います。このファイル内にも RecordFormData.videoUrl が残っているので、呼び出し側に videoUrl / videoPath / video_path の 3 系統が混在します。DB 由来の snake_case は変換レイヤーで吸収し、UI 型は camelCase に揃えた方が安全です。

💡 方向性
-    video_path?: string;
-    video_thumbnail_path?: string;
+    videoPath?: string;
+    videoThumbnailPath?: string;

他の record 型定義と isRecordMetadata() の戻り型も同じ命名に揃えてください。

As per coding guidelines "UI type definitions should use camelCase properties (defined in `apps/shared/types/ui.ts` and `apps/web/stores/types.ts`)".

Also applies to: 170-171, 450-451

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/shared/types/ui.ts` around lines 95 - 96, The UI types in
apps/shared/types/ui.ts must use camelCase: replace video_path and
video_thumbnail_path with videoPath and videoThumbnailPath in the Record /
RecordFormData type definitions and ensure RecordFormData.videoUrl aligns with
the videoPath naming; update any related record type aliases and the
isRecordMetadata() return type to the same camelCase properties so callers only
see videoUrl/videoPath (not snake_case), and keep snake_case -> camelCase
conversion in the DB/serialization layer instead of in UI types.
apps/web/app/api/stripe/portal/route.ts-22-26 (1)

22-26: ⚠️ Potential issue | 🟠 Major

user_subscriptions テーブルに行が存在しないユーザーではキャッシュの書き戻しが失敗します。

single() でデータが見つからない場合、subscription は null になります。その後のフォールバック処理は動作しますが、49–52 行目の update() は対象行が存在しないため無操作に終わります。update() は行がないときでもエラーを返さず(error: null)、結果として stripe_customer_id がキャッシュされません。以後のリクエストでも毎回 Stripe Search にフォールバックするため、不要な API 呼び出しが発生します。

50–52 行目を upsert() に変更してください:

修正案
await supabase
  .from("user_subscriptions")
  .upsert({ id: user.id, stripe_customer_id: customerId })
  .eq("id", user.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/stripe/portal/route.ts` around lines 22 - 26, The current
query reads subscription with const { data: subscription } = await
supabase.from("user_subscriptions").select("stripe_customer_id").eq("id",
user.id).single(), but later the code calls
supabase.from("user_subscriptions").update(...) which is a no-op when the row
doesn't exist; replace that update call with an upsert so the stripe_customer_id
is written back for users without an existing row. Specifically, locate the
supabase.from("user_subscriptions").update(...) call in route.ts (the block that
writes customerId back) and change the call to use upsert({ id: user.id,
stripe_customer_id: customerId }) so the record is inserted or updated
atomically.
apps/mobile/screens/RecordLogFormScreen.tsx-75-77 (1)

75-77: ⚠️ Potential issue | 🟠 Major

複数フォーム時の動画状態管理に問題があります。

formDataList は複数のレコードフォームを扱えますが、動画の状態(existingVideoPath, existingThumbnailPath)は単一の useState で管理されています。複数フォームがある場合、すべてのフォームで同じ動画が共有されてしまいます。

新規作成モード(!recordId)では複数フォームが存在する可能性がありますが、動画アップロードは id={recordId}recordId が undefined になるため、アップロード自体は機能しない可能性があります。

🔧 修正案

動画状態を formDataList に含めるか、編集モード(単一フォーム)でのみ動画アップロードを有効にすることを検討してください:

 {/* 動画 */}
 <View style={styles.field}>
   <Text style={styles.label}>動画</Text>
+  {recordId ? (
     <VideoUploader
       type="record"
       id={recordId}
       existingVideoPath={existingVideoPath}
       existingThumbnailPath={existingThumbnailPath}
       isPremium={isPremium}
       onUploadComplete={(vPath, tPath) => {
         setExistingVideoPath(vPath);
         setExistingThumbnailPath(tPath);
       }}
       onDelete={() => {
         setExistingVideoPath(null);
         setExistingThumbnailPath(null);
       }}
     />
+  ) : (
+    <Text style={styles.hintText}>記録保存後に動画を追加できます</Text>
+  )}
 </View>

Also applies to: 728-745

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/screens/RecordLogFormScreen.tsx` around lines 75 - 77, The video
state (existingVideoPath, existingThumbnailPath) is currently global and will be
shared across multiple entries in formDataList; change this by moving
video/thumbnail fields into each form entry inside formDataList (e.g., add
videoPath and thumbnailPath properties), initialize them when creating entries,
and update all handlers and components that read or set
existingVideoPath/existingThumbnailPath to accept an entry identifier (index or
the entry's id) and update the corresponding formDataList[index].videoPath /
.thumbnailPath; alternatively, if you only want uploads in edit mode, gate the
upload UI/handlers behind recordId (only enable when recordId is truthy) and
keep current single-state approach for the single-form edit case.
apps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsx-623-673 (1)

623-673: ⚠️ Potential issue | 🟠 Major

動画アップロード時のID参照が不正確

動画アップロード処理で mr.id を使用していますが、これは新規作成時にはフォーム内部で生成された一時的なIDです。レコード挿入後に返される newRecord.id を使用する必要があります。

現在のコードでは、新規作成した validRecords ループと動画アップロードの styleEntries ループが分離しているため、新しいレコードIDとの紐付けができていません。

また、Lines 641-643 の PUT リクエストでレスポンスステータスのチェックがなく、アップロード失敗時にも後続処理が継続してしまいます。

🔧 修正案: レコード作成と動画アップロードを連携させる
      // 新規レコードを作成
      for (const record of validRecords) {
+       // 対応する styleEntry と memberRecord を特定
+       const matchingEntry = styleEntries.find(e => e.styleId === record.styleId);
+       const matchingMr = matchingEntry?.memberRecords.find(mr => mr.memberUserId === record.memberUserId);
+
        const { data: newRecord, error: recordError } = await supabase
          .from("records")
          .insert({
            // ... existing insert code
          })
          .select("id")
          .single();

        if (recordError) {
          console.error(`Record作成エラー (${record.memberName}):`, recordError);
          hasError = true;
          continue;
        }

        // ... split times handling ...

+       // 動画アップロード(レコード作成直後に対応するIDで実行)
+       if (matchingMr?.videoFile && newRecord) {
+         try {
+           const uploadUrlRes = await fetch("/api/storage/videos/upload-url", {
+             method: "POST",
+             headers: { "Content-Type": "application/json" },
+             body: JSON.stringify({ type: "record", id: newRecord.id, contentType: "video/mp4" }),
+           });
+           if (!uploadUrlRes.ok) continue;
+           // ... rest of upload logic using newRecord.id ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsx
around lines 623 - 673, The upload loop uses the temporary form-generated mr.id
instead of the persisted record ID returned after inserting validRecords, and it
also lacks response checks on the PUT uploads; fix by creating a mapping from
the temporary id to the returned newRecord.id during the record-creation step
that processes validRecords (capture the API response for each created record
and store {tempId -> newId}), then in the styleEntries -> memberRecords loop
replace usage of mr.id with the mapped newId (skip if no mapping), and after
fetching videoUploadUrl/thumbnailUploadUrl check response.ok and also check the
PUT upload responses (videoUploadRes.ok and thumbnailUploadRes.ok) before
proceeding to confirm and team-assign; reference symbols: validRecords,
styleEntries, memberRecords, mr.id, videoUploadUrl, thumbnailUploadUrl,
vPath/tPath, and the fetch calls to "/api/storage/videos/upload-url", the PUT to
videoUploadUrl/thumbnailUploadUrl, "/api/storage/videos/confirm", and
"/api/storage/videos/team-assign".
apps/web/components/video/VideoPlayer.tsx-118-127 (1)

118-127: ⚠️ Potential issue | 🟠 Major

Presigned URL でのダウンロード属性が機能しない

download="video.mp4" 属性は、Cloudflare R2 の presigned URL(異なるオリジン)では期待通りに動作しません。generateVideoGetUrl()ResponseContentDisposition パラメータなしで署名付き URL を生成しているため、ブラウザが download 属性を無視してナビゲーション(新しいタブで開く)として扱います。

確実にダウンロードさせるには、GetObjectCommandResponseContentDisposition: 'attachment; filename="video.mp4"' を指定するか、代替手段として Blob 経由のダウンロード実装を検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/video/VideoPlayer.tsx` around lines 118 - 127, The
presigned URL download is ignored by the browser because generateVideoGetUrl()
signs the R2 GetObjectCommand without setting ResponseContentDisposition, so
update the presigning logic in generateVideoGetUrl (the function that builds the
GetObjectCommand) to include ResponseContentDisposition: 'attachment;
filename="video.mp4"' so the browser treats the URL as a download;
alternatively, if you prefer a client-side fallback, change the VideoPlayer
component (where canDownload and presignedUrl are used) to fetch the presigned
URL, convert the response to a Blob, create an object URL, and trigger an anchor
click with download to force download when the presigned
ResponseContentDisposition cannot be set.
supabase/functions/_shared/stripe-webhook-handler.ts-137-165 (1)

137-165: ⚠️ Potential issue | 🟠 Major

処理前に processed を確定すると、一時障害でイベントが永久に取りこぼれます。

processed_webhook_events への INSERT をハンドラ実行前に確定しているので、その後の Stripe / Supabase 失敗で 500 を返しても次回配信は duplicate 扱いになります。課金自体は Stripe 側で既に冪等なので、ここは processing / processed の二段階管理か、成功後マークにしないと購読状態が不整合になりやすいです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_shared/stripe-webhook-handler.ts` around lines 137 - 165,
The current tryMarkAsProcessed flow marks events as processed up-front causing
permanent drops on transient handler failures; change to a two-phase idempotency
record in the processed_webhook_events lifecycle: instead of inserting a final
processed row before handling in tryMarkAsProcessed, either (A) insert a row
with a status field like "processing" (and a processed_at nullable) using a
unique constraint on event_id, then update that row to status "processed" and
set processed_at only after the handler succeeds (and treat existing
"processing" rows older than a short TTL as stale), or (B) avoid pre-inserting
entirely and insert the processed row only after successful handling; update
code paths that call tryMarkAsProcessed to use the new semantics and ensure
duplicate detection still checks event_id uniqueness (handle error.code "23505"
as duplicate), and add cleanup/TTL logic for stale "processing" entries if you
implement option A.
apps/web/app/api/storage/videos/upload-url/route.ts-65-92 (1)

65-92: ⚠️ Potential issue | 🟠 Major

この所有者チェックでは、チーム管理者の代理アップロードが常に 403 になります。

PracticeLogClient の新フローは、管理者が他メンバーの practice_logs を保存した後にこの API で署名 URL を取りに来ます。ここで practice_logs.user_id === user.id を要求すると、所有者がメンバー本人である以上、代理アップロードは全件失敗します。管理者向けフローは別認可にするか、チーム管理者検証付きの一時アップロードモードを追加した方がよいです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/storage/videos/upload-url/route.ts` around lines 65 - 92,
The owner-check logic in the route (the branches that compare record.user_id ===
user.id and log.user_id === user.id) incorrectly blocks team-admin delegated
uploads; update the authorization to also allow admin agents: when type ===
"record" or "practice_log" (practice_logs.user_id), after fetching the target
row (record or log), permit the request if either user.id matches OR the
requesting user is a team admin for the target owner (call an existing helper
like isTeamAdminFor(user.id, targetOwnerId) or verify the team_admin
relationship), or alternatively accept an explicit delegatedUpload flag from
PracticeLogClient that triggers a different admin-validated path; ensure you
reference/modify the owner checks around record.user_id and log.user_id to
include that admin/delegated condition and return 403 only if neither condition
is satisfied.
apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx-436-477 (1)

436-477: ⚠️ Potential issue | 🟠 Major

各 fetch レスポンスの ok を確認してください。

fetch は 4xx/5xx でも例外にならないため、PUT / confirm / team-assign の失敗が catch に入らず、そのまま成功扱いでリダイレクトされます。これにより、ユーザーには保存成功に見えても、実際には動画のアップロードや確認処理が失敗している可能性があります。各 fetch のレスポンスの ok を確認し、失敗時には適切にエラーハンドリングを行ってください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx
around lines 436 - 477, The upload flow in PracticeLogClient.tsx currently
ignores non-2xx fetch responses for the PUTs to
videoUploadUrl/thumbnailUploadUrl and the POSTs to "/api/storage/videos/confirm"
and "/api/storage/videos/team-assign", so failures won't surface to the outer
catch; update the logic in the method handling the upload (the block that
obtains uploadUrlRes and then does fetch(videoUploadUrl...),
fetch(thumbnailUploadUrl...), fetch("/api/storage/videos/confirm"...), and
fetch("/api/storage/videos/team-assign"...)) to await each fetch response, check
response.ok, and on failure read response.text() (or json()) and throw or return
an Error with that message so the outer catch can handle it (or mark the
specific member upload failed and continue), ensuring any non-ok response is
logged and prevents a false-success redirect.
supabase/functions/_shared/stripe-webhook-handler.ts-182-197 (1)

182-197: ⚠️ Potential issue | 🟠 Major

customer.subscription.created では、失敗した初回決済(incomplete)を含む非 active 状態を単純に active に統一しないでください。

Stripe の subscription.status 値の意味を正確に理解する必要があります:

  • incomplete: 初回決済失敗(まだ支払い未確認、有効期限は約23時間)
  • incomplete_expired: 初回決済期限切れ(終端状態)
  • active: 有効な購読状態
  • past_due: 最新請求書未払い(リトライ中)
  • canceled / unpaid / paused: premium 権限を付与すべきでない状態

現在のコードは trialing 以外をすべて active に潰すため、決済失敗した incomplete 状態のサブスクリプションにも premium アクセス権を与えてしまいます。Stripe の仕様に従い、activetrialing のみを許容し、その他のステータスは保持するか明示的に制限してください。

コードスニペット
const status = subscription.status === "trialing" ? "trialing" : "active";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_shared/stripe-webhook-handler.ts` around lines 182 - 197,
The current line that forces any non-"trialing" status to "active" is incorrect;
change the logic that sets the local status variable (the line defining status)
to preserve subscription.status instead of coercing to "active" and ensure
premium access is granted only when status is explicitly "active" or "trialing"
(and not for "incomplete", "incomplete_expired", "past_due", "canceled", etc.);
update the upsertData construction (the upsertData object and its plan field) so
plan is set to "premium" only when the preserved status is "active" or
"trialing" and otherwise set to a non-premium value (e.g., null/"free") or leave
it unset.
apps/web/lib/cropVideo.ts-89-94 (1)

89-94: ⚠️ Potential issue | 🟠 Major

VideoEncoder のエラーハンドラーで throw しても Promise が reject されません。

エラーコールバック内で throw しても、外側の Promise を reject しないため、エラーが適切に伝播しません。エラーを追跡して Promise を reject するか、別の方法でハンドリングする必要があります。

🐛 提案する修正
+  let encoderError: Error | null = null;
+
   const encoder = new VideoEncoder({
     output: (chunk, meta) => muxer.addVideoChunk(chunk, meta ?? {}),
     error: (e) => {
-      throw e;
+      encoderError = e;
     },
   });

また、フレーム処理ループ内でエラーをチェックするか、Promise を reject するロジックを追加してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/cropVideo.ts` around lines 89 - 94, The VideoEncoder error
callback currently just throws inside error: (e) => { throw e; } which does not
reject the surrounding Promise or propagate the error; update the encoder
creation to capture the external Promise's reject (or set an async error
handler) and call that reject inside the error callback (e.g., store a reject
function used by the Promise that wraps your encoding flow), or set an error
flag (e.g., encoderError) that the frame-processing loop inspects and then
rejects the Promise or aborts processing; reference the VideoEncoder instance
named encoder and the error callback, and ensure any place that awaits the
encoding (the Promise wrapping muxer.addVideoChunk/output processing) is
rejected when encoder.error runs.
apps/web/lib/r2-video.ts-16-24 (1)

16-24: ⚠️ Potential issue | 🟠 Major

環境変数の非 null アサーション(!)はランタイムエラーのリスクがあります。

getS3Client()R2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEY が undefined の場合でも S3Client を作成しようとしますが、認証に失敗します。isVideoR2Enabled() と同様のチェックを追加するか、明示的なエラーを投げることを推奨します。

🛡️ 提案する修正
 const getS3Client = () => {
+  if (!process.env.R2_ACCOUNT_ID || !process.env.R2_ACCESS_KEY_ID || !process.env.R2_SECRET_ACCESS_KEY) {
+    throw new Error("R2環境変数が設定されていません");
+  }
   return new S3Client({
     region: "auto",
     endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
     credentials: {
-      accessKeyId: process.env.R2_ACCESS_KEY_ID!,
-      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
+      accessKeyId: process.env.R2_ACCESS_KEY_ID,
+      secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
     },
   });
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/r2-video.ts` around lines 16 - 24, getS3Client currently uses
non-null assertions on process.env.R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY
which can cause runtime failures; update getS3Client to validate those env vars
(or call isVideoR2Enabled()) before creating the S3Client and throw a clear
error if they are missing. Locate getS3Client and add explicit checks for
process.env.R2_ACCESS_KEY_ID and process.env.R2_SECRET_ACCESS_KEY, and if either
is undefined throw a descriptive Error (e.g., "R2 credentials missing: set
R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY") instead of using "!"; then pass the
validated values into the S3Client constructor. Ensure any calling code either
guards with isVideoR2Enabled() or handles the thrown error.
apps/web/components/video/VideoUploader.tsx-74-80 (1)

74-80: ⚠️ Potential issue | 🟠 Major

uploadUrlRes.json() が2回呼ばれる可能性があります。

Line 75 で uploadUrlRes.json() を呼び、Line 79-80 でも再度呼んでいます。Response body は一度しか読めないため、エラー時に body を消費した後、成功パスで再度読もうとするとエラーになります。

🐛 提案する修正
       const uploadUrlRes = await fetch("/api/storage/videos/upload-url", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ type, id: targetId, contentType: "video/mp4" }),
       });

+      const uploadUrlData = await uploadUrlRes.json() as UploadUrlResponse | { error?: string; message?: string };
+
       if (!uploadUrlRes.ok) {
-        const data = await uploadUrlRes.json() as { error?: string; message?: string };
-        throw new Error(data.message ?? data.error ?? "アップロードURLの取得に失敗しました");
+        const errorData = uploadUrlData as { error?: string; message?: string };
+        throw new Error(errorData.message ?? errorData.error ?? "アップロードURLの取得に失敗しました");
       }

-      const { videoUploadUrl, thumbnailUploadUrl, videoPath: vPath, thumbnailPath: tPath } =
-        await uploadUrlRes.json() as UploadUrlResponse;
+      const { videoUploadUrl, thumbnailUploadUrl, videoPath: vPath, thumbnailPath: tPath } =
+        uploadUrlData as UploadUrlResponse;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/video/VideoUploader.tsx` around lines 74 - 80, The
response body is being consumed twice via uploadUrlRes.json(); read the JSON
once into a variable and reuse it: await the body (e.g., const body = await
uploadUrlRes.json()), then check uploadUrlRes.ok and throw using
body.message/body.error if needed, otherwise destructure videoUploadUrl,
thumbnailUploadUrl, videoPath (vPath) and thumbnailPath (tPath) from that same
body (reference symbols: uploadUrlRes, UploadUrlResponse, videoUploadUrl,
thumbnailUploadUrl, videoPath/vPath, thumbnailPath/tPath).
supabase/functions/_shared/revenucat-webhook-handler.ts-258-266 (1)

258-266: ⚠️ Potential issue | 🟠 Major

update() が affected_rows をチェックせず、行が存在しない場合のエラーを検出できません

現在のコードでは、user_subscriptions テーブルに該当ユーザーの行が存在しない場合、update() は成功しますが行は更新されません(affected rows = 0)。特に以下のイベントタイプでこの問題が顕在化します:

  • RENEWAL, PRODUCT_CHANGE, UNCANCELLATION, CANCELLATION: userId の存在確認がなく、update が失敗してもエラーが報告されません(hasActiveStripeSubscription のチェックがスキップされるため)
  • INITIAL_PURCHASE: hasActiveStripeSubscription が .single() を使用しているため、行がない場合は例外が発生し、結果的にエラーハンドリングされます

解決方法として、Stripe ハンドラー(stripe-webhook-handler.ts:201)と同様に upsert() の使用を検討してください。または最低限、affected_rows をチェックして、更新対象の行が存在しない場合は明示的にエラーを返してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_shared/revenucat-webhook-handler.ts` around lines 258 -
266, The update path using
supabase.from("user_subscriptions").update(update).eq("id", userId) does not
check affected_rows so a no-op update (zero rows) is treated as success; modify
the handler to either use upsert() like the Stripe handler or after the update
inspect the response (e.g., check count/affected_rows or returned data length)
and treat zero affected rows as an error—return the JSON error and log
accordingly so downstream checks (hasActiveStripeSubscription) still run when
the row is missing.
apps/mobile/utils/videoUpload.ts-217-226 (1)

217-226: ⚠️ Potential issue | 🟠 Major

confirmUpload 失敗時にアップロード済みファイルが孤立します。

R2 への PUT が成功した後に DB 確定が失敗すると、未参照オブジェクトが残り続けます。uploadVideo 全体を try/catch で包み、失敗時の補償削除(またはサーバー側のクリーンアップジョブ)を追加してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/utils/videoUpload.ts` around lines 217 - 226, Wrap the whole
upload flow in uploadVideo (the section that runs uploadVideoToR2 and
uploadThumbnailToR2 and then calls confirmUpload) in a try/catch: perform the
parallel uploads and call confirmUpload inside the try; in the catch, attempt
compensating deletes for the uploaded objects identified by videoPath and
thumbnailPath (use your R2 delete helper or call the same R2 client used by
uploadVideoToR2/uploadThumbnailToR2), log any delete failures, then rethrow or
return a clear failure; ensure progress callbacks still reach 100/cleanup in
finally as appropriate.
apps/mobile/utils/videoUpload.ts-8-8 (1)

8-8: ⚠️ Potential issue | 🟠 Major

環境変数未設定時に本番APIへフォールバックしないでください。

webApiUrl 未設定で https://swim-hub.app に送信されるため、開発/検証環境から本番データへ誤アクセスするリスクがあります。未設定時は即エラーにしてください。

💡 修正案
-const WEB_API_URL = Constants.expoConfig?.extra?.webApiUrl || "https://swim-hub.app";
+const webApiUrl = Constants.expoConfig?.extra?.webApiUrl;
+if (!webApiUrl) {
+  throw new Error("expo.extra.webApiUrl が未設定です");
+}
+const WEB_API_URL = webApiUrl;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/utils/videoUpload.ts` at line 8, The WEB_API_URL constant
currently falls back to "https://swim-hub.app" when
Constants.expoConfig?.extra?.webApiUrl is missing; change this so that if
webApiUrl is undefined/null you throw a clear runtime error instead of using the
production URL. Replace the fallback assignment for WEB_API_URL in
apps/mobile/utils/videoUpload.ts with logic that validates
Constants.expoConfig?.extra?.webApiUrl and throws a descriptive Error (or
asserts) when absent, and update any call sites that assume WEB_API_URL is
always present accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f10ccae6-5b4c-471e-bece-3bb5a2e8fb7d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c2bb59 and 86e61d0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (113)
  • .github/workflows/ci.yml
  • .github/workflows/supabase.yml
  • apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx
  • apps/mobile/components/calendar/DayDetailModal/types.ts
  • apps/mobile/components/practices/PracticeLogItem.tsx
  • apps/mobile/components/shared/PremiumBadge.tsx
  • apps/mobile/components/shared/VideoPlayer.tsx
  • apps/mobile/components/shared/VideoUploader.tsx
  • apps/mobile/components/shared/index.ts
  • apps/mobile/hooks/useVideoPresignedUrl.ts
  • apps/mobile/package.json
  • apps/mobile/screens/CompetitionBasicFormScreen.tsx
  • apps/mobile/screens/PracticeFormScreen.tsx
  • apps/mobile/screens/PracticeLogFormScreen.tsx
  • apps/mobile/screens/PracticeTimeFormScreen.tsx
  • apps/mobile/screens/RecordDetailScreen.tsx
  • apps/mobile/screens/RecordFormScreen.tsx
  • apps/mobile/screens/RecordLogFormScreen.tsx
  • apps/mobile/utils/imageUpload.ts
  • apps/mobile/utils/videoUpload.ts
  • apps/shared/__mocks__/supabase.ts
  • apps/shared/__tests__/api/records.test.ts
  • apps/shared/__tests__/hooks/queries/records.test.ts
  • apps/shared/__tests__/hooks/useRecords.test.tsx
  • apps/shared/__tests__/premium.test.ts
  • apps/shared/api/records.ts
  • apps/shared/constants/premium.ts
  • apps/shared/hooks/index.ts
  • apps/shared/hooks/queries/records.ts
  • apps/shared/hooks/useAuthState.ts
  • apps/shared/types/auth.ts
  • apps/shared/types/practice.ts
  • apps/shared/types/record.ts
  • apps/shared/types/supabase-schema.ts
  • apps/shared/types/ui.ts
  • apps/shared/utils/premium.ts
  • apps/shared/utils/validators.ts
  • apps/shared/vitest.config.ts
  • apps/web/.wrangler/state/v3/r2/miniflare-R2BucketObject/0a1b22727b76c386299eed302e8436724750ce227c75ac3307458277e1de66e7.sqlite
  • apps/web/__tests__/components/forms/RecordForm.test.tsx
  • apps/web/__tests__/contexts/CalendarProvider.test.tsx
  • apps/web/__tests__/hooks/useRecordForm.test.ts
  • apps/web/app/(authenticated)/competition/_client/CompetitionClient.tsx
  • apps/web/app/(authenticated)/dashboard/_client/FormModals.tsx
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx
  • apps/web/app/(authenticated)/dashboard/_hooks/useCalendarData.ts
  • apps/web/app/(authenticated)/dashboard/_hooks/useCalendarHandlers.ts
  • apps/web/app/(authenticated)/dashboard/_hooks/useDashboardHandlers.ts
  • apps/web/app/(authenticated)/dashboard/_providers/CalendarProvider.tsx
  • apps/web/app/(authenticated)/settings/_client/SettingsClient.tsx
  • apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx
  • apps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsx
  • apps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_server/RecordDataLoader.tsx
  • apps/web/app/(unauthenticated)/about/page.tsx
  • apps/web/app/api/storage/images/competition/route.ts
  • apps/web/app/api/storage/images/practice/route.ts
  • apps/web/app/api/storage/videos/confirm/route.ts
  • apps/web/app/api/storage/videos/practice-log/route.ts
  • apps/web/app/api/storage/videos/presigned-url/route.ts
  • apps/web/app/api/storage/videos/record/route.ts
  • apps/web/app/api/storage/videos/team-assign/route.ts
  • apps/web/app/api/storage/videos/upload-url/route.ts
  • apps/web/app/api/stripe/checkout/route.ts
  • apps/web/app/api/stripe/portal/route.ts
  • apps/web/app/api/stripe/verify-session/route.ts
  • apps/web/app/api/subscription/status/route.ts
  • apps/web/app/page.tsx
  • apps/web/app/pricing/page.tsx
  • apps/web/app/sitemap.ts
  • apps/web/components/forms/CompetitionBasicForm.tsx
  • apps/web/components/forms/PracticeBasicForm.tsx
  • apps/web/components/forms/practice-log/PracticeLogForm.tsx
  • apps/web/components/forms/practice-log/hooks/usePracticeLogForm.ts
  • apps/web/components/forms/practice-log/types.ts
  • apps/web/components/forms/record-log/RecordLogForm.tsx
  • apps/web/components/forms/record-log/components/RecordLogEntry.tsx
  • apps/web/components/forms/record-log/hooks/useRecordLogForm.ts
  • apps/web/components/forms/record-log/types.ts
  • apps/web/components/forms/record/RecordForm.tsx
  • apps/web/components/forms/record/components/RecordSetItem.tsx
  • apps/web/components/forms/record/hooks/useRecordForm.ts
  • apps/web/components/forms/record/types.ts
  • apps/web/components/layout/Footer.tsx
  • apps/web/components/settings/SubscriptionSettings.tsx
  • apps/web/components/ui/PremiumBadge.tsx
  • apps/web/components/video/TeamVideoUploader.tsx
  • apps/web/components/video/VideoEditor.tsx
  • apps/web/components/video/VideoPlayer.tsx
  • apps/web/components/video/VideoUploader.tsx
  • apps/web/components/video/index.ts
  • apps/web/contexts/AuthProvider.tsx
  • apps/web/e2e/playwright/.auth/user.json
  • apps/web/e2e/src/tests/billing.spec.ts
  • apps/web/lib/auth-api.ts
  • apps/web/lib/cropVideo.ts
  • apps/web/lib/r2-video.ts
  • apps/web/lib/r2.ts
  • apps/web/lib/stripe.ts
  • apps/web/package.json
  • apps/web/stores/form/competitionRecordStore.ts
  • apps/web/stores/types.ts
  • apps/web/wrangler.jsonc
  • package.json
  • supabase/functions/_shared/revenucat-webhook-handler.ts
  • supabase/functions/_shared/stripe-webhook-handler.ts
  • supabase/functions/revenucat-webhook/index.ts
  • supabase/functions/stripe-webhook/index.ts
  • supabase/migrations/20260312000001_create_processed_webhook_events.sql
  • supabase/migrations/20260317000000_add_video_paths.sql
  • supabase/migrations/20260318000000_drop_video_url.sql
  • supabase/migrations/20260320000001_cleanup_webhook_events.sql
  • supabase/migrations/20260320000002_add_stripe_customer_id.sql

Comment thread apps/web/app/api/storage/videos/team-assign/route.ts Outdated
Comment thread apps/web/app/api/stripe/verify-session/route.ts
Comment thread apps/web/app/page.tsx Outdated
Comment thread apps/web/contexts/AuthProvider.tsx Outdated
Comment thread apps/web/e2e/playwright/.auth/user.json Outdated
- Updated text styles in the Home component for better readability.
- Implemented timeout handling in CompetitionDetails and PracticeDetails components to improve user experience during data loading.
- Added error handling for goal deletion and editing in GoalsClient to ensure robust cache invalidation.
- Refactored PracticeLogClient to build a logId map for better data association.
- Enhanced subscription verification logic in the Stripe API to ensure valid subscription statuses.
- Improved AuthGuard to handle hydration state more effectively, ensuring smoother user experience during authentication.
- Updated E2E tests to utilize supabaseLogin for consistent login handling and improved data loading checks.
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "http://127.0.0.1:54321";
const supabaseAnonKey =
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0";

Check warning

Code scanning / Trivy

JWT token Medium

Artifact: apps/web/e2e/src/utils/supabase-login.ts
Type:
Secret JWT token
Severity: MEDIUM
Match: "*********************************************************************************************************************************************************";
@ryuuhei0729
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

♻️ Duplicate comments (1)
apps/web/app/api/storage/videos/team-assign/route.ts (1)

75-93: ⚠️ Potential issue | 🟠 Major

practice-log 側でも targetUserId との一致を確認してください。

ここは practice_id -> teamId しか見ていないため、同じチーム内なら別メンバーの practice_logs.id でも通ります。すると別ユーザーのログ行を更新しつつ、保存先パスだけ targetUserId 配下にできてしまい、行データと動画の所属がずれます。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/storage/videos/team-assign/route.ts` around lines 75 - 93,
現在のチェックは practice_logs → practices の team_id 一致しか行っておらず、同チームの別ユーザーの
practice_logs.id を使えてしまう問題があります。最初の supabase クエリで取得している practiceLog(変数
practiceLog、変数 sourceId を使用)に対して targetUserId と一致する条件を追加し、たとえば .eq("user_id",
targetUserId) のようにして取得時点で user 所有を検証し、必要であれば存在しない場合は 403
を返すように修正してください(practiceLogs クエリと後続の practice クエリの両方を参照している箇所を更新)。
🧹 Nitpick comments (1)
apps/web/app/(authenticated)/goals/_client/GoalsClient.tsx (1)

67-69: キャッシュ無効化失敗時に、ユーザー向けエラー表示が不足しています。

現在はコンソール出力のみで、画面上では失敗が判別できません。日本語の通知(toast/inline/alert)を追加してください。

💡 最小差分例
   } catch (e) {
     console.error("キャッシュ無効化エラー:", e);
+    alert("画面の更新に失敗しました。再読み込みしてもう一度お試しください。");
   } finally {

As per coding guidelines, "Implement error state handling in components" and "Display error messages in Japanese".

Also applies to: 98-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(authenticated)/goals/_client/GoalsClient.tsx around lines 67 -
69, The catch blocks in GoalsClient.tsx that currently only call console.error
(e.g., the catch at the invalidate cache call around "catch (e) {
console.error(...); }" and the similar one at lines ~98-100) need to show a
Japanese user-facing error; replace or augment the console.error with a call to
the app's notification mechanism (e.g., toast.error or setting an error
state/Alert) with a concise Japanese message like
"キャッシュを無効化できませんでした。もう一度お試しください。" and ensure the toast/Alert component is
imported and used consistently; update both catch sites in the GoalsClient
component (the invalidate cache catch and the other catch at 98-100) so users
see the error on-screen.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx:
- Around line 81-83: loadRecords の開始時に actualRecords と competitionImages
を必ずクリアし、単に setLoading(false) を呼ぶだけのタイムアウト処理をやめて専用のタイムアウト状態(例: timedOut または
loadError)を追加して管理してください; 具体的には loadRecords 内で最初に actualRecords = [] と
competitionImages = [] をクリアし、setLoading(true) をセットし、setTimeout 側では
setTimedOut(true) と setLoading(false) を組み合わせてタイムアウトが発生したことを示すようにし、成功時には timedOut
を false に戻してタイムアウトのフラグで UI(「大会記録を追加」表示など)を分岐させるようにして下さい; また timeoutId
のクリアとコンポーネントのクリーンアップ(unmount 時の clearTimeout)を忘れずに実装し、同様の修正を CompetitionDetails
コンポーネントの他の load 部分(loadRecords を呼ぶ箇所)にも適用してください.

In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 448-489: The upload flow currently ignores non-ok responses
(uploadUrlRes, PUT to videoUploadUrl/thumbnailUploadUrl, confirm and team-assign
fetches) causing silent failures; update the upload loop in PracticeLogClient
(references: uploadUrlRes, videoUploadUrl, thumbnailUploadUrl, confirmFormData,
and the fetch calls to "/api/storage/videos/confirm" and
"/api/storage/videos/team-assign") to explicitly check response.ok for each
network call, throw or collect a descriptive error including
logId/memberId/teamId and response status/body when a call fails, and
surface/aggregate those errors to the caller or UI instead of continuing
silently. Ensure you validate the PUT responses for both video and thumbnail,
the confirm POST response, and the team-assign POST response and stop/rollback
further steps on failure.

In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 16-25: Don't trust tempVideoPath/tempThumbnailPath from the
request body: stop passing those raw values into
copyVideoInR2/deleteVideosFromR2 and avoid deleting before DB updates; instead,
resolve the server-issued temporary paths on the server (e.g., lookup the temp
object keys associated with sourceId/teamId or generate/verify the temp names
inside the route handler) and use those verified keys for copyVideoInR2; perform
deleteVideosFromR2 only after the DB update/transaction that assigns the video
to targetUserId/teamId has succeeded (and handle rollback/error so temp objects
remain intact on failure). Ensure the changes touch the route handler logic that
deserializes body (type, sourceId, targetUserId, teamId) and the code paths that
call copyVideoInR2 and deleteVideosFromR2 so they use server-verified temp paths
and post-commit deletion.

In `@apps/web/app/api/stripe/verify-session/route.ts`:
- Around line 91-109: The Supabase update call on table user_subscriptions using
supabase.from("user_subscriptions").update(...) only checks for error but not
whether a row was actually updated; change the call to append
.select("id").single() and then verify both error and that data is present (if
no data or null, return a 500 with the existing message). Also replace
updated_at: new Date().toISOString() with date-fns formatted timestamp (e.g.,
formatISO(new Date())) to follow the guideline; keep the rest of the fields and
error logging as-is.

In `@apps/web/components/profile/ProfileEditModal.tsx`:
- Line 80: Replace the direct use of err.message in the UI: in ProfileEditModal
where setError(err instanceof Error ? err.message : "プロフィールの更新に失敗しました") is
called, keep logging the raw error (e.g., processLogger.error(err) or
console.error) but set a Japanese fixed or mapped user-facing message via
setError (for example a constant "プロフィールの更新に失敗しました" or use a mapper function
that converts known error types to Japanese messages); if desired, implement a
small map function (e.g., mapProfileErrorToJapanese) and use it before calling
setError to avoid exposing raw English/internal messages.
- Around line 62-75: The current Promise.race usage (TIMEOUT, timeoutPromise,
and onUpdate({...})) only treats the timeout as a failure but does not cancel
the ongoing update, causing concurrent updates when isUpdating is cleared;
modify the code to either (A) implement true cancellation: create an
AbortController, pass its signal through onUpdate (and ensure MyPageClient
update accepts and honors an AbortSignal to abort the Supabase request) and call
controller.abort() when the timeout elapses, or (B) remove Promise.race and
instead await onUpdate without cancellation but show a timeout notification to
the user (keep isUpdating true until the real update resolves/rejects); update
the call sites that reference TIMEOUT/timeoutPromise and ensure onUpdate and the
underlying update function accept and check an AbortSignal if you choose option
A.

In `@apps/web/contexts/AuthProvider.tsx`:
- Around line 450-458: The code currently sets loading: false immediately when
updating auth state in the session handling block (see setAuthState usage),
which causes premium checks to treat subscription:null as non-premium; instead
preserve loading:true until the async subscription fetch completes (or introduce
a separate subscriptionLoading flag) and only set loading:false (or
subscriptionLoading:false) after subscription is fetched and set into
authState.subscription; apply the same change to the initial session
initialization path as well (the other setAuthState sites in the file mentioned
around the later blocks) so UI components that use premium check utilities wait
for subscription resolution before deciding premium vs free UI.

In `@apps/web/e2e/src/config/config.ts`:
- Around line 11-15: The fallback candidate in the candidates array uses
path.resolve(__dirname, "../../../..") which points to apps instead of apps/web;
update that entry to path.resolve(__dirname, "../../../") so the candidates
array (variable name: candidates) correctly includes the apps/web directory when
resolving .env files from config.ts's __dirname context.

In `@apps/web/e2e/src/config/global-setup.ts`:
- Around line 17-34: The upsert call in global-setup.ts currently ignores
Supabase's returned error object so failures can be masked; update the call to
capture the result (e.g., const { data, error } = await
supabase.from("user_subscriptions").upsert(..., { onConflict: "id" })), check if
error is truthy and rethrow it (throw error) so the outer try/catch surfaces the
failure, and only log the success message ("✅ テストユーザーを Premium プランに設定しました")
after confirming no error; apply this check around the upsert call in the same
function.

In `@apps/web/e2e/src/tests/attendance.spec.ts`:
- Around line 125-131: The test currently treats "読み込み中" as an acceptable
success condition, which masks a permanently stuck loading state; remove "読み込み中"
from the success OR checks in the attendance test (the code using
page.textContent("body") / pageText includes(...)) and instead first wait for
the loading indicator to disappear (e.g., await
page.waitForSelector('text=読み込み中', { state: 'detached' }) or wait for the
specific loading locator used in the app to be hidden), then read page content
and assert that it contains "直近の出欠" or "表示できる月がありません" or "イベントがありません"; apply the
same change to the other occurrences flagged (the other two similar checks).

In `@apps/web/e2e/src/tests/billing-e2e-manual.spec.ts`:
- Around line 203-204: The test currently records failures into testResults
(const testResults: Record<number, { status: "OK" | "NG"; note: string }> = {})
but never fails the test run; update every place that sets testResults[no] = {
status: "NG", ... } so it also fails the test immediately by throwing an Error
or calling Playwright's assertion (e.g., throw new Error(`Billing check failed
for ${no}: ${note}`) or expect(false, note)), ensuring CI detects the failure;
look for all updates to testResults (including the block around the shown
declaration and the other occurrences noted at 1347-1353) and add the
throw/expect call right after setting the NG entry.

In `@apps/web/e2e/src/tests/goals.spec.ts`:
- Around line 28-34: In createAdminClient, remove the hard-coded SUPABASE
service-role JWT and only read process.env.SUPABASE_SERVICE_ROLE_KEY; if that
env var is not set, fail fast (throw an error or stop the test/seed run) instead
of using a default, and ensure the createClient call still uses the retrieved
serviceKey and auth options; this prevents accidental leaking of admin
credentials.
- Around line 173-189: The test currently only logs goalCount and goalCountRetry
but never asserts that seeded goals exist; update the test around the goalItems
locator (selector '[class*="cursor-pointer"][class*="rounded-lg"]') to include
assertions: when goalCount > 0 assert goalCount > 0, and in the retry branch
after computing goalCountRetry assert goalCountRetry > 0 (use your test
framework's expect to fail the test if no goals are present), keeping the
existing waits (page.waitForTimeout) and logs.

In `@apps/web/e2e/src/tests/members.spec.ts`:
- Around line 148-159: The use of locator.isVisible({ timeout }) is incorrect
because isVisible ignores timeout and returns immediately; in the block that
defines hasTeamMemberManagement (using
page.locator('[data-testid="team-member-management"]') and locator.isVisible)
replace that pattern with an explicit wait such as await
expect(page.locator('[data-testid="team-member-management"]')).toBeVisible({
timeout: 15000 }) or await
page.locator('[data-testid="team-member-management"]').waitFor({ state:
"visible", timeout: 15000 }) and remove the .catch() boolean fallback logic;
apply the same replacement for the other similar checks in this file (the blocks
around the variables/locators at the other reported sections) so each visibility
check actually waits up to 15s instead of returning immediately.

In `@apps/web/e2e/src/tests/personal/competition.spec.ts`:
- Around line 40-47: The test currently hardcodes a Supabase service-role JWT in
the serviceKey constant and proceeds to call createClient; remove the embedded
JWT and require SUPABASE_SERVICE_ROLE_KEY to be set in the environment, i.e.
make serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY and if it's
undefined/empty (check the serviceKey variable) abort the test/seed startup
early (throw/return/skip) before calling createClient; update references to
supabaseUrl/serviceKey and ensure createClient(supabaseUrl, serviceKey, ...) is
only invoked when the env var is present.
- Around line 50-53: The test seeds the user by hardcoding
"e2e-test@swimhub.com" which can differ from the account used by
supabaseLogin/EnvConfig; change the lookup to use the configured E2E email (use
the same EnvConfig or E2E_EMAIL used by supabaseLogin) when finding the user
(replace the hardcoded string in the find predicate), and ensure testUser is
validated (fail early or create/seed) if that configured email is not present;
refer to supabase.auth.admin.listUsers, supabaseLogin(page), EnvConfig/E2E_EMAIL
and the testUser variable when making the change.

In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 496-504: The test (TC-PRACTICE-004) currently depends on prior
test state by accepting either 2x2 or 3x3 inputs via the hardcoded times array;
make the test deterministic by explicitly seeding the required practice log
shape at test setup (e.g., add a setup step or call a helper like
seedPracticeLogs/seedPracticeConfig to create exactly N sets × M reps expected
by this spec) and then derive the times array from that seeded shape (or assert
against the seeded size). Update the test setup (beforeEach or the start of the
TC-PRACTICE-004 test) to create the practice data programmatically so the code
paths that reference the times array and the form inputs always see the same
number of time input fields. Ensure any helper name used (seedPracticeLogs,
createPracticeConfig, or similar) is referenced where you add the call so
reviewers can locate the change.
- Around line 29-35: The code in createAdminClient embeds a hard-coded service
role JWT in the serviceKey fallback (const serviceKey) which must be removed;
instead require SUPABASE_SERVICE_ROLE_KEY from environment and if it's not set,
make setup/seeding skip or fail early (e.g., throw or return a sentinel from the
routine that calls createAdminClient) so high-privilege keys are never
committed. Update createAdminClient to only read
process.env.SUPABASE_SERVICE_ROLE_KEY (no default literal), ensure callers check
for its presence and handle missing key by aborting or skipping seed operations,
and add a clear error/log message when the env var is absent.

In `@apps/web/e2e/src/tests/schedule.spec.ts`:
- Around line 307-315: The code uses locator.isVisible({ timeout: 15000 }) which
ignores the timeout and does not wait; replace calls like
practicesHeader.isVisible({ timeout: 15000 }) with an explicit wait such as
await expect(practicesHeader).toBeVisible({ timeout: 15000 }) or await
practicesHeader.waitFor({ state: "visible", timeout: 15000 }) and keep the same
branching logic, and make the same replacement for the other occurrences flagged
(the similar locator variables used around the other comment ranges).

In `@apps/web/e2e/src/utils/supabase-login.ts`:
- Line 2: EnvConfig.getTestEnvironment() is coupling credential resolution to
E2E_BASE_URL and can throw/fallback when baseUrl is missing; change the login
utility to read E2E_EMAIL and E2E_PASSWORD directly (or via a dedicated
EnvConfig.getCredentials() helper) instead of calling
EnvConfig.getTestEnvironment(), leaving Playwright's baseURL management
separate; update the code paths that currently call
EnvConfig.getTestEnvironment() in this file (and the related block around lines
24-32) to obtain only credentials and not depend on baseUrl so tests don't
silently fall back to defaults or skip.

---

Duplicate comments:
In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 75-93: 現在のチェックは practice_logs → practices の team_id
一致しか行っておらず、同チームの別ユーザーの practice_logs.id を使えてしまう問題があります。最初の supabase クエリで取得している
practiceLog(変数 practiceLog、変数 sourceId を使用)に対して targetUserId と一致する条件を追加し、たとえば
.eq("user_id", targetUserId) のようにして取得時点で user 所有を検証し、必要であれば存在しない場合は 403
を返すように修正してください(practiceLogs クエリと後続の practice クエリの両方を参照している箇所を更新)。

---

Nitpick comments:
In `@apps/web/app/`(authenticated)/goals/_client/GoalsClient.tsx:
- Around line 67-69: The catch blocks in GoalsClient.tsx that currently only
call console.error (e.g., the catch at the invalidate cache call around "catch
(e) { console.error(...); }" and the similar one at lines ~98-100) need to show
a Japanese user-facing error; replace or augment the console.error with a call
to the app's notification mechanism (e.g., toast.error or setting an error
state/Alert) with a concise Japanese message like
"キャッシュを無効化できませんでした。もう一度お試しください。" and ensure the toast/Alert component is
imported and used consistently; update both catch sites in the GoalsClient
component (the invalidate cache catch and the other catch at 98-100) so users
see the error on-screen.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2cfea1b5-6ef1-4a12-99c1-0b397722c823

📥 Commits

Reviewing files that changed from the base of the PR and between 86e61d0 and 0aec516.

📒 Files selected for processing (27)
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx
  • apps/web/app/(authenticated)/goals/_client/GoalsClient.tsx
  • apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx
  • apps/web/app/api/storage/videos/team-assign/route.ts
  • apps/web/app/api/stripe/verify-session/route.ts
  • apps/web/app/page.tsx
  • apps/web/components/auth/AuthGuard.tsx
  • apps/web/components/forms/CompetitionBasicForm.tsx
  • apps/web/components/profile/ProfileEditModal.tsx
  • apps/web/components/settings/SubscriptionSettings.tsx
  • apps/web/contexts/AuthProvider.tsx
  • apps/web/e2e/playwright/.auth/user.json
  • apps/web/e2e/src/config/config.ts
  • apps/web/e2e/src/config/global-setup.ts
  • apps/web/e2e/src/config/playwright.config.ts
  • apps/web/e2e/src/tests/attendance.spec.ts
  • apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
  • apps/web/e2e/src/tests/goals.spec.ts
  • apps/web/e2e/src/tests/members.spec.ts
  • apps/web/e2e/src/tests/mypage.spec.ts
  • apps/web/e2e/src/tests/personal/competition.spec.ts
  • apps/web/e2e/src/tests/personal/practice.spec.ts
  • apps/web/e2e/src/tests/schedule.spec.ts
  • apps/web/e2e/src/tests/teams.spec.ts
  • apps/web/e2e/src/utils/supabase-login.ts
  • apps/web/lib/supabase-auth/middleware.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/e2e/playwright/.auth/user.json
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx
  • apps/web/app/page.tsx
  • apps/web/components/forms/CompetitionBasicForm.tsx

Comment on lines +81 to +83
const timeoutId = setTimeout(() => {
setLoading(false);
}, 15000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find apps/web -name "CompetitionDetails.tsx" -type f

Repository: ryuuhei0729/swim-hub

Length of output: 182


🏁 Script executed:

git ls-files apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/

Repository: ryuuhei0729/swim-hub

Length of output: 525


🏁 Script executed:

cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | head -250

Repository: ryuuhei0729/swim-hub

Length of output: 10361


🏁 Script executed:

cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | sed -n '250,360p'

Repository: ryuuhei0729/swim-hub

Length of output: 5206


🏁 Script executed:

cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | sed -n '290,340p'

Repository: ryuuhei0729/swim-hub

Length of output: 2280


🏁 Script executed:

# Check if there are any other places where actualRecords/competitionImages are reset
grep -n "setActualRecords\|setCompetitionImages" apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx

Repository: ryuuhei0729/swim-hub

Length of output: 387


タイムアウト時のloading状態の扱いを改善してください。

loadRecords() の開始時に actualRecordscompetitionImages をリセットしていないため、15秒でタイムアウトが発火した場合、データ取得がまだ完了していないのに loading=false になってしまいます。その結果、前回の大会データが画面に残ったままになるか、初回表示では「大会記録を追加」ボタンが誤って表示されます。

タイムアウト時の状態をより明確に区別するために、専用の loadError または timedOut 状態を追加し、 actualRecords をロード開始時にクリアすることを推奨します。

修正案
+  const [loadError, setLoadError] = useState<string | null>(null);

   useEffect(() => {
-    const timeoutId = setTimeout(() => {
-      setLoading(false);
-    }, 15000);
+    const timeoutId = window.setTimeout(() => {
+      setLoadError("読み込みがタイムアウトしました。再度お試しください。");
+      setLoading(false);
+    }, 15000);

     const loadRecords = async () => {
       try {
         setLoading(true);
+        setLoadError(null);
+        setActualRecords([]);
+        setCompetitionImages([]);

コーディングガイドラインの「Display appropriate loading states in components」および「Implement error state handling in components」に対応してください。

適用箇所:198-200行目、210-213行目も同様の修正が必要です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
around lines 81 - 83, loadRecords の開始時に actualRecords と competitionImages
を必ずクリアし、単に setLoading(false) を呼ぶだけのタイムアウト処理をやめて専用のタイムアウト状態(例: timedOut または
loadError)を追加して管理してください; 具体的には loadRecords 内で最初に actualRecords = [] と
competitionImages = [] をクリアし、setLoading(true) をセットし、setTimeout 側では
setTimedOut(true) と setLoading(false) を組み合わせてタイムアウトが発生したことを示すようにし、成功時には timedOut
を false に戻してタイムアウトのフラグで UI(「大会記録を追加」表示など)を分岐させるようにして下さい; また timeoutId
のクリアとコンポーネントのクリーンアップ(unmount 時の clearTimeout)を忘れずに実装し、同様の修正を CompetitionDetails
コンポーネントの他の load 部分(loadRecords を呼ぶ箇所)にも適用してください.

Comment thread apps/web/app/api/storage/videos/team-assign/route.ts
Comment on lines +91 to +109
const { error } = await supabase
.from("user_subscriptions")
.update({
plan: "premium",
status,
provider: "stripe",
provider_subscription_id: subscription.id,
premium_expires_at: unixToISO(subscription.current_period_end),
current_period_start: unixToISO(subscription.current_period_start),
cancel_at_period_end: subscription.cancel_at_period_end,
trial_start: unixToISO(subscription.trial_start),
trial_end: unixToISO(subscription.trial_end),
updated_at: new Date().toISOString(),
})
.eq("id", userId);

if (error) {
console.error("Supabase update error:", error);
return NextResponse.json({ error: "サブスクリプションの更新に失敗しました" }, { status: 500 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/app/api/stripe/verify-session/route.ts | head -150

Repository: ryuuhei0729/swim-hub

Length of output: 4880


🏁 Script executed:

# Check if date-fns is used elsewhere in the codebase for timestamp creation
fd -e ts -e tsx apps/web | xargs rg "new Date\(\)\.toISOString\(\)" -A 2 -B 2 | head -40

Repository: ryuuhei0729/swim-hub

Length of output: 3235


🏁 Script executed:

# Check date-fns usage patterns in the codebase
fd -e ts -e tsx apps/web | xargs rg "formatISO|toISOString|format.*date" -l | head -10

Repository: ryuuhei0729/swim-hub

Length of output: 870


🏁 Script executed:

# Check if there are examples of how Supabase updates are handled elsewhere
rg "\.update\(" apps/web -A 3 -B 1 | head -50

Repository: ryuuhei0729/swim-hub

Length of output: 2672


🏁 Script executed:

# Check Supabase documentation behavior patterns and check if select() is actually used with updates
rg "\.update\(" apps/web --context 5 | grep -A 5 "\.select\(\)"

Repository: ryuuhei0729/swim-hub

Length of output: 46


🏁 Script executed:

# Check if upsert is used elsewhere in the codebase for similar patterns
rg "\.upsert\(" apps/web -A 3 | head -50

Repository: ryuuhei0729/swim-hub

Length of output: 636


🏁 Script executed:

# Check for any row count verification patterns in the codebase
rg "\.single\(\)" apps/web -B 2 -A 2 | head -30

Repository: ryuuhei0729/swim-hub

Length of output: 1570


🏁 Script executed:

# Check the Supabase Firebase Edge Function for similar patterns
cat -n supabase/functions/_shared/stripe-webhook-handler.ts | grep -A 20 "\.update\(\|\.upsert\("

Repository: ryuuhei0729/swim-hub

Length of output: 87


🏁 Script executed:

# Verify the exact behavior: look for how update() is typically handled
rg "\.update\(" supabase/functions/_shared --context 7 | head -60

Repository: ryuuhei0729/swim-hub

Length of output: 4954


🏁 Script executed:

# Check the package.json to confirm date-fns is available
cat apps/web/package.json | grep -A 2 -B 2 "date-fns"

Repository: ryuuhei0729/swim-hub

Length of output: 252


🏁 Script executed:

# Check if there's a specific pattern for how updated_at timestamps are typically set in the codebase
rg "updated_at" apps/web --context 2 | grep -A 2 -B 2 "new Date\|formatISO\|fromNow" | head -30

Repository: ryuuhei0729/swim-hub

Length of output: 1604


🏁 Script executed:

# Check if there's a pattern for how row count verification should work in Supabase operations
rg "count\(\)" apps/web --context 2 | head -20

Repository: ryuuhei0729/swim-hub

Length of output: 1436


🏁 Script executed:

# Check database schema for user_subscriptions to understand if row is guaranteed to exist
fd . --full-path "supabase/migrations" -name "*.sql" | head -10

Repository: ryuuhei0729/swim-hub

Length of output: 294


🏁 Script executed:

# Find migration files related to user_subscriptions
find supabase -name "*.sql" | xargs grep -l "user_subscriptions" | head -5

Repository: ryuuhei0729/swim-hub

Length of output: 314


🏁 Script executed:

# Check if there's an initialization trigger or constraint that ensures user_subscriptions row exists for each user
cat supabase/migrations/*.sql 2>/dev/null | grep -A 10 "user_subscriptions" | head -50

Repository: ryuuhei0729/swim-hub

Length of output: 1918


🏁 Script executed:

# Check the migration that creates user_subscriptions for new users
cat supabase/migrations/20260313000000_handle_new_user_add_subscription.sql

Repository: ryuuhei0729/swim-hub

Length of output: 1092


🏁 Script executed:

# Verify the exact coding guideline on date-fns and when to use it
rg "date-fns" --context 3 < /dev/null || echo "Checking instructions..."
cat /dev/null

Repository: ryuuhei0729/swim-hub

Length of output: 50376


🏁 Script executed:

# Check how other API routes handle Supabase updates for critical operations
rg "api/stripe" apps/web --context 1 -l

Repository: ryuuhei0729/swim-hub

Length of output: 232


UPDATE後の行数検証がありません

Supabase の update() は既定ではエラー情報のみ返すため、この実装では premium 状態が実際に保存されたか確認できません。重要な購読更新処理なので、.select("id").single() を追加して少なくとも 1 行が更新されたことを確認してください。

また、同時に updated_at: new Date().toISOString() は date-fns を使用してください(ガイドラインに従う)。

💡 修正例
  const { error } = await supabase
    .from("user_subscriptions")
    .update({
      plan: "premium",
      status,
      provider: "stripe",
      provider_subscription_id: subscription.id,
      premium_expires_at: unixToISO(subscription.current_period_end),
      current_period_start: unixToISO(subscription.current_period_start),
      cancel_at_period_end: subscription.cancel_at_period_end,
      trial_start: unixToISO(subscription.trial_start),
      trial_end: unixToISO(subscription.trial_end),
      updated_at: new Date().toISOString(),
    })
    .eq("id", userId);

-  if (error) {
+  if (error || !data || data.length === 0) {
     console.error("Supabase update error:", error);
     return NextResponse.json({ error: "サブスクリプションの更新に失敗しました" }, { status: 500 });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/stripe/verify-session/route.ts` around lines 91 - 109, The
Supabase update call on table user_subscriptions using
supabase.from("user_subscriptions").update(...) only checks for error but not
whether a row was actually updated; change the call to append
.select("id").single() and then verify both error and that data is present (if
no data or null, return a 500 with the existing message). Also replace
updated_at: new Date().toISOString() with date-fns formatted timestamp (e.g.,
formatISO(new Date())) to follow the guideline; keep the rest of the fields and
error logging as-is.

Comment on lines +62 to +75
// タイムアウト付きで更新を実行
const TIMEOUT = 15000;
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("更新がタイムアウトしました")), TIMEOUT)
);
await Promise.race([
onUpdate({
name: formData.name.trim(),
birthday,
gender: formData.gender,
bio: formData.bio.trim() || null,
}),
timeoutPromise,
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd ProfileEditModal.tsx

Repository: ryuuhei0729/swim-hub

Length of output: 164


🏁 Script executed:

find apps/web/components/profile -name "*.tsx" -o -name "*.ts" | head -20

Repository: ryuuhei0729/swim-hub

Length of output: 298


🏁 Script executed:

find apps/web/app -path "*mypage*" -name "*Client*.tsx" | head -20

Repository: ryuuhei0729/swim-hub

Length of output: 124


🏁 Script executed:

wc -l apps/web/components/profile/ProfileEditModal.tsx

Repository: ryuuhei0729/swim-hub

Length of output: 116


🏁 Script executed:

cat -n apps/web/components/profile/ProfileEditModal.tsx

Repository: ryuuhei0729/swim-hub

Length of output: 9881


🏁 Script executed:

wc -l apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx

Repository: ryuuhei0729/swim-hub

Length of output: 128


🏁 Script executed:

sed -n '90,140p' apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx | cat -n

Repository: ryuuhei0729/swim-hub

Length of output: 2227


🏁 Script executed:

sed -n '1,50p' apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx | cat -n

Repository: ryuuhei0729/swim-hub

Length of output: 1914


🏁 Script executed:

rg "queryClient|useCallback|handleProfileUpdate" apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx -A 2 -B 1

Repository: ryuuhei0729/swim-hub

Length of output: 1684


Promise.race のタイムアウトだけでは更新が止まらず、再送が競合する可能性があります。

Promise.race は負けた onUpdate(...) を中断しないため、15秒後にタイムアウトで失敗扱いになっても、MyPageClient.tsx の Supabase 更新処理は裏で継続します。finallyisUpdatingfalse に戻るため、ユーザーが再送すると古い更新と新しい更新が同時実行され、どちらが先に完了するかで最終的なプロフィール状態が決まります。真のキャンセル処理を実装するか、タイムアウトを通知に変更する方が安全です。

💡 安全側の最小修正例
-      const timeoutPromise = new Promise<never>((_, reject) =>
-        setTimeout(() => reject(new Error("更新がタイムアウトしました")), TIMEOUT)
-      );
-      await Promise.race([
-        onUpdate({
-          name: formData.name.trim(),
-          birthday,
-          gender: formData.gender,
-          bio: formData.bio.trim() || null,
-        }),
-        timeoutPromise,
-      ]);
+      const timeoutId = window.setTimeout(() => {
+        setError(
+          "更新に時間がかかっています。通信状況をご確認のうえ、そのままお待ちください。",
+        );
+      }, TIMEOUT);
+      try {
+        await onUpdate({
+          name: formData.name.trim(),
+          birthday,
+          gender: formData.gender,
+          bio: formData.bio.trim() || null,
+        });
+      } finally {
+        window.clearTimeout(timeoutId);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/profile/ProfileEditModal.tsx` around lines 62 - 75, The
current Promise.race usage (TIMEOUT, timeoutPromise, and onUpdate({...})) only
treats the timeout as a failure but does not cancel the ongoing update, causing
concurrent updates when isUpdating is cleared; modify the code to either (A)
implement true cancellation: create an AbortController, pass its signal through
onUpdate (and ensure MyPageClient update accepts and honors an AbortSignal to
abort the Supabase request) and call controller.abort() when the timeout
elapses, or (B) remove Promise.race and instead await onUpdate without
cancellation but show a timeout notification to the user (keep isUpdating true
until the real update resolves/rejects); update the call sites that reference
TIMEOUT/timeoutPromise and ensure onUpdate and the underlying update function
accept and check an AbortSignal if you choose option A.

Comment thread apps/web/e2e/src/tests/personal/competition.spec.ts
Comment thread apps/web/e2e/src/tests/personal/competition.spec.ts
Comment thread apps/web/e2e/src/tests/personal/practice.spec.ts
Comment thread apps/web/e2e/src/tests/personal/practice.spec.ts
Comment thread apps/web/e2e/src/utils/supabase-login.ts Outdated
ryuhei.hosoi and others added 3 commits March 25, 2026 18:36
- Changed loading state from "networkidle" to "domcontentloaded" in openDateModal function for better synchronization.
- Updated date selection logic to ensure visibility checks are more robust, enhancing test reliability.
- Added detailed error messages for video and thumbnail upload failures, including member names and HTTP status codes.
- Implemented an alert to notify users of any upload errors after processing all video files.
- Improved error handling in the subscription verification logic to ensure proper feedback when no subscription record is found.
- Add load error state and timeout notification in CompetitionDetails
- Validate server-side paths and reorder delete-after-DB-update in team-assign route
- Replace Promise.race with proper timeout handling in ProfileEditModal
- Keep auth loading state until subscription fetch completes in AuthProvider
- Fix E2E config path resolution, add upsert error handling in global-setup
- Remove "読み込み中" from success conditions in attendance tests
- Add recordResult helper to fail tests on NG in billing E2E
- Add missing assertion in goals spec, use env vars for E2E credentials
- Decouple supabase-login from EnvConfig.getTestEnvironment()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ryuuhei0729
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- Simplified date selection logic in openDateModal by removing unnecessary visibility checks.
- Added skip condition for Stripe tests when STRIPE_SECRET_KEY is not set, improving test reliability.
- Updated trial badge and plan management tests to use more efficient locator methods for better performance.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

♻️ Duplicate comments (3)
apps/web/e2e/src/tests/personal/practice.spec.ts (1)

497-502: ⚠️ Potential issue | 🟠 Major

TC-PRACTICE-004 が前テストの更新結果に依存したままです。

ここで 2×2 と 3×3 の両方を許容している限り、単体実行や retry で期待する入力マトリクスが変わり続けます。TC-PRACTICE-004 の先頭で必要な practice_log 形状をシードし直して、このテスト単体で固定した前提を持たせた方がよいです。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/e2e/src/tests/personal/practice.spec.ts` around lines 497 - 502,
TC-PRACTICE-004 currently relies on prior test state; seed the required
practice_log shape at the start of that test (or in test.beforeEach for the
TC-PRACTICE-004 describe/block) so the test always runs with a fixed matrix
(e.g., create a practice_log with the expected sets×reps and timestamps or
clear-and-create the record used by the UI). Specifically, add a setup step that
POSTs/creates the practice_log fixture or calls the existing helper that sets
sets/reps to the exact dimensions your assertions expect, then proceed to
interact with the time input fields—this removes dependency on TC-PRACTICE-003
and stabilizes retries. Ensure you reference and update the practice_log used by
the TC-PRACTICE-004 test block rather than reading whatever previous state
exists.
apps/web/app/api/storage/videos/team-assign/route.ts (1)

74-94: ⚠️ Potential issue | 🔴 Critical

practice-log 側でも targetUserId を更新条件に含めてください。

この分岐は practice_logs.idpractices.team_id しか見ていないので、同じチーム内の別メンバーの practice_logs.id を渡すと、その行に動画パスを書き換えられます。user_id === targetUserId の検証を追加して、更新クエリ側でも同じ条件を掛けておかないと誤紐付けを防ぎきれません。

修正イメージ
-      const { data: practiceLog } = await supabase
+      const { data: practiceLog, error: practiceLogError } = await supabase
         .from("practice_logs")
-        .select("id, practice_id")
+        .select("id, user_id, practice_id")
         .eq("id", sourceId)
+        .eq("user_id", targetUserId)
         .maybeSingle();
+
+      if (practiceLogError) {
+        console.error("練習ログ確認エラー:", practiceLogError);
+        return NextResponse.json({ error: "練習ログの確認に失敗しました" }, { status: 500 });
+      }
       const { error } = await supabase
         .from("practice_logs")
         .update({ video_path: finalVideoPath, video_thumbnail_path: finalThumbnailPath })
-        .eq("id", sourceId);
+        .eq("id", sourceId)
+        .eq("user_id", targetUserId);

Also applies to: 129-132

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/storage/videos/team-assign/route.ts` around lines 74 - 94,
The current checks for practice_logs only filter by practice_logs.id and
practices.team_id, so add the targetUserId equality to the practice_logs query
and to the subsequent update condition: when querying practice_logs (variable
practiceLog) include .eq("user_id", targetUserId) and when validating/updating
ensure the update query for the video path also includes .eq("user_id",
targetUserId) so only the target user's practice_log row can be read/modified
(referencing practiceLog, practice, and targetUserId).
apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx (1)

420-447: ⚠️ Potential issue | 🟠 Major

log_ids 不足時に動画を黙って捨てないでください。

動画が選択されているのに result.log_ids が無い、または件数が足りない場合、ここは continue で抜けるだけなので、保存成功に見えたまま一部の動画だけ落ちます。選択動画数と log_ids の件数を先に突き合わせて、不足分は videoUploadErrors に積んで明示したほうが安全です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx
around lines 420 - 447, The code currently silently skips videos when
result.log_ids is missing/short; before building logIdMap (or immediately after
obtaining logIds), compute the expected number of log entries (sum of
targetMembers across menus with videoFiles) and compare to logIds.length, and
for any missing entries push descriptive messages into videoUploadErrors (use
the same keys like `${mi}_${member.user_id}` or count mismatches) so missing
thumbnails/logIds are reported instead of being dropped; update the later loop
that looks up logId (where logIdMap.get(`${i}_${memberId}`) is used) to record
an error into videoUploadErrors when logId is undefined rather than just
continue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 69-70: The submit flow currently only processes new files in
menu.videoFiles / videoThumbnails and calls replace_practice_logs which
recreates logs, so existing video_path / video_thumbnail_path on existing logs
get dropped; fix by preserving and reassigning existing paths during
submit—e.g., when building the payload in PracticeLogClient, read existing
log.video_path and log.video_thumbnail_path into the form state (or include them
in the menu item shape) and copy them into the replacement entries if no new
File/Blob is provided, or alternately detect logs that already have
video_path/video_thumbnail_path and avoid doing a full replace_practice_logs for
those entries so their paths are retained. Ensure you update the codepaths that
reference menu.videoFiles, videoThumbnails, replace_practice_logs, video_path,
and video_thumbnail_path.

In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 37-55: The Supabase queries (adminMembership, targetMembership,
record, practiceLog, practice) currently only read the data field and ignore the
returned error; after each .select(...).maybeSingle() call (for adminMembership
and targetMembership) and the other Supabase queries later, check the returned
error first and if present return a NextResponse.json with a 500 status
(including an appropriate error message), otherwise proceed to use the data;
update the blocks referencing adminMembership, targetMembership, record,
practiceLog and practice to destructure both data and error from the query
result and branch on error before treating null data as “not found” or
“forbidden.”

In `@apps/web/e2e/src/config/config.ts`:
- Around line 17-19: The dotenv loading loop over candidates currently calls
dotenv.config for each dir which doesn't overwrite existing process.env, so
earlier-loaded root .env wins; update the logic in the loop that iterates over
candidates to enforce the intended precedence by either (A) loading only the
first matching candidate (break after the successful dotenv.config) or (B)
calling dotenv.config with { override: true } for the later files so
apps/web/.env.local can replace root values; change the calls to dotenv.config({
path: path.resolve(dir, ".env.local"), override: true }) / dotenv.config({ path:
path.resolve(dir, ".env"), override: true }) or implement the early-exit
behavior on the first found candidate to ensure consistent env priority (modify
the loop that references candidates and the two dotenv.config calls).

In `@apps/web/e2e/src/tests/billing-e2e-manual.spec.ts`:
- Around line 738-745: The test computes is409 from the API call but doesn't use
it in the pass/fail decision, so a broken duplicate-purchase guard could be
missed; update the recordResult call (the condition argument currently using
!hasSelectBtn && hasManage) to also require is409 (i.e., !hasSelectBtn &&
hasManage && is409) and keep the diagnostic message that includes apiStatus;
locate the call to page.request.post("/api/stripe/checkout") / apiResponse /
is409 and modify the recordResult invocation to include is409 in the boolean
condition.
- Around line 30-36: getAdminClient currently returns a client even when
SUPABASE_SERVICE_ROLE_KEY is empty and createPremiumUser ignores Supabase
errors, causing failed premium seeds to fall back to free users; update
getAdminClient to validate SUPABASE_SERVICE_ROLE_KEY and throw a clear error if
unset, and wrap Supabase calls in createPremiumUser (and any code touching
user_subscriptions upsert) with try-catch, checking the upsert response.error
and throwing if non-null so failures stop the test flow immediately rather than
returning a free user.
- Around line 791-798: The test currently considers the scenario successful
using only stillOnStripe; update the assertion to require both an error message
and that the page is still on Stripe by combining hasError and stillOnStripe
(e.g., passCondition = hasError && stillOnStripe) before calling recordResult;
reference the existing variables errorMsg, hasError, stillOnStripe and
recordResult so you locate the change point and update the recorded message to
include both flags.
- Around line 9-12: newCleanPage creates a BrowserContext without baseURL so
subsequent relative navigations (e.g., page.goto("/pricing") and
page.goto("/dashboard")) fail; update newCleanPage to call browser.newContext
with the baseURL set (e.g., browser.newContext({ baseURL:
"http://localhost:3000" })) and likewise change any other manual
browser.newContext() usages in this file (including the anonymous contexts used
around page.goto calls) to use the same baseURL or refactor into a shared helper
that centralizes newContext({ baseURL: ... }) so all relative page.goto calls
work.

In `@apps/web/e2e/src/tests/goals.spec.ts`:
- Around line 281-314: The test currently treats missing edit buttons or dialogs
as a skipped-success by logging and returning; instead assert failures so CI
catches regressions: remove the console.log(...) + return branches that bypass
checks for editButtonVisible/pageEditVisible and dialogVisible, and replace them
with explicit assertions (e.g., using expect) referencing the existing
locators/vars (firstGoalCard, editButton, pageEditButton, dialogVisible,
goalItems) so the test fails when the edit button or dialog is not present or
visible rather than silently returning.
- Around line 66-77: The beforeAll seeding currently returns on Supabase errors
or missing user which leaves the suite green; wrap Supabase calls (e.g.,
supabase.auth.admin.listUsers and any user insert) inside try-catch and on any
caught error call throw new Error(...) (or use Jest's fail) so beforeAll
rejects, and explicitly throw when testUser is not found (instead of return) to
fail the suite; update references in this file to ensure listUsers results and
insertion failures propagate by throwing with a clear message.

In `@apps/web/e2e/src/tests/members.spec.ts`:
- Around line 148-159: The current early-return when team-member-management
isn't found (the hasTeamMemberManagement check using
page.locator('[data-testid="team-member-management"]')) lets CI pass silently if
the UI is broken; instead, only skip when the team truly doesn't exist (the
prior teamId/tab navigation check), and replace the console.log(...) + return
with an explicit assertion like
expect(page.locator('[data-testid="team-member-management"]')).toBeVisible() so
failures are caught; update the same pattern where used elsewhere (the other
hasTeamMemberManagement-like blocks) to assert visibility rather than quietly
returning.

In `@apps/web/e2e/src/tests/personal/competition.spec.ts`:
- Around line 50-55: The beforeAll is silently returning when seeding or
resolving the test user fails (see supabase.auth.admin.listUsers(), testUser
variable), which causes tests to incorrectly pass; wrap the Supabase calls in
try-catch and, instead of returning, throw or call the test runner's fail (or
expect) with a clear error message including testEmail and context (e.g., "E2E
seed: user not found" or the caught error), and apply the same change to the
other similar blocks at the lines referenced so the suite fails fast on seed or
insert errors.

In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 70-81: The beforeAll seed logic (in practice.spec.ts) currently
swallows Supabase errors by logging and returning, so replace those early
returns with explicit test failures and add try/catch around Supabase calls
(e.g., supabase.auth.admin.listUsers, any insert/update calls referenced at
lines ~137-140, ~190-193, ~221-224) inside the beforeAll; on catch or when
required data (testUser) is missing, call the test framework fail/assertion (or
throw an Error) with a descriptive message instead of console.error+return so
the suite fails fast when seeding fails. Ensure you update all occurrences where
console.error(...); return; is used during seeding to use the same try/catch +
throw/fail behavior.

In `@apps/web/e2e/src/tests/schedule.spec.ts`:
- Around line 307-314: 現在のチェック treats missing UI as acceptable by falling back
to URL check using practicesHeader, isHeaderVisible and expect(page.url()),
which masks regressions; replace this fallback with hard assertions so the test
fails if required UI is absent. Specifically, in the block using practicesHeader
locator and isHeaderVisible (and similar blocks at the other ranges), remove the
silent catch/console.log and instead assert visibility of the header
(practicesHeader.waitFor / expect(practicesHeader).toBeVisible()) and assert
presence/visibility of the schedule action buttons (e.g., locate and expect the
"練習記録追加" and "大会追加" buttons to be visible) so the test fails immediately when
those UI elements are missing rather than passing on URL-only checks. Ensure any
thrown errors are allowed to surface (do not swallow with .catch(() => false) or
return) so CI fails on UI regressions.

In `@apps/web/e2e/src/utils/supabase-login.ts`:
- Around line 84-119: The cookie chunking uses standard base64 and a uniform
`base64-` prefix which doesn't match `@supabase/ssr`; change to base64url (replace
+/ with -_ and strip padding) and emit the library's metadata on the first
chunk: compute encodedSession as base64url (e.g.,
Buffer.from(sessionData).toString('base64') then replace and strip '=' or use
base64url), set the first cookie value to
`base64l-<originalLength>-<firstChunkData>` (use cookie name cookieBaseName for
single or first chunk) and subsequent chunks as the raw chunk data (no `base64-`
prefix) with names cookieBaseName.1, cookieBaseName.2, etc.; update the code
paths that build chunks (encodedSession, chunkSize, chunks) and the
page.context().addCookies calls to follow this format so `@supabase/ssr` can
reconstruct the session.

---

Duplicate comments:
In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 420-447: The code currently silently skips videos when
result.log_ids is missing/short; before building logIdMap (or immediately after
obtaining logIds), compute the expected number of log entries (sum of
targetMembers across menus with videoFiles) and compare to logIds.length, and
for any missing entries push descriptive messages into videoUploadErrors (use
the same keys like `${mi}_${member.user_id}` or count mismatches) so missing
thumbnails/logIds are reported instead of being dropped; update the later loop
that looks up logId (where logIdMap.get(`${i}_${memberId}`) is used) to record
an error into videoUploadErrors when logId is undefined rather than just
continue.

In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 74-94: The current checks for practice_logs only filter by
practice_logs.id and practices.team_id, so add the targetUserId equality to the
practice_logs query and to the subsequent update condition: when querying
practice_logs (variable practiceLog) include .eq("user_id", targetUserId) and
when validating/updating ensure the update query for the video path also
includes .eq("user_id", targetUserId) so only the target user's practice_log row
can be read/modified (referencing practiceLog, practice, and targetUserId).

In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 497-502: TC-PRACTICE-004 currently relies on prior test state;
seed the required practice_log shape at the start of that test (or in
test.beforeEach for the TC-PRACTICE-004 describe/block) so the test always runs
with a fixed matrix (e.g., create a practice_log with the expected sets×reps and
timestamps or clear-and-create the record used by the UI). Specifically, add a
setup step that POSTs/creates the practice_log fixture or calls the existing
helper that sets sets/reps to the exact dimensions your assertions expect, then
proceed to interact with the time input fields—this removes dependency on
TC-PRACTICE-003 and stabilizes retries. Ensure you reference and update the
practice_log used by the TC-PRACTICE-004 test block rather than reading whatever
previous state exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: deccc478-bde8-43ac-b41e-6b39d68a1252

📥 Commits

Reviewing files that changed from the base of the PR and between 0aec516 and 1d203ff.

📒 Files selected for processing (16)
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
  • apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx
  • apps/web/app/api/storage/videos/team-assign/route.ts
  • apps/web/app/api/stripe/verify-session/route.ts
  • apps/web/components/profile/ProfileEditModal.tsx
  • apps/web/contexts/AuthProvider.tsx
  • apps/web/e2e/src/config/config.ts
  • apps/web/e2e/src/config/global-setup.ts
  • apps/web/e2e/src/tests/attendance.spec.ts
  • apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
  • apps/web/e2e/src/tests/goals.spec.ts
  • apps/web/e2e/src/tests/members.spec.ts
  • apps/web/e2e/src/tests/personal/competition.spec.ts
  • apps/web/e2e/src/tests/personal/practice.spec.ts
  • apps/web/e2e/src/tests/schedule.spec.ts
  • apps/web/e2e/src/utils/supabase-login.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/e2e/src/tests/attendance.spec.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/e2e/src/config/global-setup.ts
  • apps/web/components/profile/ProfileEditModal.tsx
  • apps/web/contexts/AuthProvider.tsx
  • apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
  • apps/web/app/api/stripe/verify-session/route.ts

Comment thread apps/web/app/api/storage/videos/team-assign/route.ts
Comment thread apps/web/e2e/src/config/config.ts
Comment thread apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
Comment thread apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
Comment thread apps/web/e2e/src/tests/members.spec.ts
Comment thread apps/web/e2e/src/tests/personal/competition.spec.ts
Comment thread apps/web/e2e/src/tests/personal/practice.spec.ts
Comment thread apps/web/e2e/src/tests/schedule.spec.ts
Comment thread apps/web/e2e/src/utils/supabase-login.ts
ryuhei.hosoi added 3 commits March 25, 2026 21:29
…dSetItem and RecordLogEntry components

- Updated RecordSetItem and RecordLogEntry components to include data-testid attributes for the premium badge, enhancing testability.
- Adjusted E2E tests to verify the presence of the premium badge using both data-testid and text search methods for improved reliability.
…onents

- Added useEffect hooks in CalendarView and DatePicker to set current year and month on the client side, preventing hydration mismatches.
- Streamlined date formatting in TeamCompetitionForm and TeamPracticeForm for consistency.
- Improved loading state handling in AuthGuard to ensure proper rendering during authentication checks.
- Updated E2E tests to ensure robust handling of record saving and editing scenarios.
- Added multiple versions of picomatch (2.3.2, 3.0.2, 4.0.4) to package.json and pnpm-lock.yaml for improved compatibility.
- Enhanced metadata across various pages by adding canonical URLs to improve SEO and page indexing.
@ryuuhei0729
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/forms/record-log/components/RecordLogEntry.tsx (1)

363-371: ⚠️ Potential issue | 🟡 Minor

アクセシビリティ改善: aria-label を追加してください。

削除ボタンにはアイコンのみが表示されており、スクリーンリーダーユーザーにはボタンの目的が伝わりません。

♿ 修正案
               <button
                 type="button"
                 onClick={() => onRemoveSplitTime(originalIndex)}
                 className="p-2 text-red-600 hover:text-red-700"
                 disabled={isLoading}
                 data-testid={`record-split-remove-button-${sectionIndex}-${splitIndex + 1}`}
+                aria-label={`スプリットタイム ${splitIndex + 1} を削除`}
               >
                 <TrashIcon className="h-5 w-5" />
               </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx` around
lines 363 - 371, The delete button in RecordLogEntry.tsx lacks an accessible
name for screen readers; update the <button> element (used with
onRemoveSplitTime, originalIndex, sectionIndex, splitIndex, isLoading) to
include a descriptive aria-label such as "Remove split {splitIndex + 1}" (or a
localized equivalent) so the button's purpose is exposed to assistive
technologies while preserving existing props (type, onClick, disabled,
data-testid).
🧹 Nitpick comments (3)
apps/web/components/forms/record-log/components/RecordLogEntry.tsx (1)

391-399: 冗長なnullish coalescingを削除してください。

isPremium は関数シグネチャで既にデフォルト値 false が設定されているため(75行目)、isPremium ?? false は冗長です。

♻️ 修正案
         <VideoUploader
           type="record"
           id={recordId && isDbUuid(recordId) ? recordId : undefined}
           existingVideoPath={videoPath ?? undefined}
           existingThumbnailPath={videoThumbnailPath ?? undefined}
-          isPremium={isPremium ?? false}
+          isPremium={isPremium}
           onUploadComplete={(vPath, tPath) => onVideoPathChange(vPath, tPath)}
           onDelete={onVideoDelete}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx` around
lines 391 - 399, Remove the redundant nullish coalescing for the isPremium prop
passed to the VideoUploader component: since the parent component's function
signature already provides a default value for isPremium (false), change the
prop from isPremium ?? false to simply isPremium in the VideoUploader JSX
instance to avoid unnecessary fallback logic.
apps/web/components/forms/record/components/RecordSetItem.tsx (1)

317-317: 冗長な nullish coalescing の削除を検討してください。

isPremium は line 55 ですでにデフォルト値 false が設定されているため、isPremium ?? false は冗長です。

♻️ 修正案
-            isPremium={isPremium ?? false}
+            isPremium={isPremium}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/forms/record/components/RecordSetItem.tsx` at line 317,
The prop passing uses redundant nullish coalescing: change the call site that
sets isPremium={isPremium ?? false} in the RecordSetItem component to simply
isPremium={isPremium} since isPremium already has a default of false in the
component's props (defined where isPremium default is set). Remove the "??
false" to eliminate redundancy without changing behavior.
apps/web/components/team/TeamCompetitionForm.tsx (1)

29-36: フォーム初期値の重複を1箇所に集約してください。

初期 formData と成功時/クローズ時リセットで同じオブジェクトが3回定義されており、将来の項目追加時に差分漏れが発生しやすいです。createInitialFormData() の関数化を推奨します。

Also applies to: 79-86, 99-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/team/TeamCompetitionForm.tsx` around lines 29 - 36,
Extract the initial form object into a single factory function
createInitialFormData() that returns { title: "", date: format(new Date(),
"yyyy-MM-dd"), endDate: "", place: "", poolType: 0, note: "" }; use
createInitialFormData() as the argument to useState to initialize formData and
call setFormData(createInitialFormData()) wherever the form is reset (e.g., on
successful submit and on close) so the initial shape is defined in one place and
stays consistent with future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/team/TeamCompetitionForm.tsx`:
- Line 31: The direct calls to format(new Date(), "yyyy-MM-dd") in
TeamCompetitionForm.tsx must be replaced with a safe helper that validates the
Date before formatting; create or use a getTodayDate() helper which constructs
new Date(), checks isValid(date) from date-fns, and returns the formatted string
(or a sensible fallback) and then replace every occurrence of format(new Date(),
"yyyy-MM-dd") in TeamCompetitionForm (the initial form default and the other two
spots where that exact call appears) with getTodayDate(). Ensure you reference
and import the helper where needed and keep the formatting string "yyyy-MM-dd"
inside the helper so callers no longer call format() directly on a raw Date.

In `@apps/web/components/ui/DatePicker.tsx`:
- Around line 85-87: Guard against invalid dates by validating defaultMonth with
date-fns isValid before calling startOfMonth or using it to initialize
currentMonth; update the code paths that call startOfMonth(defaultMonth) (and
the conditional that falls back to new Date(0)) to first check
isValid(defaultMonth) and only use startOfMonth(defaultMonth) when valid,
otherwise fall back to startOfMonth(new Date(0)) and ensure any later checks
(the conditional around defaultMonth at the later branch) also use
isValid(defaultMonth) to decide fallback.

---

Outside diff comments:
In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx`:
- Around line 363-371: The delete button in RecordLogEntry.tsx lacks an
accessible name for screen readers; update the <button> element (used with
onRemoveSplitTime, originalIndex, sectionIndex, splitIndex, isLoading) to
include a descriptive aria-label such as "Remove split {splitIndex + 1}" (or a
localized equivalent) so the button's purpose is exposed to assistive
technologies while preserving existing props (type, onClick, disabled,
data-testid).

---

Nitpick comments:
In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx`:
- Around line 391-399: Remove the redundant nullish coalescing for the isPremium
prop passed to the VideoUploader component: since the parent component's
function signature already provides a default value for isPremium (false),
change the prop from isPremium ?? false to simply isPremium in the VideoUploader
JSX instance to avoid unnecessary fallback logic.

In `@apps/web/components/forms/record/components/RecordSetItem.tsx`:
- Line 317: The prop passing uses redundant nullish coalescing: change the call
site that sets isPremium={isPremium ?? false} in the RecordSetItem component to
simply isPremium={isPremium} since isPremium already has a default of false in
the component's props (defined where isPremium default is set). Remove the "??
false" to eliminate redundancy without changing behavior.

In `@apps/web/components/team/TeamCompetitionForm.tsx`:
- Around line 29-36: Extract the initial form object into a single factory
function createInitialFormData() that returns { title: "", date: format(new
Date(), "yyyy-MM-dd"), endDate: "", place: "", poolType: 0, note: "" }; use
createInitialFormData() as the argument to useState to initialize formData and
call setFormData(createInitialFormData()) wherever the form is reset (e.g., on
successful submit and on close) so the initial shape is defined in one place and
stays consistent with future changes.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b625bfe-6a6c-4532-904e-8bd9bc68b013

📥 Commits

Reviewing files that changed from the base of the PR and between 1d203ff and 4a0a4c8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • apps/web/app/(authenticated)/dashboard/_components/CalendarView.tsx
  • apps/web/app/(unauthenticated)/about/page.tsx
  • apps/web/app/(unauthenticated)/blog/page.tsx
  • apps/web/app/(unauthenticated)/blog/tag/[tag]/page.tsx
  • apps/web/app/(unauthenticated)/contact/layout.tsx
  • apps/web/app/(unauthenticated)/login/layout.tsx
  • apps/web/app/(unauthenticated)/privacy/page.tsx
  • apps/web/app/(unauthenticated)/reset-password/layout.tsx
  • apps/web/app/(unauthenticated)/signup/layout.tsx
  • apps/web/app/(unauthenticated)/support/page.tsx
  • apps/web/app/(unauthenticated)/terms/page.tsx
  • apps/web/app/(unauthenticated)/tokushoho/page.tsx
  • apps/web/app/(unauthenticated)/update-password/layout.tsx
  • apps/web/components/auth/AuthGuard.tsx
  • apps/web/components/forms/record-log/components/RecordLogEntry.tsx
  • apps/web/components/forms/record/components/RecordSetItem.tsx
  • apps/web/components/team/TeamCompetitionForm.tsx
  • apps/web/components/team/TeamPracticeForm.tsx
  • apps/web/components/ui/DatePicker.tsx
  • apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
  • package.json
✅ Files skipped from review due to trivial changes (11)
  • apps/web/app/(unauthenticated)/update-password/layout.tsx
  • apps/web/app/(unauthenticated)/blog/page.tsx
  • apps/web/app/(unauthenticated)/reset-password/layout.tsx
  • apps/web/app/(unauthenticated)/contact/layout.tsx
  • apps/web/app/(unauthenticated)/login/layout.tsx
  • apps/web/app/(unauthenticated)/signup/layout.tsx
  • apps/web/app/(unauthenticated)/terms/page.tsx
  • apps/web/components/team/TeamPracticeForm.tsx
  • apps/web/app/(unauthenticated)/privacy/page.tsx
  • apps/web/app/(unauthenticated)/tokushoho/page.tsx
  • apps/web/app/(unauthenticated)/support/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/components/auth/AuthGuard.tsx
  • package.json
  • apps/web/e2e/src/tests/billing-e2e-manual.spec.ts

title: "",
date: format(new Date(), "yyyy-MM-dd"), // ローカル日付のYYYY-MM-DD形式
endDate: "", // 終了日(複数日開催の場合)
date: format(new Date(), "yyyy-MM-dd"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/components/team/TeamCompetitionForm.tsx | head -110

Repository: ryuuhei0729/swim-hub

Length of output: 3614


format() の前に isValid() チェックを入れてください。

Line 31、81、101 で format(new Date(), "yyyy-MM-dd") を直接呼んでおり、日付妥当性チェックのガイドラインに未準拠です。getTodayDate() ヘルパーで集約すると安全性と一貫性が上がります。

修正案(diff)
-import { format, parseISO } from "date-fns";
+import { format, isValid, parseISO } from "date-fns";
+
+const getTodayDate = () => {
+  const now = new Date();
+  return isValid(now) ? format(now, "yyyy-MM-dd") : "";
+};

 const [formData, setFormData] = useState({
   title: "",
-  date: format(new Date(), "yyyy-MM-dd"),
+  date: getTodayDate(),
   endDate: "",
   place: "",
   poolType: 0,
   note: "",
 });

     setFormData({
       title: "",
-      date: format(new Date(), "yyyy-MM-dd"),
+      date: getTodayDate(),
       endDate: "",
       place: "",
       poolType: 0,
       note: "",
     });

   const handleClose = () => {
     if (!loading) {
       onClose();
       setFormData({
         title: "",
-        date: format(new Date(), "yyyy-MM-dd"),
+        date: getTodayDate(),
         endDate: "",
         place: "",
         poolType: 0,
         note: "",
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/team/TeamCompetitionForm.tsx` at line 31, The direct
calls to format(new Date(), "yyyy-MM-dd") in TeamCompetitionForm.tsx must be
replaced with a safe helper that validates the Date before formatting; create or
use a getTodayDate() helper which constructs new Date(), checks isValid(date)
from date-fns, and returns the formatted string (or a sensible fallback) and
then replace every occurrence of format(new Date(), "yyyy-MM-dd") in
TeamCompetitionForm (the initial form default and the other two spots where that
exact call appears) with getTodayDate(). Ensure you reference and import the
helper where needed and keep the formatting string "yyyy-MM-dd" inside the
helper so callers no longer call format() directly on a raw Date.

Comment on lines +85 to +87
if (defaultMonth) return startOfMonth(defaultMonth);
// SSR時はエポック起点を仮設定し、useEffectでクライアント側の現在月に更新
return startOfMonth(new Date(0));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

defaultMonth 未検証で Invalid Date が state に入り、描画時に例外化する可能性があります。

Line 85 で defaultMonth を無条件で startOfMonth に渡しており、不正な Date が来ると currentMonth が壊れます。さらに Line 92 の条件も defaultMonth の妥当性を見ないため、フォールバックに入れません。isValid でガードしてください。

修正案(例)
   const [currentMonth, setCurrentMonth] = useState<Date>(() => {
     if (value) {
       const date = typeof value === "string" ? parseISO(value) : value;
       if (isValid(date)) return startOfMonth(date);
     }
-    if (defaultMonth) return startOfMonth(defaultMonth);
+    if (defaultMonth && isValid(defaultMonth)) return startOfMonth(defaultMonth);
     // SSR時はエポック起点を仮設定し、useEffectでクライアント側の現在月に更新
     return startOfMonth(new Date(0));
   });

   // クライアント側でのみ現在月を設定(Hydration Mismatch回避)
   useEffect(() => {
-    if (!value && !defaultMonth) {
+    if (!value && (!defaultMonth || !isValid(defaultMonth))) {
       setCurrentMonth(startOfMonth(new Date()));
     }
-  }, []); // eslint-disable-line react-hooks/exhaustive-deps
+  }, [value, defaultMonth]);

Based on learnings: Applies to **/*.{ts,tsx,js,jsx} : Always check date validity with isValid() before calling format() from date-fns.

Also applies to: 90-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/ui/DatePicker.tsx` around lines 85 - 87, Guard against
invalid dates by validating defaultMonth with date-fns isValid before calling
startOfMonth or using it to initialize currentMonth; update the code paths that
call startOfMonth(defaultMonth) (and the conditional that falls back to new
Date(0)) to first check isValid(defaultMonth) and only use
startOfMonth(defaultMonth) when valid, otherwise fall back to startOfMonth(new
Date(0)) and ensure any later checks (the conditional around defaultMonth at the
later branch) also use isValid(defaultMonth) to decide fallback.

ryuhei.hosoi added 2 commits March 26, 2026 15:43
…yout

- Updated practices, records, and teams queries to ensure proper handling of undefined or non-array data.
- Refactored layout components to adjust the order of AuthProvider and QueryProvider for better context management.
- Replaced dynamic imports with static imports for several components to enhance performance and reduce bundle size.
- Improved loading state management in dashboard hooks and forms for better user experience.
…ization

- Changed loading state from "networkidle" to "domcontentloaded" in multiple E2E test scenarios to enhance synchronization and reliability during page interactions.
- Ensured consistent loading behavior across various components, particularly in the billing and competition features.
@ryuuhei0729 ryuuhei0729 changed the base branch from main to develop March 31, 2026 12:21
- Added RevenueCat API key to app configuration for subscription management.
- Updated app version to 1.4.0 in app.json.
- Removed unused React and Expo dependencies from package.json and pnpm-lock.yaml.
- Enhanced mobile navigation by adding a Paywall screen and integrating it into the Settings screen for premium upgrades.
- Improved holiday utility to dynamically load holiday data for better performance.
- Adjusted Metro configuration for better module resolution in a monorepo setup.
anonKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0',
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0",

Check warning

Code scanning / Trivy

JWT token Medium

Artifact: apps/web/e2e/src/config/playwright.config.ts
Type:
Secret JWT token
Severity: MEDIUM
Match: "*********************************************************************************************************************************************************",
serviceRoleKey:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU',
}
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU",

Check warning

Code scanning / Trivy

JWT token Medium

Artifact: apps/web/e2e/src/config/playwright.config.ts
Type:
Secret JWT token
Severity: MEDIUM
Match: "********************************************************************************************************************************************************************",
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@apps/mobile/app.config.js`:
- Around line 36-38: The current defaulting of webApiUrl to
"https://swim-hub.app" causes non-production runs to accidentally hit
production; change the logic around the webApiUrl/native environment detection
so that webApiUrl is only defaulted to the production URL when
process.env.EXPO_PUBLIC_ENVIRONMENT === "production" (use the existing
environment variable), and for any non-production environment
(development/staging) fail-fast instead (throw an error or exit) if webApiUrl is
not provided; look for the webApiUrl variable and the environment usage in the
app.config.js export and implement the conditional fallback + fail-fast behavior
there.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9aa5b9ff-b417-4763-960e-76b8541f7c6a

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0a4c8 and a63a957.

📒 Files selected for processing (18)
  • .gitignore
  • apps/mobile/app.config.js
  • apps/mobile/app.json
  • apps/mobile/components/shared/PremiumBadge.tsx
  • apps/mobile/contexts/AuthProvider.tsx
  • apps/mobile/eas.json
  • apps/mobile/lib/revenucat.ts
  • apps/mobile/metro.config.js
  • apps/mobile/navigation/MainStack.tsx
  • apps/mobile/navigation/types.ts
  • apps/mobile/package.json
  • apps/mobile/screens/PaywallScreen.tsx
  • apps/mobile/screens/SettingsScreen.tsx
  • apps/mobile/scripts/ensure-react-version.js
  • apps/shared/hooks/queries/practices.ts
  • apps/shared/hooks/queries/records.ts
  • apps/shared/hooks/queries/teams.ts
  • apps/shared/utils/holiday.ts
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • apps/mobile/components/shared/PremiumBadge.tsx

Comment thread apps/mobile/app.config.js Outdated
Comment on lines +36 to +38
webApiUrl: webApiUrl || "https://swim-hub.app",
revenuecatIosApiKey: revenuecatIosApiKey,
environment: process.env.EXPO_PUBLIC_ENVIRONMENT || "development",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

非本番で webApiUrl 未設定時に本番APIへフォールバックします

Line 36 の固定フォールバックにより、開発/検証環境の設定漏れで本番APIへ接続するリスクがあります。非本番は fail-fast にしてください。

💡 修正例
 const webApiUrl = process.env.EXPO_PUBLIC_WEB_API_URL;
+const environment = process.env.EXPO_PUBLIC_ENVIRONMENT || "development";
+
+if (!webApiUrl && environment !== "production") {
+  throw new Error("EXPO_PUBLIC_WEB_API_URL is required outside production");
+}

 module.exports = {
   ...baseConfig.expo,
   extra: {
@@
-    webApiUrl: webApiUrl || "https://swim-hub.app",
+    webApiUrl: webApiUrl || "https://swim-hub.app",
     revenuecatIosApiKey: revenuecatIosApiKey,
-    environment: process.env.EXPO_PUBLIC_ENVIRONMENT || "development",
+    environment: environment,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/mobile/app.config.js` around lines 36 - 38, The current defaulting of
webApiUrl to "https://swim-hub.app" causes non-production runs to accidentally
hit production; change the logic around the webApiUrl/native environment
detection so that webApiUrl is only defaulted to the production URL when
process.env.EXPO_PUBLIC_ENVIRONMENT === "production" (use the existing
environment variable), and for any non-production environment
(development/staging) fail-fast instead (throw an error or exit) if webApiUrl is
not provided; look for the webApiUrl variable and the environment usage in the
app.config.js export and implement the conditional fallback + fail-fast behavior
there.

ryuhei.hosoi and others added 9 commits April 2, 2026 18:23
…ent structure

- Removed direct usage of Expo Constants for environment variables, replacing them with a centralized env module for better maintainability.
- Updated CalendarHeader component to improve loading state handling and button arrangement.
- Adjusted various components to utilize the new env module for accessing configuration values, enhancing consistency across the application.
- Improved AppState management in QueryProvider to ensure proper refetching behavior when the app returns from the background.
- Add scripts/** to eslint ignores for apps/mobile (fixes lint parse error)
- Update Next.js 16.1.6 → 16.2.2 (fixes 5 CVEs including CSRF bypass, HTTP smuggling)
- Update undici override >=7.24.0 → >=7.24.7 (fixes 5 WebSocket DoS CVEs)
- Add overrides for node-forge, @xmldom/xmldom, path-to-regexp (fixes 6 high vulnerabilities)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed conditional rendering of the loading indicator in the Email login button, streamlining the component structure.
- Ensured consistent button content display for improved user experience.
The xmldom >=0.8.12 override introduced a breaking change where
DOMParser.parseFromString() requires an explicit mimeType argument.
@expo/plist calls it without mimeType, causing iOS build failure.
This vulnerability must be fixed upstream by Expo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: multiple React instances (19.1.0 in root/shared, 19.2.3 in web)
caused renderHook to return null, failing 375/599 tests.

Changes:
- Unify React to 19.2.3 in all packages (web, mobile, shared)
- Upgrade vitest 1.6.1 → 3.2.4, @vitest/coverage-v8 1.6.0 → 3.2.4
- Upgrade jsdom 24.1.3 → 26.x for React 19 compatibility
- Simplify vitest.config.ts (remove unnecessary aliases and base config indirection)
- Add IS_REACT_ACT_ENVIRONMENT flag in vitest.setup.ts
- Fix Mock<T> generics for vitest 3.x in useImageUpload.test.ts

Result: 24/24 test files pass, 599/599 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…st scrolling logic

- Removed outdated text from iOSCalendarSyncSettings to streamline user information.
- Enhanced scrolling synchronization in TeamMemberList by clarifying comments and disabling unnecessary scroll event handling for improved performance.
…nt and legal links

- Added a cancellation note and legal links (Terms of Service and Privacy Policy) to the PaywallScreen.
- Implemented a restore purchases feature in the SettingsScreen, including a loading state and a link to manage subscriptions.
- Improved user experience by providing clear access to legal information and subscription management options.
- Increased safety timeout for loading state from 10 seconds to 15 seconds to ensure proper session initialization.
- Simplified onAuthStateChange handling by removing redundant session fetching and improving asynchronous subscription management.
- Ensured that loading state is cleared immediately upon session change, while handling RevenueCat login and subscription fetching asynchronously to avoid blocking UI updates.
- Added error handling for RevenueCat login and subscription fetching to improve robustness.
- Upgraded Expo and related packages to version 55.0.11 for enhanced features and performance.
- Updated React and React DOM to version 19.2.4 across the monorepo to ensure consistency.
- Enhanced AuthProvider with improved session management and error handling.
- Refactored app configuration files for better structure and maintainability.
- Added new environment variables for Supabase integration in eas.json.
- Improved module resolution in Metro configuration for better compatibility in a monorepo setup.
Comment thread apps/mobile/eas.json
"EXPO_PUBLIC_ENVIRONMENT": "production",
"EXPO_PUBLIC_REVENUCAT_IOS_API_KEY": "appl_YadgyFdSryuxgGOGiXAaabdkYNl",
"EXPO_PUBLIC_SUPABASE_URL": "https://kolhxtkxlkkorcfzyadk.supabase.co",
"EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtvbGh4dGt4bGtrb3JjZnp5YWRrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY4MDI2NzYsImV4cCI6MjA3MjM3ODY3Nn0.yOKJ7Vt3lZf_-b0vuT_re-RBUlH0QgPD811f7LzZRE4",

Check warning

Code scanning / Trivy

JWT token Medium

Artifact: apps/mobile/eas.json
Type:
Secret JWT token
Severity: MEDIUM
Match: O_PUBLIC_SUPABASE_ANON_KEY": "****************************************************************************************************************************************************************************************************************",
ryuhei.hosoi added 3 commits April 6, 2026 23:35
- Removed fullscreen and picture-in-picture options from VideoPlayer for a simplified user experience.
- Replaced benefits list with a PlanComparisonTable in PaywallScreen to enhance subscription clarity and presentation.
- Updated styles to reflect the new comparison section layout.
- Changed the icon type in Sidebar from ComponentType<SVGProps<SVGSVGElement>> to React.ElementType for improved flexibility.
- Refactored the loading animation styles in LoadingSpinner to use dangerouslySetInnerHTML for better performance and cleaner code.
…ption handling

- Updated AuthProvider to fetch subscription information directly from Supabase, addressing token expiration issues and simplifying error handling.
- Improved subscription state management in AuthProvider, ensuring proper updates on user changes.
- Added a check in PaywallScreen to display a message for users already on the Premium plan, enhancing user experience and clarity.
@ryuuhei0729 ryuuhei0729 changed the base branch from develop to main April 8, 2026 08:22
@ryuuhei0729 ryuuhei0729 merged commit 91bd1b5 into main Apr 8, 2026
14 checks passed
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.

2 participants