Skip to content

📱 003 クラウド同期・iPhone設計

画面構成・Service Worker・データ構造・データフロー

設計書 v1.5 / 2026-05-06


1 画面構成

iPhone PWA は step state で切り替わる5画面の単一ページアプリです。

1.1 各画面の役割

banner・login・push・list・write の5画面それぞれの表示条件と役割を説明します。

インストール案内画面 — Safariで /viewer を開いたとき(PWAではない)
ホーム画面に
追加してください
STEP 1 — 共有をタップ
STEP 2 — ホーム画面に追加
STEP 3 — 追加をタップ
app 2.9.x
banner
step: banner
表示条件
SafariでURLを直接開いた(PWAではない)
目的
ホーム画面への追加手順を案内する。初回インストール時のみ使う。
バージョン表示
app x.x.x のみ。ServiceWorkerは表示しない。
備考
skipWaiting導入済みのため、バージョンアップ目的で開く必要はない。
PWA画面 — ホーム画面アイコンから起動(URLバーなし・アプリとして動作)
俺の付箋
Googleでログイン
してください
Googleでログイン
app 2.9.x / ServiceWorker 2.9.x
ログイン
step: login
表示条件
アクセストークンがない(初回 or 有効期限切れ)
操作
「Googleでログイン」ボタン → Google OAuth(PKCE)→ トークン取得後 push へ遷移
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
セットアップ
プッシュ通知を
有効にしてください
通知を許可する
app 2.9.x / ServiceWorker 2.9.x
通知設定
step: push
表示条件
トークンあり + viewer_push_done が未設定
操作
「通知を許可する」→ OS権限ダイアログ → 許可後 list へ遷移。viewer_push_done=true を localStorage に保存。
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
メモ
大事なメモ🔔
買い物リスト🔕
アイデアメモ🔕
app 2.9.x / ServiceWorker 2.9.x
メモ一覧
step: list
表示条件
トークンあり + viewer_push_done=true(通常の起動時)
操作
+ → write(新規)
メモタップ → write(編集)
🔔 → ロック画面に表示 ON/OFF
🗑️ → メモ削除
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
← 戻るPCへ送る
大事なメモ...
iPhoneに置く
📷
B
H1
app 2.9.x / ServiceWorker 2.9.x
編集
step: write
表示条件
① 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
2URL に ?code=(OAuthコールバック)Vercel でトークン取得 → push
3URL に ?note=(通知タップで起動)IndexedDB からノート読み込み → write
4token あり・pending_open あり(30分以内)IndexedDB からノート読み込み → write
5token あり・viewer_push_done=truelist(通常起動)
6token あり・push 未設定push(通知セットアップ)
7token なしlogin

2 モジュール構造

フロントエンド・Service Worker・Vercel API の3層構成です。

2.1 フロントエンド(TypeScript / React)

画面を構成するコンポーネント・フックの依存関係を示します。 また、図には現れない共通ユーティリティファイルも以下の表に一覧します。

表 2.1-1 共通ユーティリティ(viewer/ 配下)

Noファイル名役割
1types.tsフロントエンド全体で共有する型定義(DraftRecord 等)
2utils.ts依存を持たない汎用的なユーティリティ関数群
3editor-helpers.tsテキストエリア操作やタグ管理(localStorageとの連携)を担うヘルパー

図 3-2 フロントエンドモジュール構成

2.2 Service Worker(worker/index.js)

push受信・notificationclick等を処理するSWのイベントハンドラ5点を一覧します。

表 2.2-1 SW イベントハンドラ一覧

Noイベント処理内容
1installskipWaiting() 呼び出し。新バージョンを即時有効化。
2activateclients.claim() でページの制御を取得。SW バージョンをログに記録。
3push① Push ペイロード(title / body_rich / id)を取得
fusen-meta からアクセストークンを取得
③ Drive から画像をダウンロード
fusen-drafts にノートを保存
⑤ Drive から画像ファイルを削除
notes_to_iphone.json から当該 ID を削除
pending_openfusen-meta に記録
⑧ 既存の同 ID 通知を閉じてから新規通知を表示
4notificationclick通知をタップ → locked 確認 → true なら再通知・アプリを前面に出す。
⚠️ iOS では発火しない(既知の制約)。タップ後の再通知は page.tsxpending_open フローが代替。
5messageアプリからの通信を受信。CLOSE_NOTIFICATION で通知を閉じる、GET_VERSION で SW のバージョンを返す等の処理。

