# 機能設計書 49-Electronアプリ

## 概要

本ドキュメントは、Actualの「Electronアプリ」機能の設計内容を記載する。Windows/Mac/Linux向けのデスクトップアプリケーションについて、アーキテクチャと処理内容を定義する。

### 本機能の処理概要

Electronアプリは、Actualをデスクトップアプリケーションとして動作させるプラットフォーム機能である。Chromiumベースのレンダラープロセスとメインプロセスで構成され、ネイティブ機能へのアクセスを提供する。

**業務上の目的・背景**：Webアプリケーションをデスクトップ環境で利用可能にし、ネイティブのファイルシステムアクセス、ファイルダイアログ、システム統合を提供する。オフライン対応やローカルサーバー統合により、自己ホスト環境での利用を容易にする。

**機能の利用シーン**：Windows/Mac/Linuxでの予算管理、ローカル環境でのオフライン利用、ファイルのインポート/エクスポート、システムトレイ統合時に利用される。

**主要な処理内容**：
1. Electronウィンドウの作成と管理
2. IPC通信（レンダラー⇔メイン）
3. バックグラウンドサーバープロセスの管理
4. ファイルダイアログ（開く/保存）
5. OAuthサーバー（OpenID認証用）
6. プロトコルハンドラー（app://）
7. ウィンドウ状態の永続化

**関連システム・外部連携**：Electron、Chromium、Node.js、loot-coreサーバー

**権限による制御**：OSのファイルシステム権限に依存

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | すべての画面 | 基盤機能 | Electronウィンドウ内でレンダリング |

## 機能種別

プラットフォーム・基盤機能

## 入力仕様

### 入力パラメータ

本機能はプラットフォーム基盤のため、直接的なユーザー入力パラメータはない。

### 入力データソース

- 環境変数（ACTUAL_DOCUMENT_DIR、ACTUAL_DATA_DIR）
- ウィンドウ状態ファイル
- グローバル設定ファイル（global-store.json）
- コマンドライン引数（--server）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| ウィンドウ | BrowserWindow | メインウィンドウ |
| サーバープロセス | UtilityProcess | バックグラウンドサーバー |
| 同期サーバープロセス | UtilityProcess | 内蔵同期サーバー（オプション） |

### 出力先

- デスクトップ画面（ウィンドウ表示）
- ログ（メインプロセスおよびレンダラープロセス）

## 処理フロー

### 処理シーケンス

```
1. アプリケーション起動（app.on('ready')）
   ├─ グローバル設定読み込み
   ├─ 同期サーバー自動起動（設定時）
   ├─ app://プロトコルハンドラー登録
   ├─ メインウィンドウ作成
   └─ バックグラウンドサーバープロセス起動

2. ウィンドウ作成（createWindow）
   ├─ ウィンドウ状態復元
   ├─ BrowserWindow作成
   ├─ preloadスクリプト設定
   ├─ 開発時/本番時のURL読み込み
   └─ メニュー設定

3. IPC通信設定
   ├─ get-bootstrap-data
   ├─ start-sync-server / stop-sync-server
   ├─ start-oauth-server
   ├─ open-file-dialog / save-file-dialog
   ├─ open-external-url
   └─ move-budget-directory

4. アプリ終了処理
   ├─ サーバープロセス終了
   └─ ウィンドウ状態保存
```

### フローチャート

