# 通知設計書 13-同期リセット検出

## 概要

本ドキュメントは、Actualアプリケーションにおける「同期リセット検出」エラー通知の設計仕様を定義する。この通知は、クラウドファイルで同期がリセットされたことを検出した場合に表示される。

### 本通知の処理概要

この通知は、同期処理中にサーバー上のファイルで同期リセットが行われた、または新しい暗号化キーが設定されたことを検出した場合（file-has-reset または file-has-new-key エラー）に発行される。ユーザーは、サーバーのバージョンに戻すか、ローカルファイルをアップロードするかを選択できる。

**業務上の目的・背景**：複数デバイスで同じ予算ファイルを使用している場合、あるデバイスで同期リセットが実行されると、他のデバイスのローカルデータとサーバーデータの間に不整合が発生する。この通知は、そのような状況を検出し、ユーザーに適切な対処方法を提示することで、データの整合性を維持し、意図しないデータ損失を防止する目的がある。

**通知の送信タイミング**：同期処理（fullSync）の実行時に、サーバーから`file-has-reset`または`file-has-new-key`エラーが返却された場合に発行される。具体的には、ローカルのgroupIdまたはencryptKeyIdがサーバー上の値と一致しない場合にトリガーされる。

**通知の受信者**：現在アプリケーションを操作しているユーザー。通知はアプリ内のUIに直接表示されるため、ログイン中のユーザーが受信者となる。

**通知内容の概要**：クラウドファイルで同期がリセットされたことを伝え、同期を継続するためにはリバート（サーバーバージョンに戻す）が必要であることを説明する。また、代替手段としてローカルファイルのアップロードオプションを提供する。

**期待されるアクション**：ユーザーは「Revert」ボタンをクリックしてサーバーバージョンに戻すか、メッセージ内の「upload」リンクをクリックしてローカルファイルをアップロードすることが期待される。未同期データがある場合は、アップロードを選択してデータを保持することが推奨される。

## 通知種別

アプリ内通知（In-App Notification）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（イベントリスナー経由） |
| 優先度 | 高（error） |
| リトライ | 無し（ユーザーアクション依存） |

### 送信先決定ロジック

通知は現在アクティブなブラウザ/アプリウィンドウに表示される。特定のユーザー選定ロジックは存在せず、同期エラーが発生したセッションに対して直接通知が発行される。

## 通知テンプレート

### アプリ内通知の場合

| 項目 | 内容 |
|-----|------|
| 通知タイプ | error |
| タイトル | Syncing has been reset on this cloud file |
| sticky | true（手動で閉じるまで表示継続） |
| 識別ID | needs-revert |

### 本文テンプレート

```
You need to revert it to continue syncing. Any unsynced data will be lost. If you like, you can instead
[upload this file](#upload) to be the latest version.
[Learn more](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)
```

### ボタン

| ボタン名 | アクション |
|---------|----------|
| Revert | closeAndDownloadBudget実行（サーバーバージョンをダウンロード） |

### メッセージ内リンクアクション

| リンクID | アクション |
|---------|----------|
| #upload | resetSync実行（ローカルファイルをアップロード） |

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| 該当なし | 固定テキストのため変数なし | - | - |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| イベント | sync-event (type: error, subtype: file-has-reset) | groupIdの不一致 | サーバーでgroupIdがリセットされた |
| イベント | sync-event (type: error, subtype: file-has-new-key) | encryptKeyIdの不一致 | サーバーで新しい暗号化キーが設定された |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 予算ファイル未読み込み | prefs.local.idが未設定の場合、通知は発行されない |
| 同一ID通知が表示中 | id: 'needs-revert' の通知が既に表示中の場合、重複発行されない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[同期処理開始] --> B{groupId/keyId一致?}
    B -->|不一致| C[file-has-reset/file-has-new-key エラー発生]
    B -->|一致| D[通常の同期処理継続]
    C --> E[sync-event エミット]
    E --> F[listenForSyncEvent がキャッチ]
    F --> G[cloudFileId取得]
    G --> H[通知オブジェクト作成]
    H --> I[addNotification ディスパッチ]
    I --> J[UI に通知表示]
    J --> K{ユーザーアクション}
    K -->|Revert クリック| L[closeAndDownloadBudget 実行]
    L --> M[サーバーからファイルダウンロード]
    M --> N[ローカルファイル置換]
    K -->|upload リンク| O[resetSync 実行]
    O --> P[ローカルファイルアップロード]
    K -->|閉じるボタン| Q[通知削除]
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| metadata (JSON) | cloudFileId, groupId の確認 | ファイルシステム上の metadata.json |
| Redux state | prefs.local.cloudFileId | Revertボタン用 |

### テーブル別参照項目詳細

#### metadata.json

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| cloudFileId | クラウドファイルID | 通知作成時に参照 |
| groupId | 同期グループID | 同期処理時に比較 |
| encryptKeyId | 暗号化キーID | 同期処理時に比較 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| metadata.json | UPDATE/REPLACE | Revert時にサーバーバージョンで置換 |
| db.sqlite | REPLACE | Revert時にサーバーバージョンで置換 |
| messages_crdt | DELETE/INSERT | 同期リセット時にCRDTメッセージを再構築 |

#### Revert実行時の更新

