← 一覧に戻る

review-antenna

GitHub ↗ TypeScript 最終push: 2026/6/26 20:59

WIP(現在進行中)

Work In Progress

このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。

現在の状況

RBAC(複数ユーザー共同管理)を本番反映済み(2026-06-26、migration 0010/0011 適用)。 公開マルチテナント版が本番稼働中。Node.js (Hono) + node:sqlite + systemd。

  • 本番: https://reviewantenna.ikapps.com/ (Google OAuth ログイン必須、Nginx + Let's Encrypt)
  • RBAC: 1アカウント(テナント)を複数ユーザーで権限分けして共同管理
    • ロール: オーナー / 編集者 / カスタマー対応(閲覧+返信) / 閲覧者(閲覧+通知受信)。memberships で (テナント↔ユーザー↔ロール)
    • メール招待(/members、オーナーのみ)→受諾(メール一致必須+verified)→参加。アンカーowner固定(v1は所有者移譲なし)
    • ヘッダのテナント切替セレクタ(active_owner_id をセッション保持)。ロール別にUI出し分け(編集/返信/メンバー管理)
    • 通知はテナントの購読メンバー全員へ配信(review_deliveries で(レビュー×メンバー)単位、channel無し除外、参加前は cutoff で除外)
    • 大方針: 既存 user_id を「テナントID」として温存・再解釈(apps/reviews/credentials 無改造)。 channels だけは本人データ(テナント再解釈の例外)。高権限POSTは CSRFトークン+form.owner_id で作用先確定(stale form対策)
  • 既存機能(不変): 複数認証情報+まとめて登録(0009)、/app 詳細・返信、/exportGET /reviews(READ_TOKEN・運営用横断)、 毎時 systemd timer で全テナント poll→sqlite→SESメール通知。apps.credential_id で取得鍵を明示
  • secret/設定は /home/ubuntu/apps/review-antenna/.dev.vars(APP_ENC_KEY 固定)

残・次にやること:

  1. 本番で You が実ブラウザ(Google OAuth)でログインし、招待・テナント切替・ロール別UIの最終確認
  2. (任意)Google まとめて登録の実使用時は各SAで Play Developer Reporting API 有効化+権限付与
  3. (任意)Resend/SES 独自ドメイン認証、実運用ドメイン reviewantenna.com 引っ越し
  4. (任意・RBAC将来)所有者移譲、通知メールへのテナント名表示、channels の active=0 で「無効化」と「削除」を区別

注意: システム時計が実時刻とズレている(DEVLOGの時刻はユーザー確認した実時刻を使う)。 重要: 本番 APP_ENC_KEY は固定。変えると暗号化済み認証情報が復号不能になる(同一サーバー/.dev.varsを維持)。 メモ: RBAC適用前の本番DBバックアップは ~ubuntu/ra-backup-20260626-182147.db。サーバーに sqlite3 CLI は無く、migration は node:sqlite--experimental-sqlite)で適用。migrationは「未適用を一度だけ」流す運用(0010の ALTER TABLE は再適用で失敗、適用前に PRAGMA table_info で確認)。

詳細は本番運用情報含めプロジェクトルートの 引き継ぎ.md に集約。


過去のWIPアーカイブ

(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)

2026-06-26 19:35 アーカイブ(RBAC前・0009反映状態)

公開マルチテナント版が本番稼働中。複数認証情報対応+まとめてアプリ登録を本番反映済み(2026-06-25、migration 0009)。

  • 本番 reviewantenna.ikapps.com、Google OAuth 必須・テナント分離
  • /settings で認証情報をニックネーム付きで複数登録、/apps/import でまとめて登録
  • apps.credential_id で各アプリの取得鍵を明示、poll/返信も credential_id 単位で鍵解決
  • 残: Play Reporting API、Resend/SES独自ドメイン、reviewantenna.com 引っ越し、オンボーディング導線

2026-06-25 20:00 時点のスナップショット

公開マルチテナント版が本番稼働中(2026-06-08 cutover完了)。複数認証情報対応+まとめて登録を 実装・本番反映する直前の状態。

  • 本番: https://reviewantenna.ikapps.com/ (Google OAuth 必須、テナント分離)
  • /settings で per-user 認証情報(ASC/Google SA)を 1ストア1つだけ登録→AES-256-GCM保存
  • 毎時 systemd timer で全テナントをポーリング→sqlite→メール通知(SES)
  • 運営 me は既存アプリ・レビューを保持。env フォールバックは撤去済み(DBの credentials を使用)
  • 残: Resend/SES独自ドメイン認証、reviewantenna.com 引っ越し、オンボーディング導線、通知チャネル追加

2026-06-03 16:30 時点のスナップショット

本番稼働 + 管理ダッシュボード(一覧/詳細)+ レビュー取得API + ★評価総数表示。 監視3アプリ(Vision Workout / Study Timer / 勉強タイマー)。 アプリ登録UI・アイコン表示・JSONダウンロードはこの後(18:12 セッション)で実装。 次: Slack/ntfy通知、レビュー集計、Resend独自ドメイン認証。

2026-06-02 時点のスナップショット

