Biz Admin Leads Dashboard — CRM形式の/admin ページ(Supabase版)
Next.jsプロジェクト(App Router)に1CRM風の/adminページをスキャフォールディングします。3つのタブ — 概要(KPI + グラフ)、顧客(テーブル + フィルター)、メールマーケティング(作成 + 一括送信 + 履歴)。
理念: 1CRMよりもシンプル(共通パスワード1つ、マルチユーザーなし)ですが、1〜2人で運営するビジネスオーナーが使えるよう、十分なグラフと変化率、メールマーケティング機能を備えています。すべての状態はSupabaseに保存され、スキャフォールディングは既存のプロジェクトから独立しています。
/admin → ポップアップ「管理コードを入力」→ 各タブは /api/admin/{leads|dashboard|campaigns} を呼び出す
→ 各リクエストは x-admin-pass ヘッダーを送信
→ サーバー側は ADMIN_PASSWORD env と timing-safe 比較
パスワードはReactのstateに保存 — ページをリフレッシュするとポップアップが再表示(意図的、シンプル、セッションCookie不要)。
Supabaseのみ — KVバリアントは削除されました。理由: campaigns + campaign_sends にはSQL結合とservice_roleによるRLSバイパスが必要であり、KVはネイティブでサポートしていません。
このスキルを使用する場面
- プロジェクトにSepay支払いとリードストアがすでにあり(
biz-setup-sepay-paymentからlib/leads-supabase.tsを使用)、表示レイヤー、ダッシュボード、メールマーケティングが必要な場合。 - ユーザーがCRMスタイルのページを希望している場合:変化率を示すKPIカード、日次折れ線グラフ、コンバージョンのドーナツグラフ、登録済み顧客へのプロモーションメール送信。
使用すべきでない場合:
- プロジェクトに
lib/leads-supabase.tsがない場合 → ユーザーに/biz-setup-sepay-payment(Supabaseを選択)を先に実行するよう指示してください。 - プロジェクトがVercel KVをSupabaseの代わりに使用している場合 → このスキルはKVをサポートしなくなりました。ユーザーはSupabaseに移行するか、手動で移植するかの2択です。
- ロール/権限を持つマルチ管理者が必要な場合 → このスキルは共通パスワード1つであり、適しません。
- 詳細なメールトラッキング(受信者ごとの開封/クリック)が必要な場合 → このスキルは送信成功/失敗のみを追跡します。ユーザーはトラッキング機能を持つResend/SendGrid/Brevoの一括送信を検討してください。
ワークフロー(6フェーズ)
Phase 0: Next.js App Routerの確認 + lib/leads-supabase.tsの確認 + SMTP環境変数の確認
Phase 1: ユーザーにパスワードを尋ねる(デフォルト 123456)— ゲート
Phase 2: npm依存関係 `recharts` をインストール
Phase 3: Supabase SQLエディタで supabase-migration-admin.sql を実行 — ゲート
Phase 4: ADMIN_PASSWORD を .env.local に書き込む
Phase 5: 9ファイルのコードをスキャフォールディング(5 lib + 4 route + 1 page)
Phase 6: 3段階のテスト(curl dashboard、UI スモークテスト、テストキャンペーン送信)
Phase 0 — 依存関係の確認
# Next.js + App Router の確認
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "App Router: ✓" || { echo "✗ App Routerが必要です。このスキルはPages Routerをサポートしません。"; exit 1; }
# Supabaseリードストアの確認
test -f lib/leads-supabase.ts && echo "Supabase store: ✓" || { echo "✗ lib/leads-supabase.ts がありません — 先に /biz-setup-sepay-payment を実行してSupabaseを選択してください"; exit 1; }
test -f lib/leads-store.ts && echo "プロバイダ非依存ラッパー: ✓"
test -f lib/supabase-admin.ts && echo "サービスロールクライアント: ✓"
# SMTP環境変数の確認(biz-email-setupより)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST: ✓" || { echo "✗ SMTP環境変数がありません — 先に /biz-email-setup を実行してください"; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# パッケージの確認
grep '"nodemailer"' package.json && echo "nodemailer: ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase: ✓"
lib/leads-supabase.ts がない場合:
⚠ プロジェクトにSupabaseリードストアがないようです。先に
/biz-setup-sepay-payment(Supabaseバックエンドを選択)を実行して、支払い処理とleadsテーブル、RLSを設定してください。その後、再度実行すると管理ダッシュボードをスキャフォールディングします。
SMTP環境変数がない場合:
⚠ SMTPが設定されていないようです。メールマーケティングタブでは送信にSMTPが必要です。先に
/biz-email-setupを実行してプロバイダ(Gmail/Resend SMTP/Brevo/SendGrid/Zoho...)を選択してください。その後、再度実行すると管理画面をスキャフォールディングします。メールタブなしで続行することも可能です — 確認していただけますか?
ユーザー向け要約:
以下の環境を確認しました:Next.js App Router ✓, Supabaseリードストア ✓, SMTP設定 ✓。次の手順を実行します:
- /admin 用のパスワードを確認
rechartsをインストール(折れ線グラフとドーナツグラフ用)- Supabaseで
campaigns+campaign_sendsテーブルを作成するSQLマイグレーションを実行ADMIN_PASSWORDを .env.local に追記- 9ファイルをスキャフォールディング(5 lib + 4 route + 1 page)
- テスト手順を案内
Phase 1に進んでもよろしいですか?
Phase 1 — パスワードの確認(ゲート)
以下のように尋ねてください:
/admin ページに使用するパスワードを入力してください:
- デフォルトとして以下を提案:
123456- または、本番環境用の強力なパスワードを入力(12文字以上、英字+数字+特殊文字を含む)
パスワードを入力してください(または "123456" と入力してデフォルトを使用):
ユーザーの応答を待つ。ユーザーが貼り付けたパスワードを取得。空欄の場合は 123456 を使用。
ユーザーがパスワードを確認するまでPhase 2に進まないでください。
Phase 2 — recharts のインストール
# パッケージマネージャーの検出
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # または: pnpm add recharts / yarn add recharts
nodemailer は /biz-email-setup で既にインストールされています。再インストールしないでください。
Phase 3 — SQLマイグレーションの実行(ゲート)
ユーザーに指示します:
Supabaseダッシュボード → SQLエディタ → 新しいクエリ → 以下のファイルの内容をすべて貼り付け → 実行 をクリック:
[ここに templates/supabase-migration-admin.sql を貼り付け]このSQLは2つの新しいテーブルを作成します:
campaigns(キャンペーンヘッダー)+campaign_sends(受信者ごとのログ)。既存のleadsテーブルには影響しません。実行後、SQLの最後の部分で確認してください —
campaigns: 0とcampaign_sends: 0が表示されるはずです。
ユーザーが「SQLの実行が完了しました」と確認するまで待ち、Phase 4に進みます。
Phase 4 — .env.local への書き込み
.env.local に追記します:
# Admin /admin page password
ADMIN_PASSWORD=<phase-1でユーザーが入力したパスワード>
もし ADMIN_PASSWORD がすでに存在する場合 → 上書きするかユーザーに確認(デフォルトは上書きしない)。
.env.local が .gitignore に含まれていることを確認します:
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
Phase 5 — 9ファイルのスキャフォールディング
lib/admin-auth.ts ← templates/lib-admin-auth.ts
lib/admin-stats.ts ← templates/lib-admin-stats.ts
lib/campaigns.ts ← templates/lib-campaigns.ts
lib/email-render.ts ← templates/lib-email-render.ts
lib/mailer.ts ← templates/lib-mailer.ts
app/admin/page.tsx ← templates/app-admin-page.tsx
app/api/admin/leads/route.ts ← templates/app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← templates/app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← templates/app-api-admin-campaigns-route.ts
メールレンダリングモジュール: lib/email-render.ts はピュアなモジュール(nodemailerをインポートしない) — textToHtml / wrapEmailHtml / personalize / renderCampaignEmail を含みます。lib/mailer.ts はこれらの関数を再エクスポートし、SMTPトランスポートを追加します。このように分離することで、app/admin/page.tsx(クライアントコンポーネント)がプレビューボタン用のレンダリングヘルパーをインポートする際に、nodemailerがクライアントバンドルに含まれるのを防ぎます。
biz-email-setup から lib/mailer.ts がすでに存在する場合の注意: 上書きしないでください — このスキルのメーラーは lib/admin-mailer.ts としてスキャフォールディングし、app/api/admin/campaigns/route.ts のインポート先を @/lib/admin-mailer に変更します。lib/email-render.ts は常に安全です(一意の名前で、競合しません)。
インポートエイリアスの注意: すべてのテンプレートは @/lib/... を使用します。プロジェクトの tsconfig.json に paths "@/*": ["./*"] があることを確認してください(Next.jsのデフォルト)。ない場合は、相対インポートに変更します。
lib/leads-store.ts の注意: leads APIルートは @/lib/leads-store から listLeads をインポートします(biz-setup-sepay-payment からのプロバイダ非依存ラッパー)。ユーザーが以前KVを設定してリネームした場合 → ラッパーがSupabaseを指していることを確認する必要があります。
データフロー図
ブラウザ (admin/page.tsx)
│ fetch (ヘッダー x-admin-pass)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → leads テーブル → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → leads テーブル → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → campaigns テーブル → { campaigns: [...] }
└──► POST /api/admin/campaigns (本文に bodyFormat: 'text' | 'html' を含む)
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (1回レンダリング)
└─► lib/campaigns.createCampaign (campaigns 行)
└─► ループ (200ms スロットル):
lib/mailer.personalize(html/text を受信者ごとに)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (campaign_sends 行)
└─► lib/campaigns.setCampaignStatus (最終カウント + sentAt)
return { successCount, failCount }
クライアントプレビュー(サーバーを呼び出さない): admin/page.tsx が lib/email-render を直接インポートし、
「プレビュー」ボタンで { html } + personalize(テンプレート) をレンダリング → <iframe srcDoc sandbox> 内に表示。
Phase 6 — 3段階のテスト
テスト 1 — Curl を用いたダッシュボードエンドポイントのテスト
PASS="<phase-1のパスワード>"
# 間違ったパスワード → 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# 期待: 401, {"error":"invalid_password"}
# 正しいパスワード → 200 と KPI + 時系列データ
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# 期待: period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
テスト 2 — ブラウザでのUIスモークテスト
1. pnpm dev(または yarn/npm)
2. http://localhost:3000/admin を開く
3. ポップアップ「管理ページ」が表示される → 間違ったパスワードを入力 → 「コードが違います。もう一度お試しください」
4. 正しいパスワードを入力 → ダッシュボードに遷移
5. 概要タブ: 4つのKPIカード、折れ線グラフ、ドーナツグラフ、期間選択ボタン(7日/30日/90日)が動作することを確認
6. 顧客タブ: テーブル、フィルター、CSVエクスポートが表示されることを確認
7. メールマーケティングタブ: 作成フォームと履歴(最初は空)が表示されることを確認
8. メールマーケティングタブ: 内容を入力 → 「👁 プレビュー」ボタンをクリック → モーダルにメールレンダリングが表示される
(「テキスト」↔「HTML」の切り替えで両方のモードを確認)
テスト 3 — オーナー宛のテストキャンペーン送信
重要: 設定したパスワードで、
last_days= 7 のオーディエンスを選択し、本文に{{name}}を含めてください。実際の一括送信前に、DB内の唯一のリード(オーナーのテストリード)にテスト送信してフローを確認します。
1. メールマーケティングタブを開く
2. キャンペーン名: 「テスト送信」
3. 件名: 「/adminからのテストメール — 受信できましたか?」
4. オーディエンス: 「直近の日数」→ 7日
5. 本文:
{{name}} 様
これは管理画面からのテストメールです。SMTPが正常に動作していることを確認するためのものです。
受信できましたら、実際のキャンペーン送信を開始します。
6. 「👁 プレビュー」ボタンをクリック → メールが正しく表示されることを確認 → モーダルを閉じる
7. 「今すぐ送信」ボタンをクリック → 確認
8. 10〜30秒待機(受信者数 × 200ms)
9. 確認:
- 緑色のバナー「X/X メールを送信しました(0エラー)」
- 履歴テーブルに新しい行が追加され、ステータスが「送信済み」
- オーナーが受信トレイを確認 → {{name}} が実際の名前に置き換えられたメールが届いている
最終出力
✓ Supabase + recharts + メールマーケティングを使用した CRM形式の /admin ページをスキャフォールディングしました
📁 新規ファイル (9):
lib/
admin-auth.ts (タイミングセーフなパスワードチェック)
admin-stats.ts (KPI + 時系列 + 変化率)
campaigns.ts (CRUD + オーディエンスクエリ)
email-render.ts (ピュア: textToHtml + wrapEmailHtml + renderCampaignEmail — サーバーとクライアントプレビューで共通)
mailer.ts (nodemailer トランスポーター + email-render の再エクスポート + オプトアウトフッター)
app/admin/page.tsx (1CRM 3タブUI — 概要 / 顧客 / メールマーケティング — テキスト/HTML本文 + プレビュー)
app/api/admin/
leads/route.ts (GET リスト + フィルター)
dashboard/route.ts (GET KPI + 時系列)
campaigns/route.ts (GET 履歴 + POST 一括送信)
🗄 Supabase マイグレーション:
- campaigns + campaign_sends テーブル (RLS 拒否-all, service_role バイパス)
- templates/supabase-migration-admin.sql を実行済み ✓
📦 NPM 依存関係:
- recharts (新規インストール)
- nodemailer (biz-email-setup から既存)
🔐 環境変数:
- ADMIN_PASSWORD を .env.local に追記
- .gitignore に .env.local が含まれていることを確認
- SMTP_* + MAIL_FROM は biz-email-setup から再利用
🧪 テスト:
1. ✓ Curl ダッシュボードエンドポイント (テスト1)
2. ✓ ブラウザ UI スモークテスト (テスト2)
3. ✓ オーナー宛テストキャンペーン送信 (テスト3)
🔧 ユーザー自身で行う TODO:
1.(本番デプロイ時)ADMIN_PASSWORD を Vercel 環境変数に追加:
vercel env add ADMIN_PASSWORD production
2. 再デプロイ: vercel --prod
3. https://<domain>/admin を開く → パスワード入力 → 3タブが動作することを確認
4. 初めて実際のキャンペーンを送信する際は、小規模なオーディエンス(last_days 7)を選択して
プロバイダの SMTP レート制限を確認してから、'all' にスケールアップする
テンプレート
| ファイル | 説明 |
|---|---|
templates/supabase-migration-admin.sql | campaigns + campaign_sends テーブルを作成するマイグレーション |
templates/lib-admin-auth.ts | checkAdminPass() タイミングセーフ比較 |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + 時系列 + 変化率 |
templates/lib-campaigns.ts | CRUD campaigns + queryAudience() |
templates/lib-email-render.ts | ピュアモジュール — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (テキスト/HTML)。nodemailer をインポートしない → クライアントコンポーネントがプレビューに使用可能 |
templates/lib-mailer.ts | nodemailer トランスポーター + sendOne() + sleep() + email-render からのレンダリングヘルパーを再エクスポート |
templates/app-admin-page.tsx | サイドバー + 3タブ + recharts を使用した1CRMスタイルのページ。メールタブにはテキスト/HTML形式切り替え + プレビューボタン(モーダル iframe) |
templates/app-api-admin-leads-route.ts | Leads テーブル API(listLeads() を呼び出す) |
templates/app-api-admin-dashboard-route.ts | ダッシュボード分析 API |
templates/app-api-admin-campaigns-route.ts | GET リスト + POST 作成&送信 |
アンチパターン(やらないこと)
- ❌
===でパスワードを比較する → タイミング攻撃の対象。テンプレートではlib/admin-auth.ts経由でcrypto.timingSafeEqualを使用。 - ❌ ソースにパスワードをハードコードする → 置き換え忘れ → テストパスワードで本番デプロイ。常に
process.env.ADMIN_PASSWORDを使用。 - ❌ URLクエリ文字列でパスワードを送信する(
?pass=123456)→ ブラウザ履歴とアクセスログに漏洩。x-admin-passヘッダーを使用。 - ❌ 個別のログインページ + セッションCookie + JWT を作成する ユーザーがシンプルな管理画面のみを必要としている場合。インラインポップアップで十分。
- ❌ パスワードを localStorage に保存する → JS で読み取り可能、XSS で漏洩。テンプレートでは React state に保存(リフレッシュで消える、意図的)。
- ❌ Supabase 製...
(以下、翻訳が長大になるため、必要に応じて続行可能です)