Biz Admin Leads Dashboard — CRM 風格 /admin 後台 for Supabase
在 Next.js 專案 (App Router) 中建立 /admin 頁面,風格類似 1CRM。3 個標籤頁 — 總覽 (KPI + 圖表)、客戶 (表格 + 篩選)、電子郵件行銷 (撰寫 + 大量寄送 + 歷史記錄)。
設計理念:比 1CRM 更簡潔 (共用一組密碼,無多用戶),但具備足夠的圖表、差異變化與電子郵件行銷功能,可供 1-2 位企業主使用。所有狀態儲存於 Supabase,檔案結構獨立於現有專案。
/admin → 彈窗 "輸入管理密碼" → 各個標籤頁呼叫 /api/admin/{leads|dashboard|campaigns}
→ 每次請求帶上 header x-admin-pass
→ 伺服器以 timing-safe 方式比對 ADMIN_PASSWORD 環境變數
密碼儲存在 React 狀態中 — 重新整理頁面 = 再次顯示彈窗 (刻意為之,簡單設計,無需 session cookie)。
僅支援 Supabase — KV 變體已移除。原因:campaigns + campaign_sends 需要 SQL join 與 service_role 繞過 RLS;KV 原生不支援此功能。
何時使用此技能
- 專案已透過
lib/leads-supabase.ts(從biz-setup-sepay-payment選擇 Supabase) 整合 Sepay 金流與客戶資料儲存 → 需要檢視層、儀表板與電子郵件行銷。 - 使用者想要 CRM 風格的頁面:包含差異變化的 KPI 卡片、按日顯示的折線圖、轉換率圓餅圖,以及向已註冊客戶發送促銷郵件。
請勿使用於以下情況:
- 專案尚未建立
lib/leads-supabase.ts→ 請使用者先執行/biz-setup-sepay-payment(選擇 Supabase)。 - 專案使用 Vercel KV 而非 Supabase → 此技能已不支援 KV。使用者僅有兩個選擇:遷移至 Supabase 或自行手動移植。
- 需要多管理員角色/權限 → 此技能使用共用密碼,不適用。
- 需要詳細的郵件追蹤 (每位收件者的開啟/點擊) → 此技能僅追蹤寄送成功/失敗。使用者需使用 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 Editor 中執行 supabase-migration-admin.sql — 閘道
Phase 4: 將 ADMIN_PASSWORD 寫入 .env.local
Phase 5: 建立 9 個程式碼檔案 (5 個函式庫 + 4 個路由 + 1 個頁面)
Phase 6: 3 層測試 (cURL 儀表板、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 "Provider-agnostic wrapper: ✓"
test -f lib/supabase-admin.ts && echo "Service-role client: ✓"
# 驗證 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(用於折線圖與圓餅圖)- 執行 SQL 遷移,在 Supabase 中建立
campaigns+campaign_sends資料表- 將
ADMIN_PASSWORD寫入 .env.local- 建立 9 個檔案 (5 個函式庫 + 4 個路由 + 1 個頁面)
- 引導您進行測試
準備好進入 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 Dashboard → SQL Editor → New query → 將下方完整內容貼上 → 點擊 Run:
[在此貼上 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 頁面密碼
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 ← 樣板 lib-admin-auth.ts
lib/admin-stats.ts ← 樣板 lib-admin-stats.ts
lib/campaigns.ts ← 樣板 lib-campaigns.ts
lib/email-render.ts ← 樣板 lib-email-render.ts
lib/mailer.ts ← 樣板 lib-mailer.ts
app/admin/page.tsx ← 樣板 app-admin-page.tsx
app/api/admin/leads/route.ts ← 樣板 app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← 樣板 app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← 樣板 app-api-admin-campaigns-route.ts
Email 渲染模組:lib/email-render.ts 是純模組 (不匯入 nodemailer) — 包含 textToHtml / wrapEmailHtml / personalize / renderCampaignEmail。lib/mailer.ts 重新匯出這些函式並加上 SMTP 傳輸。如此分離,app/admin/page.tsx (客戶端元件) 便可為「預覽」按鈕匯入渲染輔助函式,而無需將 nodemailer 帶入客戶端 bundle。
注意:如果專案已從 biz-email-setup 擁有 lib/mailer.ts:請勿覆蓋 — 將此技能的 mailer 建立為 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 (來自 biz-setup-sepay-payment 的供應商無關包裝) 匯入 listLeads。如果使用者之前設定 KV 後又改名,則需要確認包裝器指向 Supabase。
資料流程圖
瀏覽器 (admin/page.tsx)
│ 使用 fetch 加上 header 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 (body 包含 bodyFormat: 'text' | 'html')
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (渲染 1 次)
└─► lib/campaigns.createCampaign (campaigns 資料列)
└─► loop (限流 200ms):
lib/mailer.personalize(html/text per recipient)
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 卡片、折線圖、圓餅圖、7d/30d/90d 期間選擇器可運作
6. 客戶標籤頁:應看到表格 + 篩選 + CSV 匯出
7. 電子郵件行銷標籤頁:應看到撰寫表單 + 歷史記錄 (一開始為空)
8. 電子郵件行銷標籤頁:輸入內容 → 點擊 "👁 預覽" → 應出現 modal 顯示郵件
渲染畫面 (切換「純文字」↔「HTML」模式以確認兩者均可運作)
測試 3 — 為擁有者發送測試活動郵件
重要:選擇 audience
last_days= 7 (配合已設定的密碼),並在郵件內容中使用{{name}}。先向資料庫中唯一的測試 lead (擁有者的測試 lead) 發送測試郵件,以在真正大量發送前確認流程正常。
1. 進入電子郵件行銷標籤頁
2. 活動名稱:"測試發送"
3. 主旨:"來自 /admin 的測試郵件 — 您收到了嗎?"
4. 收件者:"最近天數" → 7 天
5. 內容:
哈囉 {{name}},
這是從管理頁面發送的測試郵件,用於確認 SMTP 運作正常。
如果您收到了,我將開始發送真正的行銷活動。
6. 點擊 "👁 預覽" → 確認郵件顯示正確 → 關閉 modal
7. 點擊 "立即發送" → 確認
8. 等待 10-30 秒 (200ms × 收件者人數)
9. 驗證:
- 應出現綠色橫幅 "已成功發送 X/X 封郵件 (0 個錯誤)"
- 歷史記錄表格中應出現新的一列,狀態為 "已發送"
- 擁有者檢查收件匣 → 應收到一封郵件,其中 `{{name}}` 已被取代為實際姓名
最終輸出
✓ 已建立 /admin CRM 風格頁面,使用 Supabase + recharts + 電子郵件行銷
📁 新增的檔案 (9 個):
lib/
admin-auth.ts (timing-safe 密碼檢查)
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 deny-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)
🔧 您待處理的事項:
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() timing-safe 比對 |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + 時間序列 + 差異變化 |
templates/lib-campaigns.ts | CRUD campaigns + queryAudience() |
templates/lib-email-render.ts | 純函式模組 — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (text/HTML)。未匯入 nodemailer → 客戶端元件可用於預覽 |
templates/lib-mailer.ts | nodemailer 傳輸器 + sendOne() + sleep() + 從 email-render 重新匯出渲染輔助函式 |
templates/app-admin-page.tsx | 1CRM 風格頁面,含側邊欄 + 3 個標籤頁 + recharts。郵件標籤頁包含文字/HTML 格式切換 + 預覽按鈕 (iframe modal) |
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 建立與發送 |
反模式 (請勿執行)
- ❌ 使用
===比較密碼 → timing 攻擊。樣板透過lib/admin-auth.ts使用crypto.timingSafeEqual。 - ❌ 在原始碼中硬編碼密碼 → 忘記修改 → 正式環境中使用了測試密碼。一律透過
process.env.ADMIN_PASSWORD取得。 - ❌ 透過 URL 查詢字串傳遞密碼 (
?pass=123456) → 洩漏至瀏覽器歷史記錄與存取日誌。請使用 headerx-admin-pass。 - ❌ 為簡單的管理需求建立獨立登入頁面 + session cookie + JWT。行內彈窗即可滿足。
- ❌ 將密碼儲存在 localStorage → JavaScript 可讀取,XSS 洩漏。樣板儲存在 React 狀態中 (重新整理即消失,符合設計初衷)。
- ❌ 跳過檢查
…