MVP完成・本番稼働中(App Store / Google Play 両対応)。

  • URL: https://review-antenna.ichirokisanuki.workers.dev
  • 毎時0分の Cron で両ストアの新着レビューを取得 → 新着のみD1保存 → メール通知
  • 監視中: App Store「Vision Workout」(1563884276) / Google Play「勉強タイマー」(jp.thomsons.pomodoro)
  • 通知先: ichirokisanuki@gmail.com(Resend、送信元 onboarding@resend.dev
  • secret登録済み: ASC3点 / Resend2点 / RUN_TOKEN / GOOGLE_SERVICE_ACCOUNT
  • 次: Resend独自ドメイン認証、チャネル追加、公開サービス化

ROADMAP(計画)

ロードマップ

完了

  • 技術スタック確定(Cloudflare Workers + D1 + Cron、Resend)
  • App Store レビュー監視(ASC API、JWT/ES256)
  • 本番デプロイ・Cron稼働
  • メール通知(Resend)
  • 初回 seed 処理・冪等性・SQL変数上限対策
  • アプリ名自動取得・メール表示整形
  • /run のトークン認証
  • Google Play 対応(サービスアカウント認証)
  • Google Play のアプリ名自動取得(Play Store og:title)
  • レビュー取得API GET /reviews(READ_TOKEN認証)
  • App Storeレビューのページング対応
  • 管理ダッシュボード(アプリ一覧・レビュー詳細、無認証)
  • ★評価総数の取得・表示(App Store / Google Play)
  • 画面からアプリ登録・削除(登録時に名前/評価/アイコン同期取得)
  • アプリアイコンの自動取得・一覧表示
  • 全レビューのJSONダウンロード(/export)
  • subrequest上限対策(seedを1回最大3アプリに分割)
  • レビューへの手動返信(App Store / Google Play)
  • 複数認証情報対応(同一ストアに複数鍵・ニックネーム)+まとめてアプリ登録(migration 0009、2026-06-25)

今週

  • 一覧のフィルタ/並べ替え(ストア別・評価の低い順・★1だけ等)
  • Resend 独自ドメイン認証(任意宛先に送信可能に)
  • (任意)まとめて登録の Google 対応を実使用するなら、本番の各サービスアカウントで Play Developer Reporting API を有効化+権限付与
  • (任意)アイコン割れ時のプレースホルダフォールバック、認証情報の label リネームUI

今月

  • 通知チャネル追加(Slack / ntfy / LINE のいずれか)
  • レビュー集計・分析(評価推移など)

今四半期 — 公開サービス化(単一コードのマルチテナント化)

  • Phase 1: 基盤 — users/sessions、Google OAuth ログイン、ログイン必須化・テナント分離(2026-06-08)
  • Phase 2: per-user 認証情報 — credentials(AES-256-GCM)、アップロードUI+接続テスト、poll/asc/gp テナント鍵対応(2026-06-08)
  • Phase 3 cutover: 本番を多人数版へ切替(reviewantenna.ikapps.com、ログイン必須)(2026-06-08)
  • RBAC(複数ユーザー共同管理)— オーナー/編集者/カスタマー対応/閲覧者の4ロール、メール招待+メンバー管理、 テナント切替、通知のメンバー配信。migration 0010/0011。Codex 3ラウンドレビューで設計収束(2026-06-26)
  • RBAC将来拡張: 所有者移譲、通知メールへのテナント名表示、channels の active=0 で「無効化」と「削除」を区別
  • Phase 3 残: Resend 独自ドメイン認証(他ユーザー通知用。reviewantenna.com 移行時に同時実施推奨)
  • 実運用ドメイン reviewantenna.com への引っ越し(設定のみ。コード/データ変更なし)
  • 新規ユーザーのオンボーディング導線(サインアップ後の通知チャネル自動作成など)
  • (実ユーザー増時)$10/1GB 増強、tsx→ビルド済みJS で軽量化

いつか

  • Amazon レビュー対応
  • レビュー返信機能(ASC / Play の返信API)→ 手動返信を実装済み(AI返信ドラフトは将来)
  • レビュー分析(評価推移、感情分析など)

DECISIONS(意思決定)

意思決定記録

このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。


2026-06-26: RBAC(複数ユーザー共同管理)の主要設計判断

背景: 1アカウントを複数ユーザーで権限分けして共同管理したい。既存は「1ユーザー=1テナント」で 全テーブルの user_id が所有者とテナントを兼ねている。共同管理には「テナント」と「メンバー/ロール」を 分離する必要があり、データモデル・認可・招待・通知の各層で方針を決める必要があった。設計は3エージェント 並列調査でたたき台→Codex 3ラウンドレビュー(R1 high3→R3 収束)で固めた(.devnotes/rbac-handoff.md)。

決定: 以下を確定し Phase 1〜4 で実装・本番反映した。

  • データモデル: 既存 user_id を「テナントID」として温存・再解釈する(apps/reviews/credentials は スキーマ無改造)。新テーブル memberships(owner本人も role='owner' で1行)と invitations を追加。
  • channels は例外(本人データ): テナント再解釈の対象外とし channels.user_id はログインユーザー本人。 通知先は各メンバーが自分で管理し、テナント削除でも消さない。
  • テナントコンテキスト: sessions.active_owner_id で保持+ヘッダのセレクタで切替。状態変更POSTは フォームの owner_id で作用先テナントを明示解決して認可(セッションのactiveだけに依存しない)。
  • 招待の本人性: メール招待リンク方式。受諾は「招待メール == ログインメール」一致必須+Google email_verified。トークン保持者なら誰でも可、は採らない。
  • 通知の既読: 単一 reviews.notified ではなく review_deliveries で (レビュー×メンバー) 単位に記録。 channel無しメンバーは配信対象外(分母に入れない)、notified は「対象全員配信済み or 対象0」で1。
  • アンカーowner固定: テナントID本人の owner membership は削除・降格不可。v1は所有者移譲なし。
  • CSRF: 高権限POSTにセッション単位のCSRFトークンを導入(sessions.csrf_token)。

理由: user_id 温存は本コードベースの設計思想に最も素直で移行リスク最小(全 WHERE user_id=? の 渡し値を差し替えるだけ)。channels本人データ化はプライバシー(他人の宛先を抱えない)と「各自が自分の通知を 管理」という直感に合致。form.owner_id 認可は複数テナント所属時の stale form 誤爆を防ぐ(Codex R2 high指摘)。 メール一致必須はトークン漏洩・転送時の被害を限定(本番テナントの読み書き権限を渡す行為のため)。 review_deliveries 方式は複数受信者での一部失敗時の取りこぼし/二重送信を防ぐ。アンカー固定は ownerEmail 表示の ズレ・「自分のテナントから抜ける」破綻を回避しv1スコープを最小化(移譲は将来 tenants テーブルで拡張可)。

2026-06-25: 複数認証情報のモデルと「まとめて登録」のスコープ

背景: 「1認証情報=そのキーで見えるアプリだけ監視できる」制約があり、別アカウント配下のアプリを 監視するには複数の認証情報が要る。同時に、ASC/サービスアカウントは「アクセス可能な全アプリ」を 列挙できるため、手入力ではなく一覧からまとめて登録したい。複数鍵にすると「各アプリをどの鍵で 取得するか」を明示する必要が生じる。

決定:

  1. credentialsUNIQUE(user_id,platform) を撤廃し、ニックネーム label で複数登録可能に。 appscredential_id を追加し、各アプリの取得鍵を登録時点で確定させる。
  2. まとめて登録は App Store / Google Play 両対応。App Store は ASC GET /v1/apps、 Google は別API(Play Developer Reporting API apps:search、別スコープ)を使い、 失敗時は手動入力にフォールバック。
  3. 複数鍵の見分けはユーザー入力のニックネーム(自動ラベルは不採用)。

理由: 「鍵→アプリ一覧→登録」という自然な流れの中で credential_id が確定するため、 別案(poll時に複数鍵を総当たり)より無駄なAPI呼び出し・401/404が無くクリーン。既存アプリは 移行SQLで現行の単一鍵へ自動紐付くため破壊性が低い。Google は Reporting API の有効化・権限付与が 前提で全環境で成功するとは限らないため、ベストエフォート+手動フォールバックに留めた。 ニックネームは Issuer ID/client_email の自動表示より識別しやすい。


2026-06-12: サービス名は Review Antenna で確定(ReviewReply 案は不採用)

背景: マネタイズの軸として「レビューへの返信(+AI返信ドラフト)」機能を検討開始。返信が売りなら 受信メタファーの「Antenna」は名前と価値提案がズレるのではという疑問が出て、ReviewReply 案を検討した。

決定: サービス名は Review Antenna のまま確定。reviewantenna.com は空きを確認済み(購入は返信機能の手応えを見てから)。

理由: reviewreply は .com/.io/.ai すべて取得済み(.ai は2024年取得=AI返信系の競合が既にひしめく激戦ワード)。 一般名詞2語で指名検索に埋もれ、かつ「返信ツール」に名前が縛られて監視・通知・AI要約への拡張と再びズレる。 入口機能(見逃さない)を名乗り、本命機能(返信)はコピーで売る構造の方が素直。

2026-06-08: 公開サービス化=単一コードのマルチテナント化(2インスタンス並走はしない)

背景: 個人専用サービスを一般ユーザーも使える公開サービスへ拡張したい。当初「個人版=me./公開版=apex」で サブドメイン分岐し2インスタンス並走する案も検討したが、Lightsail は 512MB(空き約140MB)で、Node常駐を 2本立てると基礎使用量(RSS、アクセス数とは無関係)で逼迫する懸念があった。

決定:

  • 単一コードベースをマルチテナント化する(2インスタンス並走はしない)。個人利用は「テナント me」として 同一アプリ内で継続。データモデルは既に全テーブル user_id 完備のため素直に伸ばせる。
  • 既存データの user_id='me' はそのまま運営ユーザー(users.id='me')に対応づけ、移行コストを最小化。
  • サブドメイン分岐は機能的に不要化(全員 apex にログイン)。me. は残すなら見た目・入口だけ。
  • 開発は public ブランチ + ローカルで行い、安定後にサーバーの1本を置き換える。サーバー上で2本同時に 動くのは切替の瞬間だけ → メモリ問題を回避。
  • 認証は Google OAuth。ユーザー認証情報(ASC .p8+KeyID+IssuerID / Google SA JSON)は Webアップロード→AES-256-GCMで暗号化しDB保存(運営マスター鍵は env)。Resend は運営1本で代理送信 (独自ドメイン認証が前提)。
  • 実ユーザーが付いたら Lightsail を $10/1GB へ増強、tsx→ビルド済みJSで軽量化。

理由: データモデルが最初からマルチテナント前提だったため、別アプリ化より単一コード拡張が自然で 重複保守がない。ローカル開発+ブランチなら個人運用を一切壊さずに公開版を育てられ、メモリ制約も回避できる。 この決定は 2026-06-03「管理ダッシュボードは無認証」を公開版では覆す(公開版はログイン必須・テナント分離)。


2026-06-06: Cloudflare Workers + D1 → AWS Lightsail へ移行

背景: 当初 Cloudflare Workers + D1 + Cron で構築したが、subrequest 上限(19アプリ一括seedで超過)や D1 の SQL 変数上限など、ポーリング型・全件取得ワークロードと Workers 制約の相性問題が積み重なった。 既に Lightsail(multi-purpose-lightsail-server1)で複数サービスを Nginx 集約運用しており、 そこに相乗りすれば制約から解放され、運用も一元化できる。

決定: Node.js (Hono) + node:sqlite + systemd で Lightsail に移行。D1 互換ラッパー(src/db.ts)で 既存ロジックを無改変流用し、index.tsserver.ts(常駐) + poll.ts(毎時timer) に分割。 旧 Worker + D1 は完全削除。ドメインは reviewantenna.ikapps.com

理由: subrequest/SQL変数の上限がなくなり全アプリ毎回・全件取得が可能。固定費は既存サーバーに同居で実質ゼロ増。 D1互換ラッパーにより移行コストを最小化。この決定は下記 2026-06-02 の Cloudflare 採用を覆すもの。

注意点(移行で得た知見): ① Node 22 の node:sqlite--experimental-sqlite 必須。 ② systemd の EnvironmentFile は値中の \n をエスケープ解釈して PEM/JSON を壊すため、秘密情報は .dev.vars を dotenv で読む方式に統一した。


2026-06-03: 新規アプリのseedは1回のpollで最大3つに分割

背景: 19アプリを一括登録したところ、1回のポーリングで全アプリの全件取得(各最大数十ページ)が走り、Cloudflare の subrequest 上限(1実行あたりの外部リクエスト数)を超えてエラーになった。

決定: 全件seed(重い処理)は1回のpollで最大3アプリまで。残りは次回以降のCronで順次seedする。名前・評価総数・アイコンの軽い更新は全アプリ毎回実行。

理由: subrequest上限に収めつつ、多数アプリ登録にも耐える。新着検知(seed済みアプリは最新1ページ)は軽いので影響なし。seedは数回のCronで完了する。

2026-06-03: レビューのエクスポート形式はJSON

背景: 詳細画面から全レビューをダウンロードする際の形式(JSON / Markdown 等)を選ぶ必要があった。

決定: JSON(app情報のメタ + reviews配列)。ファイル名は {アプリ名}_{ストア}_{yyyymmdd}.json

理由: 「AIが読みやすい形式」という要件に対し、構造化されパース可能なJSONが最適。AIにアプリ単位で渡して分析させやすい。

2026-06-03: 管理ダッシュボードは無認証

背景: 本番サイトにアプリ一覧・レビュー一覧のダッシュボードを追加するにあたり、認証(Basic/トークン)を付けるか決める必要があった。

決定: 無認証で公開する(//app?id=)。

理由: 表示するレビューは元々 App Store / Google Play で一般公開されている情報のため、無認証でも実害は限定的。手軽さを優先。将来センシティブな情報(売上等)を載せる段階で認証を検討する。

2026-06-03: DB は D1 を継続(スケール時に段階移行)

背景: D1 はリレーショナルDBだが、書き込み単一プライマリ・1DB=10GB上限があり、スケール懸念の指摘があった。

決定: 当面 D1 を継続する。

理由: レビュー監視は書き込みが軽く読み取り・集計中心でRDBと相性が良い。今は自分専用なので D1 単一で十分。スケールの壁が見えたら、まずテナント別D1(Cloudflare推奨パターン)、それでも足りなければ Postgres(Neon/Supabase+Hyperdrive)移行という段階方針。SQL資産はほぼ流用できる。


2026-06-02: App Store は公式 ASC API を継続(RSS案を不採用)

背景: Apple Developer Program メンバーシップが期限切れで、公式 ASC API が使えるか不安があった。代替として認証不要の公開 RSS フィード案も検討した。

決定: 公式 App Store Connect API(customerReviews)を使い続ける。

理由: 実際に叩いたところ、メンバーシップ期限切れでも customerReviews は取得できた。RSS は他人のアプリも取れる利点があるが(公開サービス化では再検討の価値あり)、まずは全期間取得可・構造が安定している公式 API で進める。

2026-06-02: レビュー重複防止は INSERT OR IGNORE 方式

背景: 当初は既存IDを SELECT してから差分を INSERT していたが、IN句に200件渡して D1 の SQL 変数上限(100)を超え too many SQL variables で落ちた。

決定: 事前 SELECT を廃止し、id を PRIMARY KEY にした上で INSERT OR IGNORE + meta.changes 集計で新着件数を得る。一括 UPDATE は50件チャンク分割。

理由: SQL変数上限に依存せず、コードもシンプルになる。冪等性も自然に担保される。

2026-06-02: メール送信は Resend を採用

背景: Cloudflare Workers からのメール送信手段を選ぶ必要があった。

決定: Resend を採用。

理由: Workers と相性が良く実装が容易、無料枠 月3000通、独自ドメイン認証も容易。MailChannels は2024年に無料プランが終了したため不採用。Amazon SES は初期設定が重い。

2026-06-02: 技術スタックは Cloudflare Workers + D1 + Cron

背景: 定期ポーリング型のレビュー監視サービスの基盤選定。Lightsail サーバー案もあった。

決定: Cloudflare Workers + D1 + Cron Triggers。

理由: 無料枠が広く、Cron で定期ポーリング・D1 で既読位置の状態管理ができる。公開サービス化しても耐えられる構成。

DEVLOG(作業ログ)

開発日誌

このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。


2026-06-26

19:35 - RBAC(複数ユーザー共同管理)を設計→実装→本番デプロイ

1アカウント(テナント)を複数ユーザーで権限分けして共同管理する機能(RBAC)を新規追加。 3エージェント並列調査で設計を固め、別LLM(Codex)の3ラウンドレビューで収束させてから Phase 1〜4 を実装し、本番(reviewantenna.ikapps.com)へ反映した。

やったこと:

  • 設計(.devnotes/rbac-handoff.md): 既存 user_id を「テナントID」として温存・再解釈する大方針。 memberships/invitations を追加し apps/reviews/credentials は無改造。Codexレビュー R1(high3/med5/low2)→R2(high1/med3/low2)→R3(収束判定)を全反映。やりとりは .gods-talk/ に記録
  • Phase 1(migration 0010・src/authz.tsauth.ts拡張): memberships/invitations(部分UNIQUE)/ sessions(active_owner_id・csrf_token)、全ユーザーを owner として backfill、resolveTenant/getRole/ canEdit 等の認可ヘルパー、email小文字正規化・emailVerified・getSessionContext/setActiveOwner
  • Phase 2(server/dashboard/credentials): テナント解決ミドルウェア+gate()+CSRF。テナントデータは t.ownerUserId、channels は user.id。状態変更POSTは form の owner_id で作用先を明示解決 (stale form の別テナント誤爆対策)。ヘッダのテナント切替セレクタ、ロール別UI出し分け、settings分割
  • Phase 3(src/invitations.ts・notify.ts・members UI): メール招待→受諾(メール一致必須+verified)、 メンバー管理(アンカーowner保護)、sendMail 汎用化(招待のみResendフォールバック)、 OAuth callback の招待Cookie合流、通知購読(notify)トグル
  • Phase 4(migration 0011・poll.ts): review_deliveries で(レビュー×メンバー)単位配信、channel無し除外、 cutoff(COALESCE(notify_at,created_at) <= COALESCE(reviewed_at,fetched_at))、notified再計算
  • 各フェーズで npm run typecheck + e2e/結合テスト(CSRF・テナント切替・ロール別UI・stale form・ 招待受諾・アンカー保護・メンバー配信・冪等性)をパス
  • 本番デプロイ: push→rsync→npm ci→停止→DBバックアップ(ra-backup-20260626-182147.db)→ 0010/0011適用(owner backfill: users2=memberships2)→再起動。/health 200・/ 302・/login 200・ /reviews 401 を確認、ログにエラーなし

決めたこと:(詳細は DECISIONS.md 2026-06-26)channels は本人データ(テナント再解釈の例外)/ アンカーowner(テナントID本人)は固定・v1は所有者移譲なし/招待本人性はメール一致必須/ 通知既読は review_deliveries で(レビュー×メンバー)単位/テナントはセッション保持+POSTは form.owner_id で作用先確定/高権限POSTにCSRFトークン導入

詰まり/気づき:

  • dev-login は .dev.vars の Google OAuth 資格が読まれると無効化される → ローカル検証時は GOOGLE_OAUTH_CLIENT_ID= 空で起動して dev-login を有効化
  • Phase 4 テスト: owner の membership.created_at は backfill で実時刻になるため、過去日時のテスト レビューは cutoff で除外される(= 参加前の過去レビューを浴びせない挙動が正しく機能している証拠)
  • サーバーに sqlite3 CLI 無し → migration は node --experimental-sqlite で適用(前回 0009 と同様)

次回やること:

  • 本番で You が実ブラウザ(Google OAuth)でログインし、招待・テナント切替・ロール別UIの最終確認
  • 検証で dev-public.db にデモ用メンバーデータを投入済み(戻すなら dev-public.db.bak
  • AGENTS.md(Codex生成スタブ)は未追跡のまま

2026-06-25

20:00 - 複数認証情報対応+まとめてアプリ登録(実装→本番デプロイ)

「1認証情報=そのキーで見えるアプリだけ」という制約に対し、(1)同一ストアに複数の認証情報を 登録できるようにし、(2)キーがアクセスできる全アプリを列挙して一括登録できるようにした。 設計判断は DECISIONS.md(2026-06-25)参照。

やったこと:

  • migration 0009_multi_credentials.sql: credentialsUNIQUE(user_id,platform) を撤廃し ニックネーム label 追加(再作成パターン)。appscredential_id 追加し、既存アプリは 現行の単一鍵へ自動紐付け(移行)
  • 一覧取得API追加: ASC GET /v1/appslistAscApps)、Google は Play Developer Reporting API apps:searchlistGooglePlayApps、スコープ別=createGoogleToken を scope 引数化)
  • まとめて登録 /apps/import: 認証情報を選ぶ→そのキーのアプリ一覧をチェックボックス表示 (登録済みは無効)→選択分を credential_id 付きで一括INSERT
  • polluser単位→credential_id単位グルーピングに変更(鍵ごとにトークン使い回し)。 返信も対象アプリの credential_id から鍵解決。手動登録フォームは鍵セレクト式+ストアに応じた placeholder 切替
  • 設定画面: ストアごとに鍵を一覧(label/登録日/削除)+ニックネーム入力つき追加フォーム。 使用中の鍵は削除を拒否(先にアプリ削除を促す)
  • ローカル検証(dev-public.db): migration適用・複数鍵・まとめて登録(ASC実APIで42件取得、 登録済み12件は無効表示)・一括/手動登録のcredential_id紐付け・削除ガード・型チェック 全OK
  • 本番デプロイ: push→rsync→サービス停止→DBバックアップ→migration 0009適用(26アプリ全て credential_id紐付け・未設定0)→再起動→手動poll検証(checkedApps:26 / errors:[])。 ダウンタイム約40秒

詰まったこと / 気づき:

  • Response.redirect は絶対URL必須。新ハンドラで相対パスを渡し500→new URL(path, req.url) で修正
  • 本番サーバーに sqlite3 CLI が無い → アプリと同じ node:sqlite--experimental-sqlite)で適用
  • Google のまとめて登録は androidpublisher と別API・別スコープ。GCP側で Reporting API 有効化+ サービスアカウント権限付与が前提。未対応環境は一覧取得が失敗するため手動入力にフォールバック
  • 一覧の Google Play アイコン割れはサーバー側は正常(URLは全件200/PNG)。ブラウザ拡張による googleusercontent.com ブロックが原因と判断。コード変更なし(割れ時のプレースホルダ フォールバックは将来の小改修候補)

