📱 003 クラウド同期・iPhone設計
画面構成・Service Worker・データ構造・データフロー
設計書 v1.5 / 2026-05-06
1 画面構成
iPhone PWA は step state で切り替わる5画面の単一ページアプリです。
1.1 各画面の役割
banner・login・push・list・write の5画面それぞれの表示条件と役割を説明します。
追加してください
- 表示条件
- SafariでURLを直接開いた(PWAではない)
- 目的
- ホーム画面への追加手順を案内する。初回インストール時のみ使う。
- バージョン表示
app x.x.xのみ。ServiceWorkerは表示しない。- 備考
- skipWaiting導入済みのため、バージョンアップ目的で開く必要はない。
してください
- 表示条件
- アクセストークンがない(初回 or 有効期限切れ)
- 操作
- 「Googleでログイン」ボタン → Google OAuth(PKCE)→ トークン取得後 push へ遷移
- バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
有効にしてください
- 表示条件
- トークンあり +
viewer_push_doneが未設定 - 操作
- 「通知を許可する」→ OS権限ダイアログ → 許可後 list へ遷移。
viewer_push_done=trueを localStorage に保存。 - バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
- 表示条件
- トークンあり +
viewer_push_done=true(通常の起動時) - 操作
- + → write(新規)
メモタップ → write(編集)
🔔 → ロック画面に表示 ON/OFF
🗑️ → メモ削除 - バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
- 表示条件
- ① list からメモ選択・新規作成
② 通知タップ(notificationclick または pending_open) - 操作
- 「PCへ送る」→ Drive経由でPC送信 → list へ
「iPhoneに置く」→ IndexedDBに下書き保存 → list へ
「← 戻る」→ list へ(保存なし) - バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
1.2 起動時の遷移ルール
PWA起動時にどの画面から始まるかを決める4つのルールを定義します。
図 3-1 起動時の画面遷移ルール
表 1.2-1 起動時画面遷移ルール
| No | 起動時の状態 | 遷移先 |
|---|---|---|
| 1 | 非 standalone(Safariで開いた) | banner |
| 2 | URL に ?code=(OAuthコールバック) | Vercel でトークン取得 → push |
| 3 | URL に ?note=(通知タップで起動) | IndexedDB からノート読み込み → write |
| 4 | token あり・pending_open あり(30分以内) | IndexedDB からノート読み込み → write |
| 5 | token あり・viewer_push_done=true | list(通常起動) |
| 6 | token あり・push 未設定 | push(通知セットアップ) |
| 7 | token なし | login |
2 モジュール構造
フロントエンド・Service Worker・Vercel API の3層構成です。
2.1 フロントエンド(TypeScript / React)
画面を構成するコンポーネント・フックの依存関係を示します。 また、図には現れない共通ユーティリティファイルも以下の表に一覧します。
表 2.1-1 共通ユーティリティ(viewer/ 配下)
| No | ファイル名 | 役割 |
|---|---|---|
| 1 | types.ts | フロントエンド全体で共有する型定義(DraftRecord 等) |
| 2 | utils.ts | 依存を持たない汎用的なユーティリティ関数群 |
| 3 | editor-helpers.ts | テキストエリア操作やタグ管理(localStorageとの連携)を担うヘルパー |
図 3-2 フロントエンドモジュール構成
2.2 Service Worker(worker/index.js)
push受信・notificationclick等を処理するSWのイベントハンドラ5点を一覧します。
表 2.2-1 SW イベントハンドラ一覧
| No | イベント | 処理内容 |
|---|---|---|
| 1 | install | skipWaiting() 呼び出し。新バージョンを即時有効化。 |
| 2 | activate | clients.claim() でページの制御を取得。SW バージョンをログに記録。 |
| 3 | push | ① Push ペイロード(title / body_rich / id)を取得 ② fusen-meta からアクセストークンを取得③ Drive から画像をダウンロード ④ fusen-drafts にノートを保存⑤ Drive から画像ファイルを削除 ⑥ notes_to_iphone.json から当該 ID を削除⑦ pending_open を fusen-meta に記録⑧ 既存の同 ID 通知を閉じてから新規通知を表示 |
| 4 | notificationclick | 通知をタップ → locked 確認 → true なら再通知・アプリを前面に出す。⚠️ iOS では発火しない(既知の制約)。タップ後の再通知は page.tsx の pending_open フローが代替。 |
| 5 | message | アプリからの通信を受信。CLOSE_NOTIFICATION で通知を閉じる、GET_VERSION で SW のバージョンを返す等の処理。 |
2.3 サーバーサイド(Vercel API Routes)
この節は、主に開発者・保守担当向けです。iPhone PWA が Google Drive 用トークンを取得・更新するときに使う Vercel API エンドポイント2点を説明します。
表 2.3-1 Vercel API Routes 一覧
| No | ファイル | 役割 |
|---|---|---|
| 1 | app/api/auth/token/route.ts | OAuth 認証コード → アクセストークン+リフレッシュトークン交換。初回ログイン時のみ呼ばれる。 |
| 2 | app/api/auth/refresh/route.ts | リフレッシュトークン → 新しいアクセストークン取得。Drive API 呼び出し時にトークン期限切れを検出したら自動呼び出し。 |
環境変数(Vercel)
Vercel の Environment Variables に設定する変数の一覧です。.env ファイルやコードにハードコードしないこと。
表 2.3-2 Vercel 環境変数一覧
| No | 変数名 | 公開範囲 | 取得元 | 用途 |
|---|---|---|---|---|
| 1 | GOOGLE_CLIENT_SECRET_PWA | サーバー専用 | Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント シークレット | 開発者が守る値。iPhone PWA が Google Drive 用トークンを取得・更新できるように、Vercel サーバーから Google へ提示する。iPhone PWA には入れない。端末上で読める場所に置くと、第三者が俺の付箋のアプリ名義で OAuth 処理を悪用する恐れがあるため。 |
| 2 | NEXT_PUBLIC_GDRIVE_CLIENT_ID | 公開可 | Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント ID | iPhone PWA が Google OAuth フローを開始するために使う公開ID。ここでいう client はユーザー端末ではなく Google に登録した「俺の付箋アプリ」を指す。公開前提なので iPhone PWA に含めてよい。 |
| 3 | DISCORD_WEBHOOK_URL | サーバー専用 | Discord サーバー → チャンネル設定 → 連携サービス → Webhook → URL をコピー | PC 設定画面のフィードバック送信を Discord へ転送するために、Vercel サーバー側で使う。 |
3 データ構造
この節は、主に開発者・保守担当向けです。IndexedDB・localStorage・Google Drive の3か所に保存するデータと、障害時に確認する場所を定義します。
3.0 鍵の前提(先に読む)
俺の付箋の鍵の話は、まず 登場人物の整理 から始めます。3 者の関係を理解せずに鍵単体の話を読んでも、空回りします。
3.0.1 登場人物(3 者)
表 3.0-1 登場人物と守るもの
| No | 登場人物 | 役割 | 守るもの |
|---|---|---|---|
| 1 | ユーザー | 俺の付箋を使う人 | 自分の付箋本文・添付・iPhone への通知の門・Google Drive の ore-no-fusen フォルダ |
| 2 | 俺の付箋アプリ開発者 | アプリを作り、Vercel に PWA を配信、Google Cloud Console で OAuth を登録 | client_secret、Vercel 環境変数、GitHub Secrets |
| 3 | 悪意ある第三者 | 上の 2 者ではない攻撃者 | (守らない・攻撃する側) |
表 3.0-2 3 者の警戒関係
| 警戒する人 | 警戒する相手 | 警戒の理由 |
|---|---|---|
| ユーザー | 悪意ある第三者 | 付箋を盗まれない・偽通知を受け取らない |
| ユーザー | 俺の付箋アプリ開発者 | 開発者を盲目的に信頼しない。必要以上の権限を要求するアプリではないか見極める |
| 俺の付箋アプリ開発者 | 悪意ある第三者 | client_secret を奪われない・悪用されない |
| 俺の付箋アプリ開発者 | ユーザー | client_secret をユーザーに渡さない(PWA・コード・公開リポジトリに含めない)。ユーザー環境が侵害された場合に被害が広がらないよう設計する |
3.0.2 鍵を記述する 3 観点
鍵は登場人物のうち所有者が決まらないと置き場所が定まりません。すべての鍵を次の 3 観点で記述します。
表 3.0-3 鍵を記述する 3 観点
| 観点 | 意味 |
|---|---|
| 所有者 | 3 者のうち誰のための鍵か |
| 目的 | 誰から、何を、どう守るのか |
| 防衛手段 | どこに置き、どう使うか(置き場所は所有者と目的から論理的に導かれる) |
3.0.3 俺の付箋に登場する鍵の一覧
表 3.0-4 鍵の一覧(詳細は各セクション参照)
| 鍵 | 所有者 | 主目的 | 主な置き場所 |
|---|---|---|---|
VAPID 鍵(push_keys.json) | ユーザー本人(ユーザーが許可した全 PC・全 iPhone で共有) | 悪意ある第三者や別鍵PCが、ユーザーの iPhone へ偽通知を送れないようにする | ユーザーの Drive 1 個を正。PC は送信時に Drive から読み、ローカルに保存しない |
ECDH 鍵(push_devices.json 内 keys) | 個々の iPhone(ユーザーのもの) | 通知本文の暗号化(端末ごと) | ユーザーの Drive、端末ごとに 1 組 |
OAuth トークン(gdrive_token.json) | ユーザー本人(その PC のみ) | Drive へのアクセス権を一時的に証明する | PC のローカル(%LOCALAPPDATA%) |
client_secret | 俺の付箋アプリ開発者 | 「俺の付箋」を名乗る他アプリが Google OAuth を通れないようにする | Vercel のサーバー(コードに含めない) |
3.0.4 VAPID 鍵の補足
VAPID 鍵については特に誤解されやすい点があるので、ここで補足します。
3.1 IndexedDB
PWA端末内に保存されるfusen-drafts・fusen-meta・fusen-logsの3ストアを定義します。
3.1.1 🗄 fusen-drafts(ノートデータ)
表 3-4 fusen-drafts スキーマ
| No | フィールド | 型 | 用途・内容 |
|---|---|---|---|
| 1 | id | string | 主キー(UUID) |
| 2 | title | string | ノートタイトル |
| 3 | body | string | 本文(Markdown) |
| 4 | images | Object[] | 添付画像({ fileName: string, blob: Blob }[]) |
| 5 | videos | Object[] | 添付動画({ fileName: string, blob: Blob }[])。ユーザー本文とは別に保持する |
| 6 | tags | string[] | 付与されたタグの配列 |
| 7 | locked | boolean | ロック画面に表示が ON なら true |
| 8 | created_at | string | 作成日時(JST ISO 8601) |
| 9 | sent_at | string | 送信日時(未送信時は undefined) |
| 10 | received_pc | boolean | PC 側が受信済みかどうか |
3.1.2 🗄 fusen-meta(メタ情報)
表 3-5 fusen-meta スキーマ
| No | キー | 用途・内容 |
|---|---|---|
| 1 | access_token | Google Drive アクセストークン(SW が参照) |
| 2 | pending_open | 次回起動時に開くノートの情報。{ id: string, t: number } |
3.1.3 🗄 fusen-logs(デバッグログ)
表 3-6 fusen-logs スキーマ
| No | フィールド | 用途・内容 |
|---|---|---|
| 1 | t | タイムスタンプ(JST) |
| 2 | msg | ログメッセージ |
3.2 localStorage
セッション管理に使うlocalStorageのキーと値の一覧です。
3.2.1 🔑 認証・設定フラグ
表 3-7 localStorage キー一覧
| No | キー | 用途・内容 |
|---|---|---|
| 1 | viewer_access_token | Google API アクセストークン |
| 2 | viewer_refresh_token | リフレッシュトークン(Vercel API 経由で更新) |
| 3 | viewer_expires_at | アクセストークンの有効期限(ms) |
| 4 | viewer_push_done | "true" なら通知設定済み → list へ直行 |
| 5 | pkce_verifier | OAuth PKCE の code_verifier(認証中のみ存在) |
| 6 | pending_note | PKCE 認証後に自動で開くノート ID |
| 7 | viewer_device_id | Web Push 用のクライアント識別子 |
| 8 | fusen_known_tags | 過去に入力したタグの履歴(サジェスト用) |
3.3 Google Drive ファイル
PCとiPhone間の中継、および Web Push 設定に使う Drive ファイルの種類と書き込み/削除責務を説明します。
表 3.3-1 Google Drive ファイル一覧
| No | ファイル名 | 書き込み | 読み取り・削除 | 用途 |
|---|---|---|---|---|
| 1 | notes_to_iphone.json | PC(gdrive.rs) | iPhone SW (push受信時) | PC から iPhone へメモ本文と添付画像名を渡すために、未処理ノートを一時保存する。SW が受信して IndexedDB に保存後、当該 ID を除いて書き戻す(または全削除)する。 |
| 2 | notes_from_iphone.json | iPhone (useBackgroundSend) | PC(gdrive.rs 30秒ポーリング) | iPhone から PC へメモ本文、添付画像名、添付動画名を渡すために、未処理ノートを一時保存する。PC 受信後は処理済みアイテムを除いた残りのみ書き戻す。 |
| 3 | fusen_img_*.jpg | iPhone または PC | 受信側が処理後に削除 | Push ペイロードや JSON に大きな画像バイナリを直接入れないために、添付画像だけを一時ファイルとして保存する。受信側が IndexedDB または PC の assets/ に保存後、削除する。 |
| 4 | fusen_video_*.mp4fusen_video_*.mov | iPhone (useBackgroundSend) | PC(gdrive.rs 30秒ポーリング) | 動画バイナリを JSON や付箋本文に埋め込まないために、添付動画を一時ファイルとして保存する。PC が assets/video/ に保存し、付箋本文へ保存先パスを追記した後に削除する。 |
| 5 | push_keys.json | PC(webpush.rs) 初回のみ作成 | iPhone(lib/push.ts)が 公開鍵を読む PC(webpush.rs)が 秘密鍵を読む | VAPID 鍵ペア。iPhone は公開鍵で Push 購読、PC は秘密鍵で送信時に署名する。鍵の所有者・目的・防衛手段は 3.0 鍵の前提、詳細は 表 3.3-4 を参照。 |
| 6 | push_devices.json | iPhone(lib/push.ts) が upsert | PC(webpush.rs)が 全端末へ Push 送信 | PC が登録済み iPhone へ Push を送るために、端末ごとの device_id / endpoint / 暗号化鍵を保存する。複数端末へ送るために、端末一覧として保持する。 |
| 7 | pc_devices.json | PC(Google連携完了時 または手動登録時) | iPhone PWA (送信直前に読む) | iPhone から PC へ送るときの送信先 PC 名簿。PCごとの pcId / 表示名 / 更新時刻を保存する。PC起動時や受信ポーリング時に勝手に書き込まない。 |
3.3.1 複数 iPhone・複数 PC の接続モデル
同じ Google Drive の ore-no-fusen フォルダを共有領域として使い、複数 iPhone / iPad と複数 PC を接続できる。 現行仕様では、1台のPCアプリが同時に使える Google Drive は1つだけである。 同じPCから「iPhone A は Drive 1」「iPhone B は Drive 2」のように複数Driveを同時に使い分ける運用は非対応とする。 Driveを切り替える場合は、PC側でGoogle Driveを再接続し、そのDriveに登録されているiPhone / iPadが送信対象になる。
図 3.3.1-1 Push鍵は「勝手に通知されない」ために1個だけ共有する
図 3.3.1-2 PC → iPhone は登録済み端末への同報送信
図 3.3.1-3 iPhone → PC は targetPcId で送信先PCを指定
3.3.2 Drive JSON データ構成
表 3.3-1 に記載した各 JSON ファイルの構造は、この節に示す。 Drive 上の JSON は、以下の構成を基本とする。 実装上の参照元は、PC 側が src-tauri/src/lib.rs / src-tauri/src/webpush.rs / src-tauri/src/gdrive.rs、iPhone 側が app/viewer/lib/push.ts / app/viewer/hooks/useBackgroundSend.ts / worker/index.js。
表 3.3-2 notes_to_iphone.json(PC → iPhone 未処理キュー)
| No | フィールド | 型 | 必須 | 用途・内容 |
|---|---|---|---|---|
| 1 | items | Object[] | ○ | 未処理ノートの配列。最大20件を保持 |
| 2 | items[].id | string | ○ | ノートID(UUID) |
| 3 | items[].title | string | ○ | 通知・表示タイトル |
| 4 | items[].body | string | ○ | Markdown本文。画像は fusen_img_* 参照 |
| 5 | items[].tags | string[] | ○ | タグ一覧 |
| 6 | items[].sent_at | string | ○ | PC送信時刻 |
| 7 | items[].received_at | null | △ | 現行ソースが互換目的で出力している残項目。処理判定には使わない |
{
"items": [
{
"id": "uuid",
"title": "買い物",
"body": "牛乳\n",
"tags": ["shopping"],
"sent_at": "2026-05-05T12:00:00Z",
"received_at": null
}
]
}表 3.3-3 notes_from_iphone.json(iPhone → PC 未処理キュー)
| No | フィールド | 型 | 必須 | 用途・内容 |
|---|---|---|---|---|
| 1 | items | Object[] | ○ | 未処理ノートの配列 |
| 2 | items[].id | string | ○ | ノートID(UUID) |
| 3 | items[].title | string | ○ | ノートタイトル |
| 4 | items[].body | string | ○ | Markdown本文。画像は fusen_img_* 参照。ユーザーが入力した本文であり、添付動画のファイル名で上書きしない |
| 5 | items[].sent_at | string | ○ | iPhone送信時刻 |
| 6 | items[].tags | string[] | ○ | タグ一覧 |
| 7 | items[].images | Object[] | △ | 添付画像一覧。各要素は Drive 一時ファイル名を持つ |
| 8 | items[].videos | Object[] | △ | 添付動画一覧。各要素は { videoFileName, originalFileName } を持つ。複数動画可 |
| 9 | items[].videoFileName / items[].originalFileName | string | △ | 旧実装互換用の先頭動画情報。新規実装では videos[] を正とする |
| 10 | items[].targetPcId | string | △ | 複数PC接続時の送信先PC ID。未指定の旧データは従来互換として全PCが受信対象にできる |
{
"items": [
{
"id": "uuid",
"title": "外出先メモ",
"body": "帰ったら確認\n",
"sent_at": "2026-05-05T12:00:00+09:00",
"targetPcId": "pc-uuid",
"tags": [],
"videos": [
{
"videoFileName": "fusen_video_20260525_073000_0.mp4",
"originalFileName": "dance.mp4"
}
]
}
]
}表 3.3-4 push_keys.json(VAPID 鍵)の鍵プロファイル
| 観点 | 値 |
|---|---|
| 所有者 | ユーザー本人(ユーザーが許可した全 PC・全 iPhone で共有する「許可された端末群の共有秘密」) |
| 目的 | 悪意ある第三者や別鍵PCが、ユーザーの iPhone へ偽通知を送れないようにする |
| 防衛手段 | Drive 上の 1 個を正とする。PC は送信時に Drive から秘密鍵を読み、メモリ上で署名に使う。iPhone は同じ鍵の公開鍵で購読する。PC ローカルには保存しない |
| 漏えい時 | 悪意ある第三者が「正規の通知」に見える Push を送れる可能性。付箋本文・添付メディアは別途 Drive 権限が必要なので読めないが、通知を送れること自体が被害。対応:Drive 上の push_keys.json を作り直し、iPhone 側で再購読する |
| 欠落時 | PC は VAPID 署名を作れず Push 送信不可。iPhone 受信は list 画面でのフォールバック取得頼みになる |
表 3.3-4-2 push_keys.json フィールド
| No | フィールド | 型 | 必須 | 用途 |
|---|---|---|---|---|
| 1 | public_key_b64url | string | ○ | iPhone が pushManager.subscribe() の applicationServerKey に渡す |
| 2 | private_key_b64url | string | ○ | PC が VAPID JWT に署名し Authorization: vapid t=...,k=... として送る |
| 3 | subject | string | ○ | VAPID JWT の sub。現行値は mailto:ore-no-fusen@example.com |
{
"public_key_b64url": "BASE64URL_PUBLIC_KEY",
"private_key_b64url": "BASE64URL_PRIVATE_KEY",
"subject": "mailto:ore-no-fusen@example.com"
}表 3.3-5 push_devices.json 内 ECDH 鍵(`keys.p256dh` / `keys.auth`)の鍵プロファイル
| 観点 | 値 |
|---|---|
| 所有者 | 個々の iPhone(端末ごとに 1 組) |
| 目的 | Push 通知本文の暗号化。iPhone 自身だけが本文を復号できるようにする |
| 防衛手段 | iPhone が購読時に生成し、Drive の push_devices.json に upsert。PC は送信時にその鍵で本文を暗号化する |
| 漏えい時 | その iPhone への暗号化済み通知の本文を悪意ある第三者が復号できる可能性。VAPID 鍵(送信権)とは別軸。対応:iPhone PWA で再購読し、新しい鍵で push_devices.json を更新 |
| 欠落時 | PC は暗号化済み通知を作れず、その iPhone への送信が失敗する |
表 3.3-5-2 push_devices.json フィールド
| No | フィールド | 型 | 必須 | 用途・内容 |
|---|---|---|---|---|
| 1 | devices | Object[] | ○ | 登録済み端末の配列 |
| 2 | devices[].device_id | string | ○ | 端末ID。iPhone PWA が localStorage に保持 |
| 3 | devices[].endpoint | string | ○ | APNs / Push Service の送信先URL |
| 4 | devices[].keys.p256dh | string | ○ | Push 暗号化用の公開鍵(ECDH) |
| 5 | devices[].keys.auth | string | ○ | Push 暗号化用の認証シークレット |
| 6 | devices[].registered_at | string | ○ | 登録時刻 |
| 7 | devices[].device_name | string | △ | 表示用端末名 |
| 8 | devices[].google_account_email | string | △ | Drive接続アカウントのメール |
| 9 | devices[].google_account_name | string | △ | Drive接続アカウント名 |
| 10 | devices[].google_account_photo | string | △ | Drive接続アカウントの画像URL |
{
"devices": [
{
"device_id": "uuid",
"endpoint": "https://web.push.apple.com/...",
"keys": {
"p256dh": "BASE64URL_P256DH",
"auth": "BASE64URL_AUTH"
},
"registered_at": "2026-05-05T12:00:00+09:00",
"device_name": "iPhone",
"google_account_email": "user@example.com"
}
]
}表 3.3-6 pc_devices.json(iPhone → PC 送信先一覧)
| No | フィールド | 型 | 必須 | 用途・内容 |
|---|---|---|---|---|
| 1 | pcs | Object[] | ○ | 登録済みPCの配列 |
| 2 | pcs[].pcId | string | ○ | PCを一意に識別するID。PC側ローカルにも保持し、受信時の自分宛判定に使う |
| 3 | pcs[].pcName | string | ○ | PWAの送信先プルダウンに表示するPC名 |
| 4 | pcs[].registeredAt | string | △ | 初回登録時刻 |
| 5 | pcs[].updatedAt | string | △ | 最終更新時刻 |
| 6 | pcs[].googleAccountEmail | string | △ | どのGoogleアカウントで登録されたPCかを確認するためのメールアドレス |
PWA の通常画面では、ユーザーに pcId を選ばせない。送信先は「家のPC」「会社のPC」のような pcName で選ばせる。同じ pcName の登録が複数ある場合は、PWA側で updatedAt が最も新しい登録を採用し、古い同名登録は通常候補に出さない。pcId は受信判定とトラブル診断用の内部IDであり、通常操作の判断材料にしない。
{
"pcs": [
{
"pcId": "pc-uuid",
"pcName": "DESKTOP-01",
"registeredAt": "2026-05-29T12:00:00+09:00",
"updatedAt": "2026-05-29T12:00:00+09:00",
"googleAccountEmail": "user@example.com"
}
]
}4 データフロー
PC→iPhone の初回セットアップ、初回以降の通常送信、iPhone→PC送信、通知ON/OFFのシーケンス図を示します。
4.1 PC → iPhone 初回セットアップと通常送信
初回は、ユーザーがPCで「iPhoneに送る」を押したことをきっかけに設定画面へ誘導し、PC側でGoogle Drive接続と push_keys.json の準備を行ってから、iPhone PWAをセットアップします。 「iPhoneに送る」は右クリックメニューに常に表示し、未設定のときは送信せず設定画面の iPhone 連携タブへ直行します。 初回以降は、PCの「iPhoneに送る」操作だけで通知が届きます。
図 3-3 PC → iPhone 初回セットアップと通常送信シーケンス
4.2 PC → iPhone 受信の補足
図 3-3 の初回セットアップで push_keys.json と push_devices.json が準備済みであれば、以降はPC側の「iPhoneに送る」操作だけで送信できます。
push_keys.json:Drive 上の 1 個を正とする Web Push 用共有VAPID鍵。iPhoneは公開鍵を使ってPush購読し、PCは送信時にDriveから秘密鍵を読み、メモリ上でWeb Push署名に使う。PCローカルには保存しない。push_devices.json:iPhone側が作成・更新する通知先デバイス一覧。PCは送信直前に Drive から再取得し、この一覧を見て送信先を決める。notes_to_iphone.json:PCからiPhoneへ渡す未処理キュー。Service Workerが受信後に処理済みファイルを削除する。
4.3 iPhone → PC 送信(ユーザー体験 + 内部処理)
iPhoneのwrite画面で「PCに送る」を押した瞬間から、PCに新しい付箋が開くまでの全体フロー。
図 3-4 iPhone → PC 送信シーケンス
4.4 ロック画面に表示 ON/OFF と再通知サイクル(REQ_IP_05)
「消す意思がないかぎりロック画面から消えない」体験を実現する ON/OFF 操作と、タップ後の再通知サイクル。 シーケンス図では、①②③ はユーザーが実施する操作、❶❷❸ は PWA・Service Worker・IndexedDB が自動実行する処理を表します。
図 3-5 ロック画面常駐サイクル(ON/OFF と再通知の実装フロー)
5 UI インタラクション
5.1 画面モード定義
リスト・ライトの 2 モードとその遷移条件を定義します。
表 5.1-1 画面モード定義
| No | モード | 状態 | 遷移トリガー |
|---|---|---|---|
| 1 | リストモード | Drive から同期した付箋を一覧表示している状態 | アプリ起動後の初期画面 |
| 2 | ライトモード | 1 枚の付箋を全画面で編集している状態(800ms 自動保存) | 付箋タップ or +ボタン |
図 3-6 画面モード遷移図
5.2 各モードの操作一覧
表 3-10 リストモードの操作
| No | 操作 | 対象 | 結果 |
|---|---|---|---|
| 1 | タップ | 付箋アイテム | ライトモードへ遷移 |
| 2 | 🔔 タップ | ロック画面に表示ボタン | locked フラグの ON/OFF 切り替え |
| 3 | 🗑️ タップ | 削除ボタン | 確認ダイアログ → Drive から削除 |
| 4 | + タップ | 新規ボタン | 空のライトモードへ遷移 |
表 3-11 ライトモードの操作
| No | 操作 | 対象 | 結果 |
|---|---|---|---|
| 1 | 文字入力 | エディタ | 800ms debounce で自動保存 |
| 2 | 画像貼り付け | エディタ | IndexedDB に保存・プレビュー表示 |
| 3 | ← タップ | 戻るボタン | リストモードへ遷移(保存確定) |
5.3 インタラクション・マトリックス
表 5.3-1 インタラクション・マトリックス
| No | 操作 | リストモード | ライトモード |
|---|---|---|---|
| 1 | タップ | ライトモードへ | - |
| 2 | 🔔 | locked ON/OFF | - |
| 3 | 🗑️ | 削除確認ダイアログ | - |
| 4 | 文字入力 | - | 自動保存 |
| 5 | ← 戻る | - | リストモードへ |
6 機能一覧
画面ごとの機能と、各機能の設計意図を記します。
6.1 メモ一覧画面(list)
表 6.1-1 メモ一覧画面の機能
| No | 機能 | 設計意図・工夫 |
|---|---|---|
| 1 | Drive → IndexedDB 同期 | 一覧を開くたびに notes_to_iphone.json を Drive から取得し、ローカルにない新着ノートを IndexedDB に取り込む。取り込み後は Drive ファイルを削除(Drive = 未処理キュー)。Drive 失敗時は IndexedDB だけで一覧表示を続ける(フォールセーフ) |
| 2 | 画像サムネイル | 添付画像がある場合、IndexedDB の Blob から URL.createObjectURL() で URL を生成してサムネイルを表示。アンマウント時に URL.revokeObjectURL() で解放する |
| 3 | ステータスバッジ | draft(下書き)/ sent(PC送信済み)/ PC受信 の3状態を sent_at フィールドの有無と received_pc フラグで判定して色分け表示する |
| 4 | 相対時間表示 | created_at から「3分前」「1時間前」「昨日」の形式に変換(formatRelativeTime())。 数字と絶対時刻を並べるより一目で新鮮度がわかる |
| 5 | 🔔/🔕 ロック画面常駐 | 後述(6.2) |
| 6 | 🗑️ 削除 | IndexedDB から削除後、Drive 上の同 ID ファイルも削除する |
| 7 | + 新規作成 | 新しい下書き ID を crypto.randomUUID() で生成し write 画面へ遷移 |
| 8 | 🔔 デバイス再登録(フッター) | silentReRegisterIfNeeded() を呼び出し、push_devices.json に自デバイスが存在しない場合のみ静かに再登録する。既存デバイスがいれば何もしない |
6.2 ロック画面常駐(🔔)
このアプリの核心機能。「消す意思がないかぎり、ロック画面から消えない」体験を実現する。
表 6.2-1 ロック画面常駐の仕組みと設計意図
| No | 観点 | 設計意図・工夫 |
|---|---|---|
| 1 | 通知表示の方式 | iOS ではメインスレッドの new Notification() が動かない。navigator.serviceWorker.ready → reg.showNotification() を使う。これが iOS で通知を出せる唯一の方法 |
| 2 | 重複防止 | reg.getNotifications() で既存通知を取得し、同じ data.id を持つものをすべて n.close() してから新規通知を表示する |
| 3 | タイトル・本文の生成 | # タイトル 行があれば it を通知タイトルに。なければ本文冒頭20文字をタイトルに使う。本文から画像タグ  を正規表現で除去してから40〜60文字を表示する |
| 4 | 楽観的更新 | ボタンを押した瞬間に UI の 🔔/🔕 を切り替える。SW 操作や IndexedDB 書き込みに失敗した場合のみロールバックする。ユーザーに「レスポンスが遅い」と感じさせない |
| 5 | 通知権限の動的確認 | Notification.permission === 'default' なら許可ダイアログを表示。denied なら UI を元に戻してエラーを表示する |
| 6 | 通知を消す権限 | ロック解除時に reg.active?.postMessage({ type: 'CLOSE_NOTIFICATION', tag }) で SW に通知クローズを依頼する。メインスレッドは通知を直接閉じられない |
| 7 | locked フラグの永続化 | locked: true/false を IndexedDB に書き込む。次回 Push 受信時に SW がこの値を参照して再通知を行うかどうかを決める |
| 8 | 効果音 | ON 時は bell_on.wav、OFF 時は bell_off.wav を Audio API で再生。操作の結果をユーザーが音で確認できる |
6.3 メモ編集画面(write)
表 6.3-1 メモ編集画面の機能
| No | 機能 | 設計意図・工夫 |
|---|---|---|
| 1 | contenteditable エディタ | contentEditable="true" の div で実装。React の value/onChange モデルを使わず DOM を直接操作。serializeEditor() が HTML → Markdown 形式に変換する |
| 2 | 3秒自動保存 | 入力のたびにタイマーをリセット。3秒間入力がなければ IndexedDB に保存(useAutoSave)。Drive は使わない。ネットワーク不要・オフライン動作 |
| 3 | バックグラウンド遷移時の強制保存 | visibilitychange で非表示になった瞬間にも保存(useVisibilitySave)。アプリを閉じてもデータが消えない |
| 4 | 📷 画像添付 | ファイル選択 → CropModal でトリミング → IndexedDB に Blob 保存 → buildImageFileName() でファイル名生成 → カーソル位置に ![]() を挿入。PCへ送るときに Drive に実際にアップロードされる |
| 5 | 🔷 Mermaid | MermaidModal でフローチャートのコードを書いて確定するとエディタに挿入される |
| 6 | ☑ チェックボックス | カーソルのある行を data-checkbox-line 属性を持つ <span> に変換(再押しで解除)。行頭が IMG ノードの場合は次の兄弟ノードに処理を移す特殊ケース対応あり |
| 7 | 🏷️ タグ | タグバーを展開。Enter または Space で確定。過去に入力したタグを localStorage(fusen_known_tags) から自動サジェスト。送信時に mergeKnownTags() で履歴に追加 |
| 8 | ← 一覧に戻る | 内容がある場合は IndexedDB に下書き保存してから list 画面へ遷移。保存はサイレントに行われ、確認ダイアログは不要 |
| 9 | 「iPhoneに置いておく」 | Drive を一切使わず IndexedDB のみに保存。ネットワーク不要。削除するまで端末に残り続ける |
| 10 | 「PCへ送る」 | 後述(6.4) |
| 11 | 🎬 VideoDrop | mp4 / mov を選択して現在の付箋に添付し、「PCへ送る」を押したときに Drive 経由で PC へ送る。画像と同じく付箋の添付部品であり、選択時に本文を上書きしない。PC側で assets/video/ に保存された絶対パスを付箋本文の末尾へ追記する |
6.4 「PCへ送る」
表 6.4-1 PCへ送る の処理ステップ
| No | ステップ | 設計意図・工夫 |
|---|---|---|
| 1 | ① トークン有効期限確認 | viewer_expires_at と Date.now() を比較。期限5分前を切っていたら送信前に Vercel /api/auth/refresh を呼んでトークンを更新する。送信中に突然期限切れにならないための先読み更新 |
| 2 | ② セッション切れ処理 | リフレッシュが失敗した場合は localStorage のトークンを削除し、login 画面へ遷移。エラーメッセージを5秒表示して消す |
| 3 | ③ 添付アップロード | 添付画像・動画を Promise.all() で並列アップロード。直列より速い |
| 4 | ④ キューへの追記 | notes_from_iphone.json を Drive から読み取り、既存アイテムの末尾に新しいアイテムを追加して上書き(read-modify-write)。旧スキーマのファイルが残っていても自動変換して引き継ぐ後方互換処理あり |
| 5 | ⑤ IndexedDB に sent を記録 | 送信後、同 ID のレコードに sent_at をセットして IndexedDB を更新。一覧画面に「送信済み」バッジが表示される |
| 6 | ⑥ 成功フィードバック | 3秒間 backgroundSendSuccess = true にして UI に成功インジケータを表示。その後自動で消える |
| 7 | ⑦ VideoDrop | 🎬で選ばれた動画は選択時点では送信しない。「PCへ送る」時に fusen_video_*.mp4/mov として Drive へアップロードし、キューには videos[] を入れる。PC側は受信時に assets/video/ へ保存し、本文にはクリック可能な絶対パスを末尾へ追記し、ack後にDrive上の一時動画を削除する |
6.5 通知許可・デバイス登録(push 画面)
表 6.5-1 プッシュ通知登録の処理ステップ
| No | ステップ | 設計意図・工夫 |
|---|---|---|
| 1 | ① 通知権限取得 | Notification.requestPermission() で OS レベルの許可ダイアログを表示。拒否された場合はエラーを表示して処理を停止 |
| 2 | ② 購読の再生成 | 既存の Push 購読があれば一度 unsubscribe() してから再登録する。クリーンな状態を保つためのリセット処理 |
| 3 | ③ VAPID 鍵での購読 | Drive の push_keys.json から public_key_b64url を取得し、その公開鍵を使って pushManager.subscribe() を実行。この公開鍵は PC 側の WebPush 送信時に使う private_key_b64url と対になる。push_keys.json が未作成の場合は、PC 側で Drive 接続後に iPhone 送信準備を行うよう案内する |
| 4 | ④ デバイス ID の永続化 | crypto.randomUUID() で端末固有の device_id を生成。localStorage に保存し以降の再登録でも同一 ID を使う |
| 5 | ⑤ push_devices.json への upsert | Drive から push_devices.json を取得し、同 device_id のエントリを更新(または新規追加)して上書き保存。旧スキーマ(endpoint 直下方式)は自動的に新スキーマに移行する |
7 エラーハンドリング・リカバリ方針
7.1 通信・認証エラー
7.1.1 Drive API エラーとトークン自動更新
Drive API 呼び出し(ダウンロードやアップロード)が失敗した場合、drive.ts 内の downloadWithAutoRefresh 等によりトークンの自動リフレッシュが試行される(実施済み)。 リフレッシュにも失敗した場合は例外がスローされ、呼び出し元の React コンポーネント側でキャッチされる。 UI上のエラーフィードバック(トースト表示等)は ⚠️ 未実施。
7.1.2 認証切れ時のフォールバック
トークンリフレッシュ(Vercel API /api/auth/refresh)が 4xx 等で失敗し、リフレッシュトークン自体が失効していると判定された場合、localStorage からトークン情報を破棄し null を返す。 これによりアプリは未認証状態とみなされ、自動的にログイン画面(login ステップ)へフォールバックする(実施済み)。
7.1.3 PCアプリ側での Drive API 失敗(gdrive.rs)
PC アプリ(Rust)が Drive API 呼び出しに失敗した場合、Err(String) を返して Tauri コマンド経由でPCフロントエンドにエラーを通知する。 PC側のアクセストークンは期限到来の 60 秒前に自動リフレッシュされ、失敗時は「Googleの認証が切れました」と返す。 PCフロントエンドでのエラーダイアログ表示(トースト等)は ⚠️ 未実施。
7.1.4 PCからの Web Push 送信失敗
PC アプリから APNs / FCM へのプッシュ送信が失敗した場合(201 以外の HTTP ステータス)、エラーコードを含む Err を返す。 PC は送信直前に push_devices.json を Drive から再取得し、古いメモリキャッシュによる VAPID 鍵不一致を事前に避ける(実施済み)。 **PC側での送信失敗時の自動リトライ機構は ⚠️ 未実施。**送信失敗時は iPhone に通知が届かないまま終了する。 エラー表示では APNs / Push Service のステータスごとに原因カテゴリを分け、ユーザーが取れる操作(Drive再接続、PWA再インストール、通信確認など)を示す。
7.2 バックグラウンド処理・リカバリ
7.2.1 Push 受信時のフォールバック(画像DL失敗時)
Service Worker (worker/index.js) 内での Push 受信処理では、画像(fusen_img_*)の Drive ダウンロードがネットワークエラー等で失敗した場合でも処理を中断しないフェイルセーフ機構がある。 画像取得に失敗した場合でも、テキスト本文のみを IndexedDB(fusen-drafts)に保存し、ユーザーへの OS 通知を確実に表示する(実施済み)。
7.2.2 デバッグログの運用方針(fusen-logs)
UIを持たない Service Worker 内で発生した処理結果やエラー(トークン取得失敗、画像保存失敗など)は、IndexedDB の fusen-logs ストアに対して fire-and-forget で記録される(実施済み)。 後から Chrome DevTools 等で内部状態や Push 受信時のエラー原因を追跡できるようになっている。
7.2.3 iOS特有の制約とリカバリサイクル
iOS の PWA 環境では、バックグラウンドでの通知タップ時(notificationclick イベント)が正常に発火しない・あるいは Web API へのアクセスが制限されるケースがある。 この制限に対するリカバリとして、通知受信時に次回開くべきノート ID を IndexedDB に保存(pending_open)し、次にユーザーがアプリを開いた際(page.tsx マウント時)に自動的にそのノートを表示するサイクルを構築している(実施済み)。
7.2.4 Service Worker の更新
skipWaiting() + clients.claim() で新しい SW が即時有効化される。バグ修正版をリリースした際に古い SW が動き続けることはない。
8 改版履歴
表 8-1 改版履歴
| No | バージョン | 日付 | 変更内容 |
|---|---|---|---|
| 1 | 1.0 | 26-04-19 | 新規作成。005_VIEWER_SCREENS.html / 007_VIEWER_CODE_STRUCTURE.html / 004_PWA_DATA_FLOW.html の内容を統合・整理 |
| 2 | 1.1 | 26-04-20 | 4.4 にロック画面常駐体験(REQ_IP_05)の再通知サイクル(①②③④)を追加。4.2 に REQ_IP_05 への参照を追加 |
| 3 | 1.2 | 26-04-20 | 4.4 の再通知フローを実態に合わせて修正。iOS では notificationclick が発火しないため pending_open + page.tsx が再通知を担う仕組みを図入りで明記。2.2 の notificationclick 説明に iOS 制約を追記 |
| 4 | 1.3 | 26-04-24 | モジュール構造図を graph LR(横向き)に変更。スクロールなしで全体が見えるよう改善。 |
| 5 | 1.4 | 26-04-27 | セクション6「機能一覧」を新規追加。メモ一覧・ロック画面常駐・メモ編集・PCへ送る・プッシュ登録の設計意図を記載。旧6→7、旧7→8に繰り下げ。 |
| 6 | 1.5 | 26-05-06 | 2.3 Vercel / OAuth、3 データ構造、6.1〜6.5 機能一覧を修正。説明対象を開発者・保守担当向けとして明記し、client_secret は開発者が守る値であること、Vercel がトークンを保存しないことを追記。表の「意味」を「用途・内容」に変更し、6.1-1 以降の表へ No を追加。 |
| 7 | 1.6 | 26-05-24 | 6.3 / 6.4 に VideoDrop を追加。PWAから mp4 / mov をDrive経由でPCへ送り、PC側で assets/video/ に保存して付箋本文へパスを記録する仕様を追記。 |
| 8 | 1.7 | 26-05-24 | VideoDrop を動画選択即送信から付箋添付後に「PCへ送る」で送信する方式へ変更。PC側本文にはクリック可能な絶対パスを記録する仕様へ更新。 |
| 9 | 1.8 | 26-05-25 | VideoDrop を複数動画対応の添付メディア仕様へ更新。videos[]、IndexedDB の videos、Drive 一時ファイル fusen_video_*、本文保護ルールを追加。 |
| 10 | 1.9 | 26-05-30 | 複数iPhone・複数PCの接続モデルを追加。PC→iPhoneは登録済み通知端末への同報送信、iPhone→PCはpc_devices.jsonとtargetPcIdで送信先PCを選択する仕様を明記。 |
| 11 | 1.10 | 26-05-30 | Web Push共有鍵の目的・保護対象・誰にとっての秘密かを追記。push_keys.json はユーザー本人のDrive上の1個を正とし、PCローカル鍵で上書きしないルールを明記。 |
| 12 | 1.11 | 26-05-30 | PC→iPhone / iPhone→PC 送信時のキュー保護を明記。Driveキュー取得失敗時は空配列で上書きせず送信を中止し、PC→iPhoneはDrive保存成功後にPushを送る。Push失敗はHTTPステータス別に分類して表示する。 |
| 13 | 1.12 | 26-05-30 | PC右クリックメニューの「iPhoneに送る」を常時表示し、未設定時は設定画面の iPhone 連携タブへ誘導する仕様を明記。 |
| 14 | 1.13 | 26-05-31 | 3.0「鍵の前提」を新設。鍵を 所有者・目的・防衛手段 の 3 観点で記述するルールと、登場する鍵の一覧表を追加。VAPID 鍵セクションの 6 つの Note を 1 つの鍵プロファイル表と同期ルールに統合し、重複を削減。push_devices.json 内 ECDH 鍵にも同じ枠組みを適用。 |
| 15 | 1.14 | 26-05-31 | 3.0 を「3 者の登場人物と関係 → 鍵の枠組み → 鍵一覧 → VAPID 補足」の順に再編成。ユーザー / 俺の付箋アプリ開発者 / 悪意ある第三者 の 3 者と互いの警戒関係を表で明示。「作者」「攻撃者」「第三者」表記を統一し、用語の揺れを解消。 |
| 16 | 1.15 | 26-05-31 | PWAの送信先PC選択は pcName を通常表示とし、同名PCが複数ある場合は updatedAt が最新の登録へ自動的に寄せる仕様を追記。pcId は受信判定・診断用の内部IDであり、通常操作でユーザーに選ばせないことを明記。 |
| 17 | 1.16 | 26-05-31 | 設定画面の接続状態で Drive 未処理キューの中身確認と、ユーザー確認付きのキューJSON削除を行える仕様を追記。 |
| 18 | 1.17 | 26-06-05 | PC→iPhone送信直前に push_devices.json を Drive から再取得する仕様を明記。予見可能なPush不整合はアプリ側で回避し、エラー時はユーザーが取れる復旧手順を表示する方針を追記。 |