```mermaid
flowchart TD
    A[Electron起動] --> B[app.on ready]
    B --> C[loadGlobalPrefs]
    C --> D{syncServer.autoStart?}

    D -->|Yes| E[startSyncServer]
    D -->|No| F[continue]

    E --> F
    F --> G[protocol.handle app://]
    G --> H{--server引数?}

    H -->|Yes| I[サーバーモードで起動]
    H -->|No| J[createWindow]

    J --> K[getWindowState]
    K --> L[new BrowserWindow]
    L --> M{isDev?}

    M -->|Yes| N[localhost:3001 読み込み]
    M -->|No| O[app://actual/ 読み込み]

    N --> P[createBackgroundProcess]
    O --> P

    P --> Q[utilityProcess.fork server.js]
    Q --> R[IPC通信待機]

    R --> S{ウィンドウ操作}
    S -->|閉じる| T[window-all-closed]
    T --> U{macOS?}

    U -->|Yes| V[バックグラウンド継続]
    U -->|No| W[app.quit]

    W --> X[before-quit]
    X --> Y[serverProcess.kill]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-49-01 | macOS終了挙動 | macOSではウィンドウを閉じてもアプリは終了しない | process.platform === 'darwin' |
| BR-49-02 | ウィンドウ状態永続化 | ウィンドウの位置・サイズを次回起動時に復元 | 常時 |
| BR-49-03 | 外部URL処理 | 外部URLは既定のブラウザで開く | app://またはlocalhost以外のURL |
| BR-49-04 | 開発モード判定 | パッケージ済みでなくPlaywrightでない場合は開発モード | !app.isPackaged && !isPlaywrightTest |
| BR-49-05 | ディレクトリトラバーサル防止 | 静的ファイルアクセス時にclient-build外へのアクセスを拒否 | app://staticアクセス時 |

### 環境変数

| 変数名 | 説明 | デフォルト値 |
|-------|------|------------|
| ACTUAL_DOCUMENT_DIR | ドキュメントディレクトリ | app.getPath('documents') |
| ACTUAL_DATA_DIR | データディレクトリ | app.getPath('userData') |
| lootCoreScript | バックグラウンドサーバースクリプト | 環境に応じて設定 |

## データベース操作仕様

本機能はデータベースを直接操作しない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ファイル操作エラー | ファイルダイアログでの保存失敗 | エラーログ出力 |
| - | ディレクトリ移動エラー | 予算ディレクトリの移動失敗 | エラーをスロー |
| - | 同期サーバー起動タイムアウト | 20秒以内に起動しない | エラーログ出力 |

### リトライ仕様

- ディレクトリ削除: promiseRetryで自動リトライ（minTimeout: 200ms, maxTimeout: 500ms, factor: 1.25）

## トランザクション仕様

なし（ファイルシステム操作）

## パフォーマンス要件

- ウィンドウ表示: 3秒以内
- 同期サーバー起動: 20秒以内
- ファイルダイアログ: 即座に表示

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

1. **nodeIntegration無効**: レンダラープロセスでのNode.jsアクセスを禁止
2. **contextIsolation有効**: preloadスクリプトとレンダラーコンテキストを分離
3. **ディレクトリトラバーサル対策**: client-build外へのファイルアクセスを403で拒否
4. **外部URL制限**: 内部URL以外はshell.openExternalで処理

## 備考

- デスクトップアプリはElectron上で動作し、Webアプリと同じReactコードを使用
- 開発モードではホットリロードのためlocalhost:3001を使用
- Playwrightテスト時は環境変数による設定が必須

---

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

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

### 推奨読解順序

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

Electronアプリで使用されるデータ型を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.ts | `packages/desktop-electron/index.ts` | グローバル変数とPayload型定義（67-73, 519-564行目） |

**読解のコツ**: clientWin（メインウィンドウ）、serverProcess（バックグラウンドサーバー）、syncServerProcess（同期サーバー）がグローバル変数として管理される。

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

Electronアプリのメインプロセス起動処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/desktop-electron/index.ts` | app.on('ready')ハンドラー（429-497行目） |

**主要処理フロー**:
1. **434行目**: グローバル設定読み込み（loadGlobalPrefs）
2. **436-439行目**: 同期サーバー自動起動判定
3. **441-484行目**: app://プロトコルハンドラー登録
4. **486-488行目**: createWindow呼び出し
5. **496行目**: createBackgroundProcess呼び出し

#### Step 3: ウィンドウ管理を理解する

BrowserWindowの作成と管理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.ts | `packages/desktop-electron/index.ts` | createWindow関数（331-421行目） |