次回やること:

  • Google のまとめて登録を実使用するなら、本番の各サービスアカウントで Reporting API を有効化
  • (任意)アイコン割れ時のフォールバック、認証情報の label リネームUI

2026-06-08

公開マルチテナント化(Phase 1〜3 cutover)

個人専用サービスを一般公開向けのマルチテナント版へ拡張し、本番を切り替えた。public ブランチで 開発→ローカル検証→mainマージ→本番デプロイ。詳細な意思決定は DECISIONS.md(2026-06-08)参照。

やったこと:

  • Phase 1: users/sessions テーブル、Google OAuth ログイン+セッション(Cookie)、ダッシュボードの ログイン必須化・テナント分離(自分のアプリ/レビューのみ)。既存 user_id='me' を運営ユーザーに対応づけ
  • Phase 2: credentials テーブル(AES-256-GCM 暗号化)、/settings で per-user 認証情報 (ASC .p8/KeyID/IssuerID・Google SA JSON)をアップロード→接続テスト→暗号化保存。 poll をテナント単位に再構成(各自の鍵でトークン生成、未設定はスキップ)。me は env フォールバック
  • Phase 3 cutover: 本番 .dev.vars に GOOGLE_OAUTH/BASE_URL/APP_ENC_KEY 追加 → publicmain マージ → rsync → prod DB バックアップ&migration 0005/0006 適用 → 再起動。reviewantenna.ikapps.com が ログイン必須の多人数版に。本番検証(/→/login、OAuthリダイレクトhttps、dev-login無効、poll 20アプリ/エラー0)OK