2.3 サーバーサイド(Vercel API Routes)

この節は、主に開発者・保守担当向けです。iPhone PWA が Google Drive 用トークンを取得・更新するときに使う Vercel API エンドポイント2点を説明します。

表 2.3-1 Vercel API Routes 一覧

Noファイル役割
1app/api/auth/token/route.tsOAuth 認証コード → アクセストークン+リフレッシュトークン交換。初回ログイン時のみ呼ばれる。
2app/api/auth/refresh/route.tsリフレッシュトークン → 新しいアクセストークン取得。Drive API 呼び出し時にトークン期限切れを検出したら自動呼び出し。

環境変数(Vercel)

Vercel の Environment Variables に設定する変数の一覧です。.env ファイルやコードにハードコードしないこと。

表 2.3-2 Vercel 環境変数一覧

No変数名公開範囲取得元用途
1GOOGLE_CLIENT_SECRET_PWAサーバー専用Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント シークレット開発者が守る値。iPhone PWA が Google Drive 用トークンを取得・更新できるように、Vercel サーバーから Google へ提示する。iPhone PWA には入れない。端末上で読める場所に置くと、第三者が俺の付箋のアプリ名義で OAuth 処理を悪用する恐れがあるため。
2NEXT_PUBLIC_GDRIVE_CLIENT_ID公開可Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント IDiPhone PWA が Google OAuth フローを開始するために使う公開ID。ここでいう client はユーザー端末ではなく Google に登録した「俺の付箋アプリ」を指す。公開前提なので iPhone PWA に含めてよい。
3DISCORD_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.jsonkeys個々の 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フィールド用途・内容
1idstring主キー(UUID)
2titlestringノートタイトル
3bodystring本文(Markdown)
4imagesObject[]添付画像({ fileName: string, blob: Blob }[]
5videosObject[]添付動画({ fileName: string, blob: Blob }[])。ユーザー本文とは別に保持する
6tagsstring[]付与されたタグの配列
7lockedbooleanロック画面に表示が ON なら true
8created_atstring作成日時(JST ISO 8601)
9sent_atstring送信日時(未送信時は undefined)
10received_pcbooleanPC 側が受信済みかどうか

3.1.2 🗄 fusen-meta(メタ情報)

表 3-5 fusen-meta スキーマ

Noキー用途・内容
1access_tokenGoogle Drive アクセストークン(SW が参照)
2pending_open次回起動時に開くノートの情報。{ id: string, t: number }

3.1.3 🗄 fusen-logs(デバッグログ)

表 3-6 fusen-logs スキーマ

Noフィールド用途・内容
1tタイムスタンプ(JST)
2msgログメッセージ

3.2 localStorage

セッション管理に使うlocalStorageのキーと値の一覧です。

3.2.1 🔑 認証・設定フラグ

表 3-7 localStorage キー一覧

Noキー用途・内容
1viewer_access_tokenGoogle API アクセストークン
2viewer_refresh_tokenリフレッシュトークン(Vercel API 経由で更新)
3viewer_expires_atアクセストークンの有効期限(ms)
4viewer_push_done"true" なら通知設定済み → list へ直行
5pkce_verifierOAuth PKCE の code_verifier(認証中のみ存在)
6pending_notePKCE 認証後に自動で開くノート ID
7viewer_device_idWeb Push 用のクライアント識別子
8fusen_known_tags過去に入力したタグの履歴(サジェスト用)

3.3 Google Drive ファイル

PCとiPhone間の中継、および Web Push 設定に使う Drive ファイルの種類と書き込み/削除責務を説明します。

表 3.3-1 Google Drive ファイル一覧

Noファイル名書き込み読み取り・削除用途
1notes_to_iphone.jsonPC(gdrive.rs)iPhone SW
(push受信時)
PC から iPhone へメモ本文と添付画像名を渡すために、未処理ノートを一時保存する。SW が受信して IndexedDB に保存後、当該 ID を除いて書き戻す(または全削除)する。
2notes_from_iphone.jsoniPhone
(useBackgroundSend)
PC(gdrive.rs
30秒ポーリング)
iPhone から PC へメモ本文、添付画像名、添付動画名を渡すために、未処理ノートを一時保存する。PC 受信後は処理済みアイテムを除いた残りのみ書き戻す。
3fusen_img_*.jpgiPhone
または PC
受信側が処理後に削除Push ペイロードや JSON に大きな画像バイナリを直接入れないために、添付画像だけを一時ファイルとして保存する。受信側が IndexedDB または PC の assets/ に保存後、削除する。
4fusen_video_*.mp4
fusen_video_*.mov
iPhone
(useBackgroundSend)
PC(gdrive.rs
30秒ポーリング)
動画バイナリを JSON や付箋本文に埋め込まないために、添付動画を一時ファイルとして保存する。PC が assets/video/ に保存し、付箋本文へ保存先パスを追記した後に削除する。
5push_keys.jsonPC(webpush.rs)
初回のみ作成
iPhone(lib/push.ts)が
公開鍵を読む
PC(webpush.rs)が
秘密鍵を読む
VAPID 鍵ペア。iPhone は公開鍵で Push 購読、PC は秘密鍵で送信時に署名する。鍵の所有者・目的・防衛手段は 3.0 鍵の前提、詳細は 表 3.3-4 を参照。
6push_devices.jsoniPhone(lib/push.ts)
が upsert
PC(webpush.rs)が
全端末へ Push 送信
PC が登録済み iPhone へ Push を送るために、端末ごとの device_id / endpoint / 暗号化鍵を保存する。複数端末へ送るために、端末一覧として保持する。
7pc_devices.jsonPC(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フィールド必須用途・内容
1itemsObject[]未処理ノートの配列。最大20件を保持
2items[].idstringノートID(UUID)
3items[].titlestring通知・表示タイトル
4items[].bodystringMarkdown本文。画像は fusen_img_* 参照
5items[].tagsstring[]タグ一覧
6items[].sent_atstringPC送信時刻
7items[].received_atnull現行ソースが互換目的で出力している残項目。処理判定には使わない
json
{
  "items": [
    {
      "id": "uuid",
      "title": "買い物",
      "body": "牛乳\n![photo](fusen_img_20260505_120000_0.jpg)",
      "tags": ["shopping"],
      "sent_at": "2026-05-05T12:00:00Z",
      "received_at": null
    }
  ]
}

表 3.3-3 notes_from_iphone.json(iPhone → PC 未処理キュー)

Noフィールド必須用途・内容
1itemsObject[]未処理ノートの配列
2items[].idstringノートID(UUID)
3items[].titlestringノートタイトル
4items[].bodystringMarkdown本文。画像は fusen_img_* 参照。ユーザーが入力した本文であり、添付動画のファイル名で上書きしない
5items[].sent_atstringiPhone送信時刻
6items[].tagsstring[]タグ一覧
7items[].imagesObject[]添付画像一覧。各要素は Drive 一時ファイル名を持つ
8items[].videosObject[]添付動画一覧。各要素は { videoFileName, originalFileName } を持つ。複数動画可
9items[].videoFileName / items[].originalFileNamestring旧実装互換用の先頭動画情報。新規実装では videos[] を正とする
10items[].targetPcIdstring複数PC接続時の送信先PC ID。未指定の旧データは従来互換として全PCが受信対象にできる
json
{
  "items": [
    {
      "id": "uuid",
      "title": "外出先メモ",
      "body": "帰ったら確認\n![](fusen_img_20260505_120000_0.jpg)",
      "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フィールド必須用途
1public_key_b64urlstringiPhone が pushManager.subscribe()applicationServerKey に渡す
2private_key_b64urlstringPC が VAPID JWT に署名し Authorization: vapid t=...,k=... として送る
3subjectstringVAPID JWT の sub。現行値は mailto:ore-no-fusen@example.com
json
{
  "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フィールド必須用途・内容
1devicesObject[]登録済み端末の配列
2devices[].device_idstring端末ID。iPhone PWA が localStorage に保持
3devices[].endpointstringAPNs / Push Service の送信先URL
4devices[].keys.p256dhstringPush 暗号化用の公開鍵(ECDH)
5devices[].keys.authstringPush 暗号化用の認証シークレット
6devices[].registered_atstring登録時刻
7devices[].device_namestring表示用端末名
8devices[].google_account_emailstringDrive接続アカウントのメール
9devices[].google_account_namestringDrive接続アカウント名
10devices[].google_account_photostringDrive接続アカウントの画像URL
json
{
  "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フィールド必須用途・内容
1pcsObject[]登録済みPCの配列
2pcs[].pcIdstringPCを一意に識別するID。PC側ローカルにも保持し、受信時の自分宛判定に使う
3pcs[].pcNamestringPWAの送信先プルダウンに表示するPC名
4pcs[].registeredAtstring初回登録時刻
5pcs[].updatedAtstring最終更新時刻
6pcs[].googleAccountEmailstringどのGoogleアカウントで登録されたPCかを確認するためのメールアドレス

PWA の通常画面では、ユーザーに pcId を選ばせない。送信先は「家のPC」「会社のPC」のような pcName で選ばせる。同じ pcName の登録が複数ある場合は、PWA側で updatedAt が最も新しい登録を採用し、古い同名登録は通常候補に出さない。pcId は受信判定とトラブル診断用の内部IDであり、通常操作の判断材料にしない。

json
{
  "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.jsonpush_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機能設計意図・工夫
1Drive → 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.readyreg.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 に通知クローズを依頼する。メインスレッドは通知を直接閉じられない
7locked フラグの永続化locked: true/false を IndexedDB に書き込む。次回 Push 受信時に SW がこの値を参照して再通知を行うかどうかを決める
8効果音ON 時は bell_on.wav、OFF 時は bell_off.wavAudio API で再生。操作の結果をユーザーが音で確認できる

6.3 メモ編集画面(write)

表 6.3-1 メモ編集画面の機能

No機能設計意図・工夫
1contenteditable エディタcontentEditable="true"div で実装。React の value/onChange モデルを使わず DOM を直接操作。serializeEditor() が HTML → Markdown 形式に変換する
23秒自動保存入力のたびにタイマーをリセット。3秒間入力がなければ IndexedDB に保存(useAutoSave)。Drive は使わない。ネットワーク不要・オフライン動作
3バックグラウンド遷移時の強制保存visibilitychange で非表示になった瞬間にも保存(useVisibilitySave)。アプリを閉じてもデータが消えない
4📷 画像添付ファイル選択 → CropModal でトリミング → IndexedDB に Blob 保存 → buildImageFileName() でファイル名生成 → カーソル位置に ![]() を挿入。PCへ送るときに Drive に実際にアップロードされる
5🔷 MermaidMermaidModal でフローチャートのコードを書いて確定するとエディタに挿入される
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🎬 VideoDropmp4 / mov を選択して現在の付箋に添付し、「PCへ送る」を押したときに Drive 経由で PC へ送る。画像と同じく付箋の添付部品であり、選択時に本文を上書きしない。PC側で assets/video/ に保存された絶対パスを付箋本文の末尾へ追記する

6.4 「PCへ送る」

表 6.4-1 PCへ送る の処理ステップ

Noステップ設計意図・工夫
1① トークン有効期限確認viewer_expires_atDate.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 への upsertDrive から 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バージョン日付変更内容
11.026-04-19新規作成。005_VIEWER_SCREENS.html / 007_VIEWER_CODE_STRUCTURE.html / 004_PWA_DATA_FLOW.html の内容を統合・整理
21.126-04-204.4 にロック画面常駐体験(REQ_IP_05)の再通知サイクル(①②③④)を追加。4.2 に REQ_IP_05 への参照を追加
31.226-04-204.4 の再通知フローを実態に合わせて修正。iOS では notificationclick が発火しないため pending_open + page.tsx が再通知を担う仕組みを図入りで明記。2.2 の notificationclick 説明に iOS 制約を追記
41.326-04-24モジュール構造図を graph LR(横向き)に変更。スクロールなしで全体が見えるよう改善。
51.426-04-27セクション6「機能一覧」を新規追加。メモ一覧・ロック画面常駐・メモ編集・PCへ送る・プッシュ登録の設計意図を記載。旧6→7、旧7→8に繰り下げ。
61.526-05-062.3 Vercel / OAuth、3 データ構造、6.1〜6.5 機能一覧を修正。説明対象を開発者・保守担当向けとして明記し、client_secret は開発者が守る値であること、Vercel がトークンを保存しないことを追記。表の「意味」を「用途・内容」に変更し、6.1-1 以降の表へ No を追加。
71.626-05-246.3 / 6.4 に VideoDrop を追加。PWAから mp4 / mov をDrive経由でPCへ送り、PC側で assets/video/ に保存して付箋本文へパスを記録する仕様を追記。
81.726-05-24VideoDrop を動画選択即送信から付箋添付後に「PCへ送る」で送信する方式へ変更。PC側本文にはクリック可能な絶対パスを記録する仕様へ更新。
91.826-05-25VideoDrop を複数動画対応の添付メディア仕様へ更新。videos[]、IndexedDB の videos、Drive 一時ファイル fusen_video_*、本文保護ルールを追加。
101.926-05-30複数iPhone・複数PCの接続モデルを追加。PC→iPhoneは登録済み通知端末への同報送信、iPhone→PCはpc_devices.jsontargetPcIdで送信先PCを選択する仕様を明記。
111.1026-05-30Web Push共有鍵の目的・保護対象・誰にとっての秘密かを追記。push_keys.json はユーザー本人のDrive上の1個を正とし、PCローカル鍵で上書きしないルールを明記。
121.1126-05-30PC→iPhone / iPhone→PC 送信時のキュー保護を明記。Driveキュー取得失敗時は空配列で上書きせず送信を中止し、PC→iPhoneはDrive保存成功後にPushを送る。Push失敗はHTTPステータス別に分類して表示する。
131.1226-05-30PC右クリックメニューの「iPhoneに送る」を常時表示し、未設定時は設定画面の iPhone 連携タブへ誘導する仕様を明記。
141.1326-05-313.0「鍵の前提」を新設。鍵を 所有者・目的・防衛手段 の 3 観点で記述するルールと、登場する鍵の一覧表を追加。VAPID 鍵セクションの 6 つの Note を 1 つの鍵プロファイル表と同期ルールに統合し、重複を削減。push_devices.json 内 ECDH 鍵にも同じ枠組みを適用。
151.1426-05-313.0 を「3 者の登場人物と関係 → 鍵の枠組み → 鍵一覧 → VAPID 補足」の順に再編成。ユーザー / 俺の付箋アプリ開発者 / 悪意ある第三者 の 3 者と互いの警戒関係を表で明示。「作者」「攻撃者」「第三者」表記を統一し、用語の揺れを解消。
161.1526-05-31PWAの送信先PC選択は pcName を通常表示とし、同名PCが複数ある場合は updatedAt が最新の登録へ自動的に寄せる仕様を追記。pcId は受信判定・診断用の内部IDであり、通常操作でユーザーに選ばせないことを明記。
171.1626-05-31設定画面の接続状態で Drive 未処理キューの中身確認と、ユーザー確認付きのキューJSON削除を行える仕様を追記。
181.1726-06-05PC→iPhone送信直前に push_devices.json を Drive から再取得する仕様を明記。予見可能なPush不整合はアプリ側で回避し、エラー時はユーザーが取れる復旧手順を表示する方針を追記。