資産管理
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
ポートフォリオの推移と構成を、美しく直感的に管理する資産管理トラッカーです。Next.js (App Router)、Prisma、Tailwind CSS を使用して構築されています。 • 🔐 認証・セキュリティ (Supabase Auth) • Google OAuth ログイン対応 VPS 上の dev DB は使いません。ローカル開発は WSL 内の MySQL のみで完結します。 • Node.js 20 系 • WSL 内の MySQL 8.0(sudo apt install mysql-server) Docker Desktop は不要です。WSL 連携が使える環境では npm run db:up:docker で Docker 版も利用できます。 1Password CLI は日常のローカル開発では不要です。本番デプロイの確認や本番 DB への接続(本番 DB のローカル接続)でのみ使用します。インストール手順はデプロイ方法を参照してください。 .env.local の Signaly 通知欄は空のままでも起動できます(未設定なら通知をスキップします)。ただし Google OAuth はログインに必須のため、Google ログイン(ローカル開発) の手順で設定してください。檢視完整 README ↗
ポートフォリオの推移と構成を、美しく直感的に管理する資産管理トラッカーです。Next.js (App Router)、Prisma、Tailwind CSS を使用して構築されています。
📊 ダッシュボード
💰 資産・カテゴリ管理
🔄 評価額の自動取得
🧾 家計簿連携(Zaim)
🔐 認証・セキュリティ (Supabase Auth)
🎨 カスタマイズ可能なUI
| 環境 | DB | 用途 |
|---|---|---|
| ローカル(WSL) | 127.0.0.1:3306 / asset_manager_dev | 日常開発 |
| 本番(VPS) | 1Password の db-host / db-name | 本番運用 |
| 本番(ローカル接続) | SSH トンネル 127.0.0.1:3307 | デバッグ用(prod:tunnel) |
VPS 上の dev DB は使いません。ローカル開発は WSL 内の MySQL のみで完結します。
sudo apt install mysql-server)Docker Desktop は不要です。WSL 連携が使える環境では
npm run db:up:dockerで Docker 版も利用できます。1Password CLI は日常のローカル開発では不要です。本番デプロイの確認や本番 DB への接続(本番 DB のローカル接続)でのみ使用します。インストール手順はデプロイ方法を参照してください。
# パッケージのインストール
npm install
# ローカル開発用の環境変数ファイルを作成(1Password 不要)
cp .env.local.example .env.local
# MySQL が未インストールの場合(初回のみ)
sudo apt update && sudo apt install -y mysql-server
# ローカル MySQL 起動 + スキーマ適用(初回のみ、sudo パスワード入力あり)
npm run db:setup
# 開発サーバーの起動
npm run dev
.env.local の Discord 通知欄(DISCORD_ALERT_WEBHOOK_URL)は空のままでも起動できます(未設定なら通知をスキップします)。ただし Google OAuth はログインに必須のため、Google ログイン(ローカル開発) の手順で設定してください。
日常の開発では npm run db:up で MySQL を起動してから npm run dev を実行します(db:setup は初回または DB を作り直したときのみ)。
npm run db:up / npm run db:setup は内部で sudo service mysql start 等を実行するため、通常は sudo パスワードの入力を求められます。Claude Code のような非対話環境ではパスワード入力ができず失敗するので、事前に一度だけ以下を対話シェルで実行し、MySQL 起動系コマンドのみパスワードなしで sudo できるようにしておきます。
npm run db:setup:sudoers
これは /etc/sudoers.d/mysql-dev-nopasswd に service mysql start/stop と mysqladmin ping / mysql(DB・ユーザー作成用)に限定した NOPASSWD ルールを追加します(他のコマンドの sudo は従来どおりパスワードが必要です)。設定後は非対話環境からでも npm run db:up / npm run db:setup が動作します。
| コマンド | 1Password | 内容 |
|---|---|---|
npm run db:up | 不要 | WSL MySQL を起動(127.0.0.1:3306) |
npm run db:up:docker | 不要 | Docker で MySQL 8.0 を起動(127.0.0.1:3308) |
npm run db:down | 不要 | ローカル MySQL を停止 |
npm run db:status | 不要 | MySQL の状態確認 |
npm run db:setup | 不要 | 起動 + prisma db push(初回用) |
npm run db:deploy:local | 不要 | schema.prisma をローカル DB に反映 |
npm run db:reset | 不要 | DB を初期化してスキーマを再適用 |
npm run db:dev | 不要 | マイグレーションファイルの作成(スキーマ変更時) |
npm run dev | 不要 | 開発サーバー起動(.env.local を使用) |
ローカル DB の接続情報(.env.local.example に記載、秘密情報ではない):
| 項目 | 値 |
|---|---|
| ホスト | 127.0.0.1 |
| ポート | 3306 |
| DB 名 | asset_manager_dev |
| ユーザー | asset_manager |
| パスワード | devpassword |
ローカル開発では prisma db push(npm run db:setup / npm run db:deploy:local)で素早くスキーマを反映します。本番デプロイは deploy.yml 内で prisma migrate deploy を実行し、prisma/migrations のマイグレーション履歴を適用します。
スキーマを変更した場合は npm run db:dev でマイグレーションファイルを生成し、prisma/migrations にコミットしてください。
# スキーマを変更したあと
npm run db:deploy:local
npm run dev # 再起動
[!IMPORTANT]
db pushだけで済ませると、本番にだけテーブルが作られません。 ローカルもCIもprisma db pushでschema.prismaを直接反映するため、マイグレーションを生成し忘れても どこも失敗しません。一方で本番はprisma migrate deployしか実行しないため、履歴に無い テーブルは本番DBに一度も作られず、画面を開いた時点でThe table X does not exist in the current databaseになります (レシートAI取込の6テーブルで実際に発生: #236)。
CI(.github/workflows/test.yml の Migration drift check)が毎回この確認を行うため、生成漏れは
Pull Requestの時点で落ちます。手元で確かめたい場合は、シャドウDBを用意して同じコマンドを
実行します(差分があれば終了コード 2)。
sudo mysql -e "CREATE DATABASE IF NOT EXISTS asset_manager_shadow;
GRANT ALL PRIVILEGES ON asset_manager_shadow.* TO 'asset_manager'@'localhost';"
npx prisma migrate diff \
--from-migrations prisma/migrations \
--to-schema-datamodel prisma/schema.prisma \
--shadow-database-url 'mysql://asset_manager:[email protected]:3306/asset_manager_shadow' \
--exit-code
シャドウDBを用意できない環境では、最後にマイグレーションを追加したコミット時点の
schema.prisma を起点にすれば、DBなしで同じ差分を出せます(生成漏れを後から埋めるときの手順)。
git show <そのコミット>:prisma/schema.prisma > /tmp/old-schema.prisma
DIR="prisma/migrations/$(date -u +%Y%m%d%H%M%S)_<名前>"
mkdir -p "$DIR"
npx prisma migrate diff \
--from-schema-datamodel /tmp/old-schema.prisma \
--to-schema-datamodel prisma/schema.prisma \
--script > "$DIR/migration.sql"
ログインは Supabase Auth 経由の Google OAuth のみです。ローカル開発でも設定が必要です。初回ログイン時にダミーデータが自動投入されます。
複数アプリで共有の開発用 Supabase プロジェクトを使います(本番用プロジェクトとは別。詳細は m-guchi/vps#42 参照)。
必要な設定:
http://localhost:3000/auth/callback
.env.local の NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY に、開発用 Supabase プロジェクトの値を記入npm run dev で http://localhost:3000/login を開くよくあるエラー:
| 症状 | 対処 |
|---|---|
redirect_uri_mismatch | Supabase プロジェクトの Redirect URLs に上記 URL を追加 |
| Google ログインがエラーになる | .env.local の NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY が空でないか確認 |
| ログイン画面も含め全ページが500になる | 同上。空だと middleware.ts がSupabaseクライアントを作れず、ページに入る前に落ちる(ログには Your project's URL and Key are required to create a Supabase client!)。画面の中身とは無関係なので、UIの変更を疑う前にここを見る |
npm run dev 起動時に、以下のアクセス経路がコンソールに表示されます。
http://<LAN-IP>.sslip.io:3000(同じ Wi-Fi 上のスマホ等から)https://asset-dev.minagu.work(Cloudflare Tunnel + Cloudflare Access 経由。自分の Google アカウントのみ許可)いずれも next.config.mjs の allowedDevOrigins(*.sslip.io / *.minagu.work)でクロスオリジンリクエストを許可しています。
LAN 内からのアクセス(Windows + WSL2 の場合)
WSL2 の IP は再起動のたびに変わるため、WSL 再起動後は管理者権限の PowerShell で以下を再実行して portproxy を張り直します。
scripts\windows\expose-dev-server.ps1
開発用 Supabase プロジェクトの Redirect URLs にも http://<LAN-IP>.sslip.io:3000/auth/callback の追加が必要です(Google ログイン(ローカル開発)参照)。
外出先からのアクセス(Cloudflare Tunnel)
cloudflared tunnel run dev-tunnel を起動しておくと、npm run dev 実行時に表示される https://asset-dev.minagu.work からアクセスできます(Cloudflare 側のトンネル・Access 設定はこのリポジトリの管理外です)。
本番データの確認が必要な場合のみ、SSH トンネル経由で接続します。
npm run prod:tunnel
スマホ等 LAN/Cloudflare Tunnel(https://asset-dev.minagu.work)経由でこのモードにアクセスする場合は、本番用 Supabase プロジェクトの Redirect URLs に https://asset-dev.minagu.work/auth/callback を追加しておく必要があります(Supabase ダッシュボード側の設定のためこのリポジトリの管理外)。未追加のままだと redirect_uri_mismatch でログインできません。
ローカル開発の秘密情報(Supabase 等)はすべて .env.local に平文で保存します(.gitignore 済みのためコミットされません)。1Password は本番デプロイと本番 DB 確認にのみ使用します。
| 用途 | テンプレート | コマンド | 1Password |
|---|---|---|---|
| ローカル開発 | .env.local(.env.local.example からコピー) | npm run dev | 不要 |
| 本番 DB(ローカル接続) | .env.1password.prod.tpl | npm run prod:tunnel | 必要(DB 認証情報 + 本番用 Supabase) |
| GitHub Actions デプロイ | GitHub の secret / variable(.github/secrets-manifest.tsv) | main への push で自動実行 | 実行時は不要(値を変えたときの同期のみ) |
Supabase プロジェクトはローカル開発用(.env.local に平文で保存)と本番用(1Password の apps/Supabase アイテム、他アプリと共有)で別のものを使い分けます。
ログイン後の画面確認だけなら、Supabase の値は不要です。npm run devで起動し、http://localhost:3000/loginの「開発用ダミーユーザーでログイン」を押してください。開発サーバーが起動ごとに専用シークレットを生成し、固定のダミーユーザーをローカルDBへ作成します。この導線はNODE_ENV=productionでは常に無効です。詳しい仕組みと他リポジトリの調査結果はSupabaseに依存しないローカル画面確認を参照してください。
1Password を使う場合(本番 DB 接続・GitHub 側への同期):
eval "$(op signin)" # または export OP_SERVICE_ACCOUNT_TOKEN=...
npm run verify:op # 本番 DB 用テンプレートの op:// 参照を確認
デプロイで使う値の同期は「2. シークレットの配布」を参照してください。
本番環境にデプロイしてビルドエラーを起こさないために、Push前に以下の品質チェックを実行してエラーが出ないか確認することを推奨します。
lint・型チェック・本番ビルドを順番に実行します(build:local は .env.local を使用、1Password 不要)。
npm run check
必要に応じて、以下のコマンドを個別に実行することもできます。
# 1. Linter(静的コード解析)による構文や未使用変数のチェック
npm run lint
# 2. TypeScriptの型チェック(型エラーの検知)
npm run typecheck
# 3. ローカルでの本番ビルドテスト(ローカル DB 接続情報を使用)
npm run build:local
すべてのコマンドがエラーなく(✓ Compiled successfully など)完了すれば、デプロイやPushの準備は完了です。
本アプリケーションは、main ブランチへの Push 操作をトリガーとして、VPS等へ自動デプロイされるよう GitHub Actions ワークフロー (.github/workflows/deploy.yml) が構成されています。
npm install -g pm2)ワークフローは実行時に GitHub の secret / variable から値を取ります(op:// の実行時参照は行いません)。
以前は実行のたびに 1Password から読んでいましたが、サービスアカウントの日次レート制限(1Password アカウント全体で 1,000 リクエスト/日。サービスアカウントを分けても分割されません)を使い切り、フリート全体のデプロイが止まったためです(issue-deck#1302)。
1Password は「人が管理する唯一の正」として残し、値を変えたときだけ scripts/sync-github-secrets.sh で GitHub 側へ同期します。どの値を GitHub 側のどこへ置くかの対応表が .github/secrets-manifest.tsv です。
| 置き場所 | 対象 | 備考 |
|---|---|---|
| organization の共通値 | SERVER_HOST・SERVER_USERNAME・SERVER_SSH_PORT(SSH 接続先)・SHARED_DB_*(共有 MariaDB)・SUPABASE_* | 他アプリと共有。このリポジトリでの設定は不要 |
| このリポジトリの secret | SSH_PRIVATE_KEY(このアプリ専用のデプロイ鍵)・DB_NAME・DISCORD_ALERT_WEBHOOK_URL | ログでマスクされる。TARGET_DIR は gate 移行(#754)で使わなくなった(撤去は移行を確かめてから) |
| このリポジトリの variable | NEXT_PUBLIC_GA_ID | クライアントバンドルに埋め込まれる公開値 |
ワークフローのジョブに書く env: ブロックは、対応表から scripts/generate-workflow-env-block.sh で生成できます。
値の正は 1Password です。保管庫名 apps に、次のアイテムを作成してください(GitHub 側へは 2-2 の同期で反映します)。
アイテム AssetManager(セキュアノート等)
| フィールド名 | 内容 | 環境変数 |
|---|---|---|
db-name | 本番用データベース名 | DB_NAME(デプロイ時に DATABASE_URL を組み立て) |
ga-id | Google Analytics 測定 ID | NEXT_PUBLIC_GA_ID |
ci-webhook-url | CI/デプロイ結果を通知する Signaly の Webhook URL | SIGNALY_WEBHOOK_URL |
target-dir | デプロイ先ディレクトリ(gate 移行(#754)で deploy.yml からは使わなくなった。置き場は gate が /home/github-user/apps/asset-manager に固定する。diag-cron.yml がまだ参照する) | 例: /home/github-user/apps/asset-manager |
異常時のDiscord通知の Webhook URL(DISCORD_ALERT_WEBHOOK_URL)は全リポジトリ共通の 1 本で、このアイテムではなく op://apps/Notify/alert-webhook-url を正とします(#767)。送るのは対応が要る異常(Zaim自動取得・積立の自動登録の失敗)だけで、成功・通常ログインは通知しません。Signalyへの送信は CI/デプロイ通知(SIGNALY_WEBHOOK_URL)だけが残っています。
アイテム Supabase(複数アプリ共有、m-guchi/vps#42 参照)
| フィールド名 | 内容 | 環境変数 |
|---|---|---|
project-url | 本番用 Supabase プロジェクトの URL | NEXT_PUBLIC_SUPABASE_URL |
publishable-key | 本番用 Supabase の Publishable key | NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
アイテム DB(MyRoom と共有可)
| フィールド名 | 内容 | 環境変数 |
|---|---|---|
db-user | MySQL ユーザー名 | DB_USER |
db-password | MySQL パスワード | DB_PASSWORD |
db-host | 本番用 MySQL ホスト | DB_HOST(デプロイ時) |
db-port | 本番用 MySQL ポート | DB_PORT(デプロイ時) |
アイテム Server(MyRoom と共有可)
| フィールド名 | 内容 |
|---|---|
host | サーバーのホスト名または IP |
username | SSH ユーザー名 |
ssh-port | SSH ポート番号 |
アイテム asset-manager(デプロイ専用の SSH 鍵。#754)
| フィールド | 内容 | GitHub 側 |
|---|---|---|
deploy-ssh-key | このアプリ専用のデプロイ鍵(秘密鍵)。VPS の authorized_keys で app-deploy-gate asset-manager に結び付けられ、upload・env・deploy しか実行できない | repo secret SSH_PRIVATE_KEY |
鍵の作成と VPS への登録は guchi-apps/vps の scripts/setup-app-keys.sh add --app asset-manager --pubkey <公開鍵> で行います。
アイテム githubaction-sshkey(「SSH 鍵」アイテム型・MyRoom と共有可。organization 共通の SERVER_SSH_PRIVATE_KEY の正。デプロイには使わず、diag-cron.yml だけが使う)
| フィールド ID | 内容 |
|---|---|
private_key | サーバー接続用 SSH 秘密鍵 |
Vault 名やアイテム名を変える場合は、.github/secrets-manifest.tsv と .env.1password.prod.tpl の op://... 参照も合わせて更新してください。
参照の確認:
op item get AssetManager --vault apps --format json | jq '.fields[] | {id, label}'
op read "op://apps/githubaction-sshkey/private_key?ssh-format=openssh"
⚠️ エラー 400: redirect_uri_mismatch が発生する場合 Supabase プロジェクトの Authentication > URL Configuration「Redirect URLs」に以下の URL を追加してください。
- 本番環境:
https://asset.gucchii.com/auth/callback- ローカル環境:
http://localhost:3000/auth/callback
1Password の値を変えたときだけ実行します。個人アカウントのセッションで実行してください(サービスアカウントには書き込み権限が無く、日次レート制限の枠も消費しません)。
eval "$(op signin)"
scripts/sync-github-secrets.sh --dry-run # 差分だけ確認
scripts/sync-github-secrets.sh
scripts/sync-github-secrets.sh --only SIGNALY_WEBHOOK_URL # 一部だけ
GitHub の Actions タブ(および issue-deck の画面)からは Sync secrets ワークフロー(.github/workflows/sync-secrets.yml)で同じ同期を起こせます。
OP_SERVICE_ACCOUNT_TOKEN(1Password Service Account のトークン。apps 保管庫への読み取り権限が必要)は GitHub Secret に残していますが、デプロイ・CI では使いません。Sync secrets ワークフローのように 1Password を読む処理だけが使います。
.envデプロイ時に GitHub の secret から渡した DB_* を scripts/construct-database-url.sh で DATABASE_URL に組み立て、VPS の gate(app-deploy-gate)へ標準入力の KEY=VALUE(1行1変数)で送ります。gate がサーバー .env に同期します(既存の同名キーは上書き、それ以外は保持)。改行を含む値は送れないため、ワークフローは送る前に止めます。MIGRATE_DATABASE_URL(マイグレーション専用ユーザー。未設定なら DATABASE_URL)は .env へ書かれず、マイグレーションだけに使われます。
| 環境変数 | GitHub 側の取得元 | 1Password(正) |
|---|---|---|
DATABASE_URL | SHARED_DB_*(org secret)+ DB_NAME(repo secret) | DB の db-* + AssetManager の db-name から自動生成 |
NEXT_PUBLIC_SUPABASE_URL | SUPABASE_PROJECT_URL(org variable) | Supabase の project-url |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | SUPABASE_PUBLISHABLE_KEY(org variable) | Supabase の publishable-key |
NEXT_PUBLIC_GA_ID | NEXT_PUBLIC_GA_ID(repo variable) | AssetManager の ga-id |
DISCORD_ALERT_WEBHOOK_URL | DISCORD_ALERT_WEBHOOK_URL(repo secret) | Notify の alert-webhook-url(全リポジトリ共通) |
設定が完了したら、main ブランチへ変更を Push するか、GitHub の Actions タブから Deploy to VPS ワークフローを手動で実行 (workflow_dispatch) してください。
デプロイは VPS の gate(/usr/local/sbin/app-deploy-gate。guchi-apps/vps#275)が行います(#754)。このアプリ専用の鍵(SSH_PRIVATE_KEY)は gate に結び付けられており、upload・env・deploy の3語しか実行できません。置き場は /home/github-user/apps/asset-manager に固定です。
env: に読み込み、GitHub 側でビルド (npm run build) およびアーカイブの作成が行われます。ssh … upload < deploy.tar.gz で配布物を置きます。ssh … env で .env へ書く値(APP_COMMIT_SHA・PORT=3102 を含む)を送ります。ssh … deploy で gate が、前回の配布物の削除 → 展開 → .env 更新 → npm ci --omit=dev → prisma migrate deploy → pm2 delete ecosystem.config.js・pm2 delete asset-manager → pm2 start ecosystem.config.js --env production → pm2 save → /api/app-version の sha が今回のコミットと一致するまでの確認(最大60秒)を行います。
@prisma/client の peerDependency として本番用の依存にも入り、その postinstall が Prisma Client を生成します(従来の npx prisma generate は不要)。scripts/test-db-connection.ts)は gate では実行しません。DB に届かないとアプリの起動確認より先に画面で気付くことになるため、デプロイ後に本番の画面を一度開いて確かめてください。
ecosystem.config.jsからプロセスを削除しただけでは本番に残る。 デプロイは gate のpm2 delete ecosystem.config.js(新しい ecosystem が宣言するプロセスだけ)・pm2 delete asset-manager→pm2 start ecosystem.config.js→pm2 saveという流れのため、定義から消したcronプロセスはVPSのPM2に登録されたまま残ります。実体の スクリプトはデプロイ時に入れ替わって消えるので、起動するたびに失敗し続ける状態になります。 cronプロセスを廃止するときは、VPS上でpm2 delete <プロセス名> && pm2 saveを手作業で 実施してください(#191)。