詰まったこと / 気づき:

  • Google OAuth クライアントは web 種別で作成。リダイレクトURIに localhost:8003 と本番httpsの両方を登録。 同意画面テスト中はテストユーザー登録 or 公開が必要(email/profileのみなら審査不要)
  • ドメインはコードから BASE_URL(env) に外出し済みのため、後で reviewantenna.com へ移すのは設定変更のみ (コード・データ移行ゼロ、再ログイン1回)。APP_ENC_KEY は固定必須(暗号化済み認証情報の復号鍵)
  • 本番には sqlite3 CLI が無いため、migration は node:sqlite の exec で適用した

残: Resend独自ドメイン認証(reviewantenna.com移行時に同時推奨)、新規ユーザーのオンボーディング導線。


2026-06-06

20:10 - Cloudflare Workers/D1 → AWS Lightsail へ移行完了

別セッションで進めていた移行(Node移植・D1データ取り込み)の残りを処理し、本番をLightsailへ切替。

やったこと:

  • 本番デプロイ: /home/ubuntu/apps/review-antenna/ へ rsync、npm ci --omit=dev、DB(14277件)を /var/lib/review-antenna/ へ配置(整合性チェックOK)
  • systemd: 常駐 review-antenna.service(Hono、ポート8003)+ 毎時 review-antenna-poll.timer
  • Route53 に reviewantenna.ikapps.com A レコード作成 → nginx リバースプロキシ + certbot で SSL
  • 疎通確認: /health・ダッシュボード・/reviews API(401含む)・poll(20アプリ・エラー0)すべてOK
  • 旧 Cloudflare Worker + D1 を完全削除(REST API経由。workers.dev URL も404化を確認)
  • tsx を devDeps → deps へ移動、wrangler.toml・wrangler 依存・d1:export を撤去
  • README / 引き継ぎ.md / Lightsailサーバー側 README を新構成に更新

