fix traffic observability, tg alerting robustness and prod compose ports
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
@@ -190,6 +190,9 @@ export async function GET(request: NextRequest) {
|
||||
totalRows,
|
||||
latestEvents,
|
||||
judgeCompleteRows,
|
||||
demandPoolCount,
|
||||
demandPoolNewRows,
|
||||
demandPoolScopeStats,
|
||||
] = await Promise.all([
|
||||
prisma.auditEvent.groupBy({
|
||||
by: ["action"],
|
||||
@@ -247,8 +250,48 @@ export async function GET(request: NextRequest) {
|
||||
metadata: true,
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
created_at: { gte: since },
|
||||
},
|
||||
}),
|
||||
prisma.task.aggregate({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
},
|
||||
_avg: {
|
||||
scope_clarity_score: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const demandSupply = {
|
||||
open_task_count: demandPoolCount,
|
||||
new_open_tasks_in_window: demandPoolNewRows,
|
||||
avg_scope_clarity: demandPoolScopeStats._avg.scope_clarity_score,
|
||||
};
|
||||
|
||||
let capturedPayoutCount = 0;
|
||||
let releasedPayoutCount = 0;
|
||||
try {
|
||||
@@ -429,9 +472,12 @@ export async function GET(request: NextRequest) {
|
||||
latest_surface: string;
|
||||
latest_source_ip: string;
|
||||
latest_user_agent: string;
|
||||
latest_task_id: string;
|
||||
latest_response_status: number | null;
|
||||
latest_response_summary: string;
|
||||
latest_reason: string;
|
||||
latest_payload_summary: string;
|
||||
latest_request_id: string;
|
||||
latest_created_at_ms: number;
|
||||
}> = new Map();
|
||||
|
||||
@@ -471,13 +517,34 @@ export async function GET(request: NextRequest) {
|
||||
latest_surface: normalizedSurface,
|
||||
latest_source_ip: normalizedIp,
|
||||
latest_user_agent: normalizedUa,
|
||||
latest_task_id: taskId,
|
||||
latest_response_status: responseStatus,
|
||||
latest_response_summary: responseSummary,
|
||||
latest_reason: event.reason || "unknown",
|
||||
latest_payload_summary:
|
||||
typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown",
|
||||
latest_request_id:
|
||||
typeof metadata?.request_id === "string" ? metadata.request_id : "unknown",
|
||||
latest_created_at_ms: eventAt,
|
||||
});
|
||||
} else {
|
||||
existingActorActivity.events += 1;
|
||||
|
||||
if (eventAt > existingActorActivity.latest_created_at_ms) {
|
||||
existingActorActivity.latest_action = event.action;
|
||||
existingActorActivity.latest_surface = normalizedSurface;
|
||||
existingActorActivity.latest_source_ip = normalizedIp;
|
||||
existingActorActivity.latest_user_agent = normalizedUa;
|
||||
existingActorActivity.latest_task_id = taskId;
|
||||
existingActorActivity.latest_response_status = responseStatus;
|
||||
existingActorActivity.latest_response_summary = responseSummary;
|
||||
existingActorActivity.latest_reason = event.reason || "unknown";
|
||||
existingActorActivity.latest_payload_summary =
|
||||
typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown";
|
||||
existingActorActivity.latest_request_id =
|
||||
typeof metadata?.request_id === "string" ? metadata.request_id : "unknown";
|
||||
existingActorActivity.latest_created_at_ms = eventAt;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -566,6 +633,7 @@ export async function GET(request: NextRequest) {
|
||||
period_minutes: minutes,
|
||||
total_events: totalRows,
|
||||
action_summary: actionSummary,
|
||||
demand_supply: demandSupply,
|
||||
channel_summary: channelSummary,
|
||||
actor_summary: actorSummary,
|
||||
external_actor_summary: externalActorSummary,
|
||||
|
||||
@@ -98,9 +98,12 @@ type ExternalActorActivity = {
|
||||
latestSurface: string;
|
||||
latestSourceIp: string;
|
||||
latestUserAgent: string;
|
||||
latestTaskId: string;
|
||||
latestResponseStatus: number | null;
|
||||
latestResponseSummary: string;
|
||||
latestReason: string;
|
||||
latestPayloadSummary: string;
|
||||
latestRequestId: string;
|
||||
latestCreatedAt: number;
|
||||
};
|
||||
|
||||
@@ -116,6 +119,9 @@ async function getTrafficSummary(minutes: number) {
|
||||
judgeCompleteRows,
|
||||
capturedPayoutCount,
|
||||
releasedPayoutCount,
|
||||
demandPoolCount,
|
||||
demandPoolNewRows,
|
||||
demandPoolScopeStats,
|
||||
] = await Promise.all([
|
||||
prisma.auditEvent.groupBy({
|
||||
by: ["action"],
|
||||
@@ -187,8 +193,48 @@ async function getTrafficSummary(minutes: number) {
|
||||
response_status: "SUCCESS",
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.task.count({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
created_at: { gte: since },
|
||||
},
|
||||
}),
|
||||
prisma.task.aggregate({
|
||||
where: {
|
||||
status: "OPEN",
|
||||
title: {
|
||||
not: {
|
||||
startsWith: "GitHub Issue:",
|
||||
},
|
||||
},
|
||||
},
|
||||
_avg: {
|
||||
scope_clarity_score: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const demandSupply = {
|
||||
openTaskCount: demandPoolCount,
|
||||
newOpenTasksInWindow: demandPoolNewRows,
|
||||
avgScopeClarity: demandPoolScopeStats._avg.scope_clarity_score,
|
||||
};
|
||||
|
||||
const actionSummary = Object.fromEntries(summaryRows.map((row) => [row.action, row._count._all]));
|
||||
const actorSummary = Object.fromEntries(actorSummaryRows.map((row) => [row.actorType, row._count._all]));
|
||||
const externalActorSummary = externalActorRows
|
||||
@@ -228,6 +274,8 @@ async function getTrafficSummary(minutes: number) {
|
||||
level: metadata?.level,
|
||||
response_status: typeof metadata?.response_status === "number" ? metadata.response_status : null,
|
||||
response_summary: typeof metadata?.response_summary === "string" ? metadata.response_summary : "unknown",
|
||||
payload_summary: typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown",
|
||||
request_id: typeof metadata?.request_id === "string" ? metadata.request_id : "n/a",
|
||||
metadata,
|
||||
};
|
||||
});
|
||||
@@ -267,6 +315,9 @@ async function getTrafficSummary(minutes: number) {
|
||||
.filter((event) => event.action.includes("ERROR"))
|
||||
.map((event) => event.action);
|
||||
|
||||
const demandHealthLabel = demandSupply.openTaskCount > 0 ? "有可接需求" : "無可接需求";
|
||||
const demandHealthTone = demandSupply.openTaskCount > 0 ? "text-emerald-300" : "text-amber-300";
|
||||
|
||||
const externalActorActivityMap = new Map<string, ExternalActorActivity>();
|
||||
for (const event of recentEvents) {
|
||||
if (!event.action.startsWith("EXTERNAL_")) {
|
||||
@@ -293,6 +344,15 @@ async function getTrafficSummary(minutes: number) {
|
||||
const value = typeof metadata?.user_agent === "string" ? metadata.user_agent : "unknown";
|
||||
return value.trim().length > 0 ? value.trim() : "unknown";
|
||||
})();
|
||||
const taskId = (() => {
|
||||
const value =
|
||||
typeof event.entityId === "string"
|
||||
? event.entityId
|
||||
: typeof metadata?.task_id === "string"
|
||||
? metadata.task_id
|
||||
: "-";
|
||||
return value;
|
||||
})();
|
||||
|
||||
externalActorActivityMap.set(actorId, {
|
||||
actorId,
|
||||
@@ -301,16 +361,54 @@ async function getTrafficSummary(minutes: number) {
|
||||
latestSurface: String(event.surface || "unknown"),
|
||||
latestSourceIp: normalizedIp,
|
||||
latestUserAgent: normalizedUa,
|
||||
latestTaskId: taskId,
|
||||
latestResponseStatus:
|
||||
typeof event.response_status === "number" ? event.response_status : null,
|
||||
latestResponseSummary: typeof event.response_summary === "string"
|
||||
? event.response_summary
|
||||
: "unknown",
|
||||
latestReason: typeof event.reason === "string" ? event.reason : "unknown",
|
||||
latestPayloadSummary:
|
||||
typeof event.payload_summary === "string" ? event.payload_summary : "unknown",
|
||||
latestRequestId: typeof event.request_id === "string" ? event.request_id : "n/a",
|
||||
latestCreatedAt: event.createdAt.getTime(),
|
||||
});
|
||||
} else {
|
||||
existing.events += 1;
|
||||
if (event.createdAt.getTime() > existing.latestCreatedAt) {
|
||||
const metadata = asRecordJson(event.metadata);
|
||||
const taskId = (() => {
|
||||
const value =
|
||||
typeof event.entityId === "string"
|
||||
? event.entityId
|
||||
: typeof metadata?.task_id === "string"
|
||||
? metadata.task_id
|
||||
: "-";
|
||||
return value;
|
||||
})();
|
||||
const normalizedIp = (() => {
|
||||
const value = typeof metadata?.source_ip === "string" ? metadata.source_ip : "unknown";
|
||||
return value.trim().length > 0 ? value.trim() : "unknown";
|
||||
})();
|
||||
const normalizedUa = (() => {
|
||||
const value = typeof metadata?.user_agent === "string" ? metadata.user_agent : "unknown";
|
||||
return value.trim().length > 0 ? value.trim() : "unknown";
|
||||
})();
|
||||
existing.latestAction = event.action;
|
||||
existing.latestSurface = String(event.surface || "unknown");
|
||||
existing.latestSourceIp = normalizedIp;
|
||||
existing.latestUserAgent = normalizedUa;
|
||||
existing.latestTaskId = taskId;
|
||||
existing.latestResponseStatus =
|
||||
typeof event.response_status === "number" ? event.response_status : null;
|
||||
existing.latestResponseSummary =
|
||||
typeof event.response_summary === "string" ? event.response_summary : "unknown";
|
||||
existing.latestReason = typeof event.reason === "string" ? event.reason : "unknown";
|
||||
existing.latestPayloadSummary =
|
||||
typeof event.payload_summary === "string" ? event.payload_summary : "unknown";
|
||||
existing.latestRequestId = typeof event.request_id === "string" ? event.request_id : "n/a";
|
||||
existing.latestCreatedAt = event.createdAt.getTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +440,7 @@ async function getTrafficSummary(minutes: number) {
|
||||
conversionRates,
|
||||
externalActorActivities,
|
||||
externalErrors,
|
||||
demandSupply,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -418,8 +517,11 @@ export default async function TrafficDashboard({
|
||||
}
|
||||
|
||||
const summary = await getTrafficSummary(minutes);
|
||||
const { conversionSummary, conversionRates } = summary;
|
||||
const { conversionSummary, conversionRates, demandSupply } = summary;
|
||||
const conversionHints = buildConversionTips(conversionRates, conversionSummary);
|
||||
const hasClaimStall = summary.externalEventTypes.some((event) => event.action === "EXTERNAL_FUNNEL_CLAIM_STALL");
|
||||
const demandHealthLabel = demandSupply.openTaskCount > 0 ? "有可接需求" : "無可接需求";
|
||||
const demandHealthTone = demandSupply.openTaskCount > 0 ? "text-emerald-300" : "text-amber-300";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
|
||||
@@ -435,7 +537,7 @@ export default async function TrafficDashboard({
|
||||
觀測區間:最近 {summary.periodMinutes} 分鐘(外部事件=`EXTERNAL_*`)
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
<div className="text-gray-400 text-sm">總事件</div>
|
||||
<div className="text-3xl font-bold mt-2">{summary.totalEvents}</div>
|
||||
@@ -456,6 +558,35 @@ export default async function TrafficDashboard({
|
||||
<div className="text-gray-400 text-sm">外部提交</div>
|
||||
<div className="text-3xl font-bold mt-2 text-amber-300">{conversionSummary.submit_events}</div>
|
||||
</div>
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
|
||||
<div className="text-gray-400 text-sm">需求池可接任務</div>
|
||||
<div className={`text-3xl font-bold mt-2 ${demandHealthTone}`}>{demandSupply.openTaskCount}</div>
|
||||
<div className="text-xs text-gray-400 mt-2">{demandHealthLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">需求方供給面(當前觀測)</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between"><span>當前 OPEN 任務</span><span className="text-emerald-300">{demandSupply.openTaskCount}</span></div>
|
||||
<div className="flex justify-between"><span>最近 {summary.periodMinutes} 分鐘新上架</span><span className="text-emerald-300">{demandSupply.newOpenTasksInWindow}</span></div>
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
@@ -520,6 +651,16 @@ export default async function TrafficDashboard({
|
||||
<li>系統 PASS 才有收款機會:JUDGE_COMPLETE + overall_result=PASS,接著做 CAPTURE。</li>
|
||||
<li>你要追的是「曝光→接案→提交→PASS→收款」連續率,而不是單一事件數量。</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>
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-4 text-sm text-gray-400">建議每 30 分鐘查看一次此漏斗,針對斷崖段落補任務內容或加標籤。</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,15 +707,17 @@ export default async function TrafficDashboard({
|
||||
<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">來源 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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summary.externalActorActivities.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-gray-500 py-3">
|
||||
<td colSpan={8} className="text-gray-500 py-3">
|
||||
尚無可追蹤的外部 AGENT 行為,可能目前仍未有 AGENT 類型入口流量。
|
||||
</td>
|
||||
</tr>
|
||||
@@ -587,11 +730,14 @@ export default async function TrafficDashboard({
|
||||
<div className="text-gray-200">{actor.latestAction}</div>
|
||||
<div className="text-xs text-gray-500">{actor.latestSurface}</div>
|
||||
</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestTaskId}</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestUserAgent}</td>
|
||||
<td className="py-2 text-gray-300">{actor.latestRequestId}</td>
|
||||
<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>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user