| 操作 | 項目 | 更新値 | 備考 |
|-----|------|-------|------|
| REPLACE | db.sqlite | サーバーバージョン | ダウンロードしたファイル |
| UPDATE | groupId | サーバーの値 | メタデータ更新 |
| UPDATE | encryptKeyId | サーバーの値 | メタデータ更新 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| ダウンロード失敗 | Revert時にネットワークエラー | エラーメッセージ表示、リトライ促進 |
| 復号化失敗 | 暗号化キーが不一致 | キー入力モーダル表示 |
| アップロード失敗 | upload選択時にエラー | エラーメッセージ表示 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 自動リトライなし |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（同一IDの重複防止のみ） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

時間帯制限なし（イベント発生時に即座に表示）

## セキュリティ考慮事項

- Revert時にユーザートークンによる認証を実施
- ダウンロードされたファイルが暗号化されている場合、ローカルキーで復号化
- キーが不一致の場合は、パスワード入力を要求

## 備考

- この通知はstickyフラグがtrueのため、ユーザーが手動で閉じるまで表示が継続される
- 「Revert」は未同期データを失う可能性があることを警告している
- 「upload」リンクは現在のローカルデータを優先する選択肢を提供
- file-has-resetとfile-has-new-keyは同じ通知で処理される

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

通知オブジェクトの型定義と、同期イベントの型定義を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notificationsSlice.ts | `packages/desktop-client/src/notifications/notificationsSlice.ts` | messageActionsプロパティの構造（23行目） |
| 1-2 | server-events.ts | `packages/loot-core/src/types/server-events.ts` | file-has-reset, file-has-new-key定義（15-16行目） |

**読解のコツ**: messageActionsはメッセージ内のリンク（#xxx形式）に対応するアクションを定義する。

#### Step 2: エントリーポイントを理解する

同期イベントのリスナー登録と通知発行の起点を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | sync-events.ts | `packages/desktop-client/src/sync-events.ts` | file-has-reset/file-has-new-keyケースの処理（246-274行目） |

**主要処理フロー**:
1. **246-247行目**: case 'file-has-reset' と case 'file-has-new-key' の共通処理
2. **252行目**: cloudFileIdをRedux stateから取得
3. **254-256行目**: タイトルとメッセージの設定（uploadリンク含む）
4. **264行目**: messageActions定義（uploadアクション）
5. **267-272行目**: Revertボタンのアクション定義

#### Step 3: Revert処理層を理解する

closeAndDownloadBudget処理の詳細を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | budgetfilesSlice.ts | `packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts` | closeAndDownloadBudget関数 |
| 3-2 | cloud-storage.ts | `packages/loot-core/src/server/cloud-storage.ts` | download関数（405-467行目） |

**主要処理フロー**:
- **405行目**: download関数開始
- **409-425行目**: ファイルダウンロード処理
- **456-464行目**: 復号化処理（暗号化されている場合）
- **466行目**: importBufferでローカルに保存

#### Step 4: Upload処理層を理解する

resetSync処理（uploadオプション）の詳細を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | appSlice.ts | `packages/desktop-client/src/app/appSlice.ts` | resetSync関数（53-93行目） |

**主要処理フロー**:
- **53行目**: resetSync thunkの定義
- **56行目**: send('sync-reset')でサーバーAPIを呼び出し

### プログラム呼び出し階層図

```
listenForSyncEvent (sync-events.ts:23)
    │
    ├─ listen('sync-event') (sync-events.ts:42)
    │      │
    │      └─ case 'file-has-reset' / 'file-has-new-key' (sync-events.ts:246)
    │             │
    │             └─ addNotification dispatch (sync-events.ts:371-375)
    │
    ├─ button.action [Revert] (sync-events.ts:268)
    │      │
    │      └─ closeAndDownloadBudget (budgetfilesSlice.ts)
    │             │
    │             └─ download (cloud-storage.ts:405)
    │                    │
    │                    ├─ fetch /download-user-file
    │                    ├─ decrypt (暗号化時)
    │                    └─ importBuffer
    │
    └─ messageActions.upload (sync-events.ts:264)
           │
           └─ resetSync (appSlice.ts:53)
```

### データフロー図

```
[入力]                          [処理]                          [出力]

サーバー応答                  sync-events.ts                   UI表示
(file-has-reset) ───▶ listenForSyncEvent ───▶ addNotification ───▶ Notifications.tsx
                              │
                              ▼
                   [Revertボタンクリック]
                              │
                              ▼
                   closeAndDownloadBudget
                              │
                              ▼
                   cloud-storage.ts:download
                              │
                              ▼
サーバーファイル ───▶ ダウンロード ───▶ ローカルファイル置換
                   (復号化)

                   ─── または ───

                   [uploadリンククリック]
                              │
                              ▼
ローカルファイル ───▶ resetSync ───▶ サーバーへアップロード
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| sync-events.ts | `packages/desktop-client/src/sync-events.ts` | ソース | 同期イベントリスナー、通知発行 |
| notificationsSlice.ts | `packages/desktop-client/src/notifications/notificationsSlice.ts` | ソース | 通知状態管理 |
| Notifications.tsx | `packages/desktop-client/src/components/Notifications.tsx` | ソース | 通知UI表示、messageActions処理 |
| budgetfilesSlice.ts | `packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts` | ソース | closeAndDownloadBudget |
| appSlice.ts | `packages/desktop-client/src/app/appSlice.ts` | ソース | resetSyncアクション |
| cloud-storage.ts | `packages/loot-core/src/server/cloud-storage.ts` | ソース | download, upload関数 |
| server-events.ts | `packages/loot-core/src/types/server-events.ts` | 型定義 | SyncSubtype定義 |