詰まったこと / 気づき:

  • サーバーは Node v22 → node:sqlite--experimental-sqlite フラグ必須(systemd で NODE_OPTIONS 指定)
  • systemd の EnvironmentFile は値中の \nn にエスケープ解釈し、PEM鍵・JSON鍵を破壊 (-----BEGIN...-----nMIGT... となり base64 不正)。→ 秘密情報は /etc/*.env をやめ、 WorkingDirectory の .dev.vars を dotenv で読む方式(env.ts のローカル経路と統一)に変更して解決。
  • wrangler CLI はこのサンドボックスでハング(TTY依存)→ Cloudflare REST API + OAuthトークンで削除実行。
  • Lightsail は 512MB RAM(空き約140MB)。常駐サーバーは ~100MB で収まり許容範囲。

次回やること: 一覧のフィルタ/並べ替え、Slack/ntfy通知、レビュー集計、Resend独自ドメイン認証。


2026-06-03

18:12 - アプリ登録/削除UI・アイコン表示・JSONダウンロード

やったこと:

  • 管理画面からアプリ登録・削除(無認証、削除は確認ダイアログ、登録後に即seed)
  • 登録時に名前・★評価総数・アイコンを同期取得(登録直後からIDでなく名前が出る)
  • アプリアイコンの自動取得・一覧表示(App Store=artworkUrl100、Google Play=og:image)
  • 全レビューのJSONダウンロード GET /export(AIが読みやすいapp情報+reviews配列、全件)
    • ファイル名 {アプリ名}_{AppStore|GooglePlay}_{yyyymmdd}.json(日本語はfilename* UTF-8)
  • ダッシュボードの見た目改善(ストアバッジを1行固定+色分け、削除ボタン等の折り返し解消)
  • 監視アプリを19個まで拡大

決めたこと:(DECISIONS.md にも転記)

  • 新規アプリの全件seedは1回のpollで最大3アプリまで分割(subrequest上限対策)
  • レビューのエクスポート形式はJSON

詰まったこと / 気づき:

  • 19アプリ一括登録で Cloudflare の subrequest 上限(1実行あたりの外部リクエスト数)超過 → 重いseedを1回最大3アプリに分割、軽い補完(名前/評価/アイコン)は全アプリ毎回。
  • 登録直後はseed未完了でアプリ名がIDのまま見える → 名前・アイコンを登録時に同期取得して解消。
  • システム時計が実時刻とズレている(dateコマンドが深夜を返すが実際は夕方)。 DEVLOGの時刻はユーザーに確認した実時刻を使う。前回の「03:19」も実際は夕方だったため修正。

次回やること: 一覧のフィルタ/並べ替え(ストア別・評価順)、Slack/ntfy通知、Resend独自ドメイン認証。

16:30 - レビュー取得API・管理ダッシュボード・★評価総数表示

やったこと:

  • レビュー取得API GET /reviews 追加(READ_TOKEN認証、app/limit/from/to でD1から取得)
  • App Storeレビュー取得をページング対応(links.next を辿る)
  • Google Play のアプリ名自動取得(Play Store ページの og:title)
  • 管理ダッシュボード(/ アプリ一覧、/app?id= レビュー詳細、無認証、ダークモード対応)
  • ★評価総数の取得・表示(App Store=iTunes lookup jp、Google Play=Play Store の JSON-LD ratingCount) → 一覧で「文章レビュー数 (★評価総数)」形式
  • 共通HTMLユーティリティを src/html.ts に切り出し(notify.ts と共有)
  • 監視対象に Study Timer(6444372570) を追加(文章レビュー463件)

決めたこと:(DECISIONS.md にも転記)

  • DB は D1 を継続(自分用のため。スケール時はテナント別D1→Postgres移行という段階方針)
  • ダッシュボードは無認証(レビューは元々ストアで公開情報のため)

気づき:

  • ASC customerReviews は文章付きレビューのみ返す。Vision Workoutは★評価4944だが文章レビューは 201件で、それは全件取得済み(取りこぼしではない)。「4900」は星だけ評価を含む総数。
  • 公式API(ASC/Play)は自分のApple Developer/Play Console権限内のアプリのみ取得可。他人のは403で不可。
  • ★評価総数は国別(iTunes lookup country=jp)。日本中心アプリなので jp で4944が一致。
  • Cloudflare secret 追加直後は伝播待ちで undefined になることがある→再デプロイで確実化。

次回やること: Slack/ntfy通知追加、レビュー集計・分析、Resend独自ドメイン認証、公開サービス化。


2026-06-02

18:40 - ゼロからレビュー監視サービスをMVPまで構築

実装ゼロの状態から、App Store / Google Play 両対応のレビュー監視・メール通知サービスを 本番稼働まで一気に構築した。

やったこと:

  • 技術スタック確定(Cloudflare Workers + D1 + Cron、メール=Resend)
  • API調査(App Store Connect / Google Play Developer API)
  • 最小構成の実装(src/index.ts の poll、asc.ts、notify.ts、D1スキーマ)
  • App Store Connect 実接続成功(JWT/ES256、200件取得・保存・冪等性)
  • 本番デプロイ(D1作成、secret設定、Worker deploy、Cron毎時0分)
  • メール通知(Resend)接続、実着信確認
  • 初回 seed 処理(既存レビューを通知抑止)、SQL変数上限対策
  • アプリ名の自動取得(iTunes lookup)、メールのplatform表示整形
  • /run に Bearer トークン認証(RUN_TOKEN)
  • Google Play 対応(src/gp.ts、サービスアカウントRS256→OAuth→reviews)、実着信確認
  • base64url等を src/jwt.ts に共通化

詰まったこと / 気づき:

  • D1 の SQL 変数上限は100。IN句に200件で too many SQL variables。 → INSERT OR IGNORE + meta.changes 集計に変更して解決。
  • Apple Developer Program メンバーシップ期限切れでも customerReviews は取得できた。
  • Google Play は直近7日のレビューのみ・タイトル無し。Play Console の権限付与は 「ユーザーと権限」でサービスアカウント招待する方式(旧 設定>APIアクセスは廃止)。
  • Cloudflare secret 追加直後は伝播が間に合わず undefined になることがある。再デプロイで確実化。

監視中: App Store「Vision Workout」(1563884276) / Google Play「勉強タイマー」(jp.thomsons.pomodoro)

次回やること: Resend独自ドメイン認証(任意宛先送信)、Slack/ntfy等チャネル追加、公開サービス化。

最近のコミット

README

review-antenna(Review Antenna)

概要

App Store・Google Play・Amazon 等のレビューを監視し、新着投稿をメール等で通知するサービス。

  • 当初は自分用、現在はマルチテナントの公開サービス(Google OAuth ログイン、ユーザーごとに分離)
  • 通知チャネルはメール(Resend)をはじめ、Slack・ntfy・LINE 等を後から追加できる設計
  • 公開URL: https://reviewantenna.ikapps.comログイン必須。将来 reviewantenna.com へ移行予定)
  • 各ユーザーは /settings で自分の App Store / Google Play API認証情報をアップロード(AES-256-GCM で暗号化保存)

技術スタック

Node.js (Hono) + node:sqlite + systemd。AWS Lightsail(multi-purpose-lightsail-server1)で常駐し、 systemd timer で毎時ポーリングする。SQLite で「どこまで読んだか」を保持する。

2026-06 に Cloudflare Workers + D1 + Cron Triggers からこの構成へ移行した。 D1 互換の最小ラッパー(src/db.ts)で既存ロジックを無改変のまま流用している。

  • 先行ターゲット: App Store Connect API(JWT/ES256 認証、全期間取得可)
  • Google Play Developer API(直近7日のみ取得可のためポーリング間隔に注意)
  • 通知メール送信: Resend

アーキテクチャ

systemd timer(毎時0分) ─▶ poll.ts ─▶ apps から監視対象を取得
                                   ─▶ ASC/Play API でレビュー取得(トークンは使い回し)
                                   ─▶ 新着のみ reviews に INSERT OR IGNORE(notified=0)
                                   ─▶ notified=0 を user 単位で集約 → channels(email) へ送信
                                   ─▶ 送信成功で notified=1

systemd service(常駐) ─▶ server.ts (Hono) ─▶ ダッシュボード / 登録・削除 / API / 手動 /run
                                              nginx(443, Let's Encrypt) でリバースプロキシ
ファイル 役割
src/server.ts 常駐 HTTP サーバー(Hono)。ダッシュボード・API・登録/削除・認証・設定・/run/health
src/auth.ts Google OAuth2 + セッション(sessions)+ email突き合わせによるユーザーupsert
src/credentials.ts テナント別の認証情報 get/save/status+接続テスト(me は env フォールバック)
src/crypto.ts 認証情報の AES-256-GCM 暗号化/復号(鍵は APP_ENC_KEY から導出)
src/poll.ts ポーリング本体。systemd timer から毎時実行。ユーザー単位に各自の認証情報で取得
src/db.ts node:sqlite による D1 互換ラッパー(prepare/bind/all/first/run/batch)
src/env.ts 環境変数の読込(.dev.vars を dotenv でロード。複数行PEM対応)
src/asc.ts App Store Connect クライアント(JWT生成 + レビュー取得 + アプリ情報)
src/gp.ts Google Play クライアント(サービスアカウントRS256→OAuth→reviews)
src/notify.ts 通知送信(type 分岐。現状 email/Resend のみ)
src/dashboard.ts ダッシュボード(一覧/詳細)・JSONエクスポートのHTML生成
src/jwt.ts base64url / pemToDer の共通ユーティリティ
src/types.ts Env と共有型(DB互換インターフェース含む)
migrations/ apps / reviews / channels スキーマ(0001〜0004)
deploy/ systemd unit(service/timer)・nginx 設定

ローカル開発

npm install

# シークレットを用意(複数行PEMもそのまま貼れる)
cp .dev.vars.example .dev.vars   # 中身を埋める

# スキーマ適用(初回のみ。DB ファイルは ./review-antenna.db)
cat migrations/*.sql | sqlite3 review-antenna.db

npm run server   # 常駐サーバー起動(http://localhost:8003)
npm run poll     # 単発ポーリング
npm run typecheck

Node 24 未満(本番サーバーは v22)では node:sqlite--experimental-sqlite が必要。 systemd unit では NODE_OPTIONS=--experimental-sqlite を設定している(ローカル v24+ では不要)。

監視対象・通知先の登録

アプリは管理ダッシュボードの登録フォームから追加できる。SQL で直接入れる場合:

-- 監視するアプリ(external_id は App Store の数値 appId / Google Play のパッケージ名)
INSERT INTO apps (user_id, platform, external_id, name, created_at)
VALUES ('me', 'app_store', '1234567890', 'My App', unixepoch()*1000);

-- 通知先(config は JSON)
INSERT INTO channels (user_id, type, config, created_at)
VALUES ('me', 'email', '{"to":"you@example.com"}', unixepoch()*1000);

本番(Lightsail)

項目
配置先 /home/ubuntu/apps/review-antenna/(rsync デプロイ)
シークレット /home/ubuntu/apps/review-antenna/.dev.vars(chmod 600、git管理外)
DB /var/lib/review-antenna/review-antenna.db
ポート 8003(nginx → 127.0.0.1:8003)
常駐 review-antenna.service(systemd、Restart=always)
毎時poll review-antenna-poll.timerreview-antenna-poll.service0 * * * * 相当)
seedキャッチアップ review-antenna-seed.timerreview-antenna-seed.service(5分間隔・未seedのみ取り込み。登録直後seedの安全網)
ドメイン https://reviewantenna.ikapps.com (Let's Encrypt SSL)

デプロイ手順・unit の実体は deploy/ を参照。再デプロイは概ね次の通り:

# ローカルから(node_modules/.git/*.db/.dev.vars は除外)
rsync -az --delete --exclude node_modules --exclude .git --exclude '*.db*' \
  --exclude .dev.vars --exclude .wrangler --exclude .devnotes \
  -e "ssh -i ~/.ssh/lightsail-multipurpose" ./ ubuntu@13.230.63.19:/home/ubuntu/apps/review-antenna/

ssh -i ~/.ssh/lightsail-multipurpose ubuntu@13.230.63.19 \
  'cd /home/ubuntu/apps/review-antenna && npm ci --omit=dev && sudo systemctl restart review-antenna'

手動ポーリング: ssh ... 'sudo systemctl start review-antenna-poll.service' ログ: ssh ... 'journalctl -u review-antenna.service -f'