fix: route traffic monitor through admin auth
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
This commit is contained in:
@@ -8,21 +8,21 @@ export const dynamic = "force-dynamic";
|
||||
const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN;
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
EXTERNAL_LIST_OPEN_TASKS: "外部公開流量頁讀取 open tasks",
|
||||
EXTERNAL_A2A_ONBOARDING_VIEW: "外部 Agent 讀取 onboarding contract",
|
||||
PUBLIC_A2A_ONBOARDING_VIEW: "公開 A2A onboarding 被讀取",
|
||||
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部 Agent 領取需求 campaign kit",
|
||||
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
|
||||
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
|
||||
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部 Agent 回報導流 touchpoint",
|
||||
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
|
||||
EXTERNAL_LIST_OPEN_TASKS: "外部查看可接需求",
|
||||
EXTERNAL_A2A_ONBOARDING_VIEW: "外部讀取合作說明",
|
||||
PUBLIC_A2A_ONBOARDING_VIEW: "公開合作說明被讀取",
|
||||
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部領取需求導流素材",
|
||||
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部查看整合目錄",
|
||||
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部領取成長導流素材",
|
||||
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部引薦紀錄",
|
||||
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部查詢引薦狀態",
|
||||
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
|
||||
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
|
||||
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始 Stripe 結帳",
|
||||
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始線上結帳",
|
||||
EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING: "外部導流需求方取得錢包付款指示",
|
||||
EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED: "外部導流提案費付款成功",
|
||||
EXTERNAL_LIST_OPEN_TASKS_SURGE: "外部公開流量突增告警",
|
||||
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部 MCP 入口讀取 open tasks",
|
||||
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部整合入口查看可接需求",
|
||||
EXTERNAL_LIST_OPEN_TASKS_MCP_SURGE: "外部 MCP 流量突增告警",
|
||||
EXTERNAL_CLAIM_TASK_SUCCESS: "外部 AI 成功接單",
|
||||
EXTERNAL_SUBMIT_SOLUTION_SUCCESS: "外部 AI 提交解法",
|
||||
@@ -32,7 +32,7 @@ const EVENT_LABELS: Record<string, string> = {
|
||||
EXTERNAL_LIST_OPEN_TASKS_MCP_ERROR: "外部 MCP 流量端點錯誤",
|
||||
EXTERNAL_FUNNEL_CLAIM_STALL: "外部曝光後未接案",
|
||||
EXTERNAL_FUNNEL_SUBMIT_STALL: "外部接案後未提交",
|
||||
EXTERNAL_FUNNEL_PASS_STALL: "外部提交後未 PASS",
|
||||
EXTERNAL_FUNNEL_PASS_STALL: "外部提交後未通過",
|
||||
EXTERNAL_FUNNEL_PAYOUT_STALL: "PASS 後未出金",
|
||||
JUDGE_COMPLETE: "AI 交件判定完成",
|
||||
};
|
||||
@@ -95,11 +95,6 @@ function isAuthorizedToken(token: string | undefined, tokenHeader: string | unde
|
||||
return tokenHeader === token;
|
||||
}
|
||||
|
||||
function eventDirection(action: string) {
|
||||
if (action.startsWith("EXTERNAL_")) return "外部";
|
||||
return "內部";
|
||||
}
|
||||
|
||||
function explainAction(action: string) {
|
||||
return EVENT_LABELS[action] || action;
|
||||
}
|
||||
@@ -617,15 +612,15 @@ function buildConversionTips(summary: {
|
||||
const steps: string[] = [];
|
||||
|
||||
if (conversionSummary.discovery_events > 0 && conversionSummary.referral_touchpoint_events === 0) {
|
||||
steps.push("A2A 曝光已有資料但外部 Agent touchpoint 為零:優先確認 campaign kit 是否要求回報 /api/a2a/referrals/touch。");
|
||||
steps.push("已有曝光但沒有引薦紀錄:請確認對外素材是否已更新,並檢查引薦連結是否正常帶入。");
|
||||
}
|
||||
|
||||
if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_view_events === 0) {
|
||||
steps.push("外部 Agent 已回報導流 touchpoint 但提案頁查看為零:優先檢查送出的 proposal_url、prefilled_proposal_url 與 vibework.wooo.work 代理。");
|
||||
steps.push("已有引薦紀錄但未進入提案頁:請檢查對外連結是否指向正式提案入口。");
|
||||
}
|
||||
|
||||
if (conversionSummary.discovery_events > 0 && conversionSummary.proposal_view_events === 0) {
|
||||
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、touchpoint 回傳 URL、外部貼文 CTA 與 /propose 代理。");
|
||||
steps.push("已有曝光但沒有提案頁查看:請檢查對外 CTA、提案入口與正式網域代理狀態。");
|
||||
}
|
||||
|
||||
if (conversionSummary.proposal_view_events > 0 && conversionSummary.proposal_created_events === 0) {
|
||||
@@ -633,11 +628,11 @@ function buildConversionTips(summary: {
|
||||
}
|
||||
|
||||
if (conversionSummary.proposal_created_events > 0 && conversionSummary.proposal_paid_events === 0) {
|
||||
steps.push("已有提案但尚未付款成功:分流檢查 Stripe checkout 與 USDC wallet receipt verification。");
|
||||
steps.push("已有提案但尚未付款成功:請分別檢查線上結帳與錢包收款確認流程。");
|
||||
}
|
||||
|
||||
if (conversionSummary.discovery_events > 0 && conversionSummary.claim_events === 0) {
|
||||
steps.push("曝光高但接單為零:請先檢查 open-tasks 回傳任務文案是否足夠明確,是否包含 npx 指令與標準格式。");
|
||||
steps.push("曝光高但接單為零:請先檢查任務標題、酬金、驗收條件與交付格式是否足夠明確。");
|
||||
}
|
||||
|
||||
if (conversionSummary.claim_events > 0 && conversionSummary.submit_events === 0) {
|
||||
@@ -645,7 +640,7 @@ function buildConversionTips(summary: {
|
||||
}
|
||||
|
||||
if (conversionSummary.submit_events > 0 && conversionSummary.judge_pass_events === 0) {
|
||||
steps.push("有提交但無 PASS:先人工檢查提交格式、sandbox 測試、deliverable 欄位是否可被執行。");
|
||||
steps.push("有提交但未通過:先檢查交付格式、測試結果與驗收條件是否可被系統判讀。");
|
||||
}
|
||||
|
||||
if (conversionSummary.judge_pass_events > 0 && conversionSummary.payout_captured === 0) {
|
||||
@@ -657,7 +652,7 @@ function buildConversionTips(summary: {
|
||||
}
|
||||
|
||||
if (summary.pass_rate < 35 && conversionSummary.submit_events > 20) {
|
||||
steps.push("PASS 率偏低:提高指令兼容度,改用可判讀的輸出欄位與固定檔名。");
|
||||
steps.push("通過率偏低:提高指令兼容度,改用可判讀的輸出欄位與固定檔名。");
|
||||
}
|
||||
|
||||
if (steps.length === 0) {
|
||||
@@ -683,8 +678,13 @@ export default async function TrafficDashboard({
|
||||
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-4">VibeWork 流量監控</h1>
|
||||
<p className="text-gray-300 mb-4">請輸入正確的監控 Token 才能查看此頁。</p>
|
||||
<p className="text-sm text-gray-500">範例:<code>/traffic?token=YOUR_TOKEN&minutes=60</code></p>
|
||||
<p className="text-gray-300 mb-6">此頁需要管理權限。請先登入管理入口後查看。</p>
|
||||
<Link
|
||||
href="/admin/traffic"
|
||||
className="inline-flex rounded-lg bg-emerald-500 px-4 py-2 text-sm font-semibold text-gray-950 hover:bg-emerald-400"
|
||||
>
|
||||
前往管理登入
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -701,14 +701,14 @@ export default async function TrafficDashboard({
|
||||
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">VibeWork 流量監控(轉化導向)</h1>
|
||||
<h1 className="text-3xl font-bold">VibeWork 流量監控</h1>
|
||||
<Link href="/" className="text-blue-400 hover:text-blue-300">
|
||||
← 回首頁
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
觀測區間:最近 {summary.periodMinutes} 分鐘(外部事件=`EXTERNAL_*`)
|
||||
觀測區間:最近 {summary.periodMinutes} 分鐘
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
@@ -725,7 +725,7 @@ export default async function TrafficDashboard({
|
||||
<div className="text-3xl font-bold mt-2 text-cyan-300">{conversionSummary.discovery_events}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
<div className="text-gray-400 text-sm">Agent 導流回報</div>
|
||||
<div className="text-gray-400 text-sm">引薦紀錄</div>
|
||||
<div className="text-3xl font-bold mt-2 text-teal-300">{conversionSummary.referral_touchpoint_events}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
@@ -764,17 +764,17 @@ export default async function TrafficDashboard({
|
||||
<div className="flex justify-between"><span>平均需求清晰度</span><span className="text-emerald-300">{demandSupply.avgScopeClarity === null ? "n/a" : `${Number(demandSupply.avgScopeClarity).toFixed(2)}`}</span></div>
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-gray-400">
|
||||
來源條件:Task.status = OPEN 且 title 不以 GitHub Issue: 開頭。
|
||||
統計已排除工程同步任務,保留可直接對外承接的需求。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部未接案自動化排查</h2>
|
||||
<h2 className="text-xl font-semibold mb-4">需求供給檢查</h2>
|
||||
<ul className="text-sm text-gray-200 space-y-2 list-disc list-inside">
|
||||
<li>1) 先確認需求池:若 <span className="text-emerald-300">{demandSupply.openTaskCount}</span> 筆,請先確認任務文案是否有可執行 step。</li>
|
||||
<li>2) 重試建議:再打 <span className="font-mono text-xs">/api/open-tasks</span> 3 次(每次間隔 30~60 秒)確認回傳一致。</li>
|
||||
<li>3) 直接驗證任務描述摘要,避免只用含「GitHub Issue:」字頭(現已過濾)。</li>
|
||||
<li>4) 若仍未有接單:將任務 reward、required_stack 與 developer_wallet 指引補齊為固定欄位範例。</li>
|
||||
<li>先確認需求池仍有 <span className="text-emerald-300">{demandSupply.openTaskCount}</span> 筆可承接需求。</li>
|
||||
<li>若曝光高但接單低,優先補強任務標題、酬金、交付格式與驗收條件。</li>
|
||||
<li>若需求清晰度偏低,先拆成較小的可交付項目,再重新對外發布。</li>
|
||||
<li>若外部流量已進來,請同步檢查提案入口與付款路徑是否保持可用。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,7 +784,7 @@ export default async function TrafficDashboard({
|
||||
<h2 className="text-xl font-semibold mb-4">外部流量轉化漏斗</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>A2A曝光→Agent導流回報</span>
|
||||
<span>A2A曝光→引薦</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
@@ -794,7 +794,7 @@ export default async function TrafficDashboard({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Agent導流回報→提案頁</span>
|
||||
<span>引薦→提案頁</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_to_proposal_view_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
@@ -854,7 +854,7 @@ export default async function TrafficDashboard({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>提交→PASS</span>
|
||||
<span>提交→通過</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.pass_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
@@ -864,7 +864,7 @@ export default async function TrafficDashboard({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>PASS→收款成功</span>
|
||||
<span>通過→收款成功</span>
|
||||
<span className="text-emerald-300">{fmtPercent(conversionRates.payout_rate)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
@@ -875,31 +875,31 @@ export default async function TrafficDashboard({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-gray-300 space-y-1">
|
||||
<div className="flex justify-between"><span>Stripe checkout 開始</span><span>{conversionSummary.proposal_checkout_events}</span></div>
|
||||
<div className="flex justify-between"><span>線上結帳開始</span><span>{conversionSummary.proposal_checkout_events}</span></div>
|
||||
<div className="flex justify-between"><span>錢包付款待確認</span><span>{conversionSummary.proposal_wallet_pending_events}</span></div>
|
||||
<div className="flex justify-between"><span>PASS 任務</span><span>{conversionSummary.judge_pass_events}</span></div>
|
||||
<div className="flex justify-between"><span>FAIL 任務</span><span>{conversionSummary.judge_fail_events}</span></div>
|
||||
<div className="flex justify-between"><span>通過任務</span><span>{conversionSummary.judge_pass_events}</span></div>
|
||||
<div className="flex justify-between"><span>未通過任務</span><span>{conversionSummary.judge_fail_events}</span></div>
|
||||
<div className="flex justify-between"><span>已出金</span><span>{conversionSummary.payout_captured}</span></div>
|
||||
<div className="flex justify-between"><span>已退費</span><span>{conversionSummary.payout_released}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">轉化流程與使用者後續處理(你現在該做什麼)</h2>
|
||||
<h2 className="text-xl font-semibold mb-4">轉化流程</h2>
|
||||
<ol className="text-sm text-gray-200 space-y-2 list-decimal list-inside">
|
||||
<li>曝光端流量先判斷:從 open-tasks / MCP 的呼叫量,看是否有可感知的外部探索流。</li>
|
||||
<li>接案事件代表 AI 真的把任務當作可接單:EXTERNAL_CLAIM_TASK_SUCCESS。</li>
|
||||
<li>提交事件代表 AI 真的開始交件:EXTERNAL_SUBMIT_SOLUTION_SUCCESS。</li>
|
||||
<li>系統 PASS 才有收款機會:JUDGE_COMPLETE + overall_result=PASS,接著做 CAPTURE。</li>
|
||||
<li>你要追的是「曝光→接案→提交→PASS→收款」連續率,而不是單一事件數量。</li>
|
||||
<li>先看外部探索流量,確認合作入口是否真的被外部系統讀取。</li>
|
||||
<li>接案代表外部 AI 已把任務判斷為可承接。</li>
|
||||
<li>提交代表外部 AI 已開始交付結果。</li>
|
||||
<li>交付通過後才進入收款與出金流程。</li>
|
||||
<li>核心指標是「曝光→接案→提交→通過→收款」連續率,而不是單一事件數量。</li>
|
||||
</ol>
|
||||
{hasClaimStall && demandSupply.openTaskCount > 0 ? (
|
||||
<div className="mt-4 p-3 border border-amber-500/40 rounded-lg text-sm text-amber-200">
|
||||
本輪有 EXTERNAL_FUNNEL_CLAIM_STALL 告警。可直接採取:
|
||||
本輪偵測到曝光後未接案。可直接採取:
|
||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||
<li>先取一筆最新 OPEN 任務,核對 `scope_clarity_score` 與 reward 欄位。</li>
|
||||
<li>發送 1 次固定模板 `claim_task` 試探,若失敗 403 則先修正 API Key/權限。</li>
|
||||
<li>若返回 200 仍無接案,調整任務文案再行發佈,讓外部判斷標準更明確。</li>
|
||||
<li>先抽查最新可接需求,核對酬金、交付範圍與驗收條件。</li>
|
||||
<li>確認外部入口仍能讀到需求列表,且需求描述不是只有同步標題。</li>
|
||||
<li>若入口可用但仍無接案,請先縮小任務範圍再重新發布。</li>
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -957,19 +957,19 @@ export default async function TrafficDashboard({
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部 Actor 行為追蹤(可追溯)</h2>
|
||||
<h2 className="text-xl font-semibold mb-4">外部來源行為追蹤</h2>
|
||||
<div className="overflow-auto max-h-96">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-700">
|
||||
<th className="text-left py-2">來源類型</th>
|
||||
<th className="text-left py-2">Actor</th>
|
||||
<th className="text-left py-2">來源</th>
|
||||
<th className="text-left py-2">事件</th>
|
||||
<th className="text-left py-2">最新行為</th>
|
||||
<th className="text-left py-2">任務</th>
|
||||
<th className="text-left py-2">來源 IP</th>
|
||||
<th className="text-left py-2">User-Agent</th>
|
||||
<th className="text-left py-2">Request-Id</th>
|
||||
<th className="text-left py-2">來源識別</th>
|
||||
<th className="text-left py-2">追蹤 ID</th>
|
||||
<th className="text-left py-2">最新回應</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -977,7 +977,7 @@ export default async function TrafficDashboard({
|
||||
{summary.externalActorActivities.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={9} className="text-gray-500 py-3">
|
||||
尚無可追蹤的外部 AGENT 行為,可能目前仍未有 AGENT 類型入口流量。
|
||||
尚無可追蹤的外部 AI 行為,可能目前仍未有外部入口流量。
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
@@ -987,8 +987,7 @@ export default async function TrafficDashboard({
|
||||
<td className="py-2 text-gray-300">{actor.actorId}</td>
|
||||
<td className="py-2 text-emerald-300">{actor.events}</td>
|
||||
<td className="py-2">
|
||||
<div className="text-gray-200">{actor.latestAction}</div>
|
||||
<div className="text-xs text-gray-500">{actor.latestSurface}</div>
|
||||
<div className="text-gray-200">{explainAction(actor.latestAction)}</div>
|
||||
</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestTaskId}</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
|
||||
@@ -997,7 +996,6 @@ export default async function TrafficDashboard({
|
||||
<td className="py-2">
|
||||
<div className="text-gray-200">{actor.latestResponseStatus ?? "n/a"}</div>
|
||||
<div className="text-xs text-gray-500">{actor.latestResponseSummary}</div>
|
||||
<div className="text-xs text-gray-600">{actor.latestPayloadSummary}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
@@ -1008,7 +1006,7 @@ export default async function TrafficDashboard({
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">外部事件說明(可讀)</h2>
|
||||
<h2 className="text-xl font-semibold mb-4">外部事件摘要</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-4 text-sm">
|
||||
{Object.entries(summary.actionSummary)
|
||||
.filter(([action]) => action.startsWith("EXTERNAL_"))
|
||||
@@ -1016,22 +1014,11 @@ export default async function TrafficDashboard({
|
||||
.slice(0, 14)
|
||||
.map(([action, count]) => (
|
||||
<div key={action} className="flex justify-between border-b border-gray-800 py-1">
|
||||
<span className="text-gray-300">{explainAction(action)} ({action})</span>
|
||||
<span className="text-gray-300">{explainAction(action)}</span>
|
||||
<span className="text-emerald-300">{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-sm text-gray-300 space-y-1">
|
||||
<p>事件方向:</p>
|
||||
{Object.entries(summary.actionSummary)
|
||||
.filter(([action]) => action.startsWith("EXTERNAL_"))
|
||||
.map(([action, count]) => (
|
||||
<div key={action} className="flex justify-between">
|
||||
<span className="text-gray-400">{eventDirection(action)}{action}</span>
|
||||
<span>{count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
@@ -1043,13 +1030,13 @@ export default async function TrafficDashboard({
|
||||
</ul>
|
||||
{summary.externalErrors.length > 0 ? (
|
||||
<div className="mt-4 text-sm text-amber-300">
|
||||
注意:目前已偵測到 {summary.externalErrors.length} 種外部錯誤事件({summary.externalErrors.join(", ")}),請優先回查 API payload 與 auth policy。
|
||||
注意:目前已偵測到 {summary.externalErrors.length} 種外部錯誤事件({summary.externalErrors.map(explainAction).join("、")}),請優先回查對外入口與權限設定。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">最近外部事件(top 120)</h2>
|
||||
<h2 className="text-xl font-semibold mb-4">最近外部事件</h2>
|
||||
<div className="space-y-2 max-h-96 overflow-auto">
|
||||
{summary.recentExternalEvents.length === 0 ? (
|
||||
<p className="text-gray-500">目前區間內尚無外部事件。</p>
|
||||
@@ -1058,19 +1045,13 @@ export default async function TrafficDashboard({
|
||||
const ts = toLocalTime(event.createdAt);
|
||||
return (
|
||||
<div key={event.id} className="border-b border-gray-800 py-2 text-sm">
|
||||
<div className="font-mono text-emerald-300">{event.action}</div>
|
||||
<div className="text-emerald-300">{explainAction(event.action)}</div>
|
||||
<div className="text-gray-400">
|
||||
actor={event.actorType}:{event.actorId || "unknown"} | 類別={actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")} | entity={event.entityType}/{event.entityId} | surface={String(event.surface || "-")} | {ts}
|
||||
來源:{event.actorId || "unknown"}|類別:{actorClassLabel((event as { actor_class?: TrafficActorClass }).actor_class || "other_external")}|任務:{event.entityId || "-"}|時間:{ts}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
response={event.response_status ?? "n/a"} / summary={event.response_summary}
|
||||
回應:{event.response_status ?? "n/a"} / {event.response_summary}
|
||||
</div>
|
||||
{event.reason ? <div className="text-gray-500 text-xs mt-1">{event.reason}</div> : null}
|
||||
{event.metadata ? (
|
||||
<pre className="text-xs text-gray-500 mt-1 whitespace-pre-wrap">
|
||||
{JSON.stringify(event.metadata, null, 2)}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -10,8 +10,15 @@ import {
|
||||
export function middleware(request: NextRequest) {
|
||||
const url = request.nextUrl;
|
||||
const isAdminPath = url.pathname.startsWith("/admin");
|
||||
const isTrafficDashboard = url.pathname === "/traffic";
|
||||
const strippedHeaders = stripClientAdminHeaders(request);
|
||||
|
||||
if (isTrafficDashboard && !url.searchParams.get("token")) {
|
||||
const adminTrafficUrl = url.clone();
|
||||
adminTrafficUrl.pathname = "/admin/traffic";
|
||||
return NextResponse.redirect(adminTrafficUrl);
|
||||
}
|
||||
|
||||
if (isAdminPath) {
|
||||
if (!isAdminRequestAuthorized(request)) {
|
||||
return adminUnauthorizedResponse();
|
||||
|
||||
Reference in New Issue
Block a user