**主要処理フロー**:
- **332行目**: ウィンドウ状態取得（getWindowState）
- **335-349行目**: BrowserWindow作成（webPreferences設定）
- **353-355行目**: 開発モードでDevTools表示
- **359-367行目**: URL読み込み（開発/本番で切替）
- **394-399行目**: 外部URL処理（setWindowOpenHandler）

#### Step 4: IPC通信を理解する

メインプロセスとレンダラープロセス間の通信を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.ts | `packages/desktop-electron/index.ts` | ipcMain.handle/onハンドラー群（524-685行目） |

**主要処理フロー**:
- **524-531行目**: get-bootstrap-data（起動データ取得）
- **533-538行目**: start/stop-sync-server（同期サーバー制御）
- **541-545行目**: start-oauth-server（OAuth用サーバー起動）
- **566-573行目**: open-file-dialog（ファイル選択ダイアログ）
- **582-603行目**: save-file-dialog（ファイル保存ダイアログ）

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

```
index.ts (メインプロセス)
    │
    ├─ app.on('ready')
    │      ├─ loadGlobalPrefs() → global-store.json読み込み
    │      ├─ startSyncServer() [オプション]
    │      │      └─ utilityProcess.fork(app.js)
    │      ├─ protocol.handle('app') → app://プロトコル処理
    │      ├─ createWindow()
    │      │      ├─ getWindowState() → ウィンドウ状態復元
    │      │      ├─ new BrowserWindow()
    │      │      ├─ win.loadURL()
    │      │      └─ Menu.setApplicationMenu()
    │      └─ createBackgroundProcess()
    │             └─ utilityProcess.fork(server.js)
    │
    ├─ ipcMain.on('get-bootstrap-data')
    │      └─ {version, isDev} を返却
    │
    ├─ ipcMain.handle('start-sync-server')
    │      └─ startSyncServer()
    │
    ├─ ipcMain.handle('stop-sync-server')
    │      └─ stopSyncServer()
    │
    ├─ ipcMain.handle('start-oauth-server')
    │      └─ createOAuthServer()
    │
    ├─ ipcMain.handle('open-file-dialog')
    │      └─ dialog.showOpenDialogSync()
    │
    ├─ ipcMain.handle('save-file-dialog')
    │      └─ dialog.showSaveDialog() → fs.writeFile()
    │
    ├─ ipcMain.handle('open-external-url')
    │      └─ shell.openExternal()
    │
    └─ ipcMain.handle('move-budget-directory')
           └─ fs-extra.copy() → fs-extra.remove()
```

### データフロー図

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

Electron起動 ───────────▶ app.on('ready')
                               │
                               ├─ loadGlobalPrefs() ────▶ グローバル設定
                               │
                               ├─ startSyncServer() ───▶ UtilityProcess
                               │       [autoStart時]      (syncServerProcess)
                               │
                               ├─ createWindow() ───────▶ BrowserWindow
                               │                           (clientWin)
                               │
                               └─ createBackgroundProcess() ▶ UtilityProcess
                                                              (serverProcess)

レンダラー               ipcMain
IPC呼び出し ───────────▶      │
                               ├─ get-bootstrap-data ───▶ {version, isDev}
                               │
                               ├─ open-file-dialog ─────▶ ファイルパス[]
                               │
                               ├─ save-file-dialog ─────▶ ファイル保存
                               │
                               └─ message ──────────────▶ serverProcess
                                                          postMessage()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/desktop-electron/index.ts` | ソース | Electronメインプロセス |
| preload.js | `packages/desktop-electron/preload.js` | ソース | プリロードスクリプト |
| server.js | `packages/desktop-electron/server.js` | ソース | バックグラウンドサーバーエントリー |
| menu.ts | `packages/desktop-electron/menu.ts` | ソース | アプリケーションメニュー定義 |
| window-state.ts | `packages/desktop-electron/window-state.ts` | ソース | ウィンドウ状態管理 |
| security.ts | `packages/desktop-electron/security.ts` | ソース | セキュリティ設定 |
| package.json | `packages/desktop-electron/package.json` | 設定 | Electronパッケージ設定 |
