169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Image from "next/image";
|
|
import { Bug, ExternalLink, RefreshCw } from "lucide-react";
|
|
import { EmptyState } from "@/components/layout/empty-state";
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
interface DebugRunSummary {
|
|
id: string;
|
|
label: string;
|
|
startedAt: string;
|
|
stepCount: number;
|
|
lastStep?: string | null;
|
|
}
|
|
|
|
interface DebugRunDetail {
|
|
run: {
|
|
id: string;
|
|
label: string;
|
|
startedAt: string;
|
|
steps: Array<{
|
|
step: string;
|
|
at: string;
|
|
url?: string;
|
|
screenshot?: string;
|
|
}>;
|
|
};
|
|
screenshots: string[];
|
|
}
|
|
|
|
export default function DebugPage() {
|
|
const [runs, setRuns] = useState<DebugRunSummary[]>([]);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [detail, setDetail] = useState<DebugRunDetail | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
async function loadRuns() {
|
|
setLoading(true);
|
|
const res = await fetch("/api/debug/runs");
|
|
const data = await res.json();
|
|
const nextRuns = (data.runs ?? []) as DebugRunSummary[];
|
|
setRuns(nextRuns);
|
|
setSelectedId((current) => current ?? nextRuns[0]?.id ?? null);
|
|
setLoading(false);
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadRuns();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!selectedId) {
|
|
setDetail(null);
|
|
return;
|
|
}
|
|
fetch(`/api/debug/runs/${selectedId}`)
|
|
.then((r) => r.json())
|
|
.then((d) => setDetail(d))
|
|
.catch(() => setDetail(null));
|
|
}, [selectedId]);
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
title="瀏覽器 Debug"
|
|
description="檢視 Threads 瀏覽器自動化的執行紀錄與截圖,用於排查發布或海巡問題。"
|
|
action={
|
|
<Button variant="outline" size="sm" onClick={loadRuns}>
|
|
<RefreshCw className="h-4 w-4" />
|
|
重新整理
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{loading ? (
|
|
<div className="skeleton h-48 animate-pulse" />
|
|
) : runs.length === 0 ? (
|
|
<EmptyState
|
|
icon={Bug}
|
|
title="尚無 Debug 紀錄"
|
|
description="設定開啟 Debug 後重試發布"
|
|
/>
|
|
) : (
|
|
<div className="grid gap-5 lg:grid-cols-[280px_1fr]">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">紀錄列表</CardTitle>
|
|
<CardDescription>最近 30 次</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{runs.map((run) => (
|
|
<button
|
|
key={run.id}
|
|
type="button"
|
|
onClick={() => setSelectedId(run.id)}
|
|
className={`w-full rounded-lg border px-3 py-2.5 text-left text-sm transition-colors ${
|
|
selectedId === run.id
|
|
? "border-foreground bg-muted"
|
|
: "border-border hover:bg-muted/60"
|
|
}`}
|
|
>
|
|
<p className="font-medium">{run.label}</p>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
{new Date(run.startedAt).toLocaleString("zh-TW")} · {run.stepCount} 步
|
|
</p>
|
|
{run.lastStep && (
|
|
<p className="mt-1 truncate text-xs text-muted-foreground">{run.lastStep}</p>
|
|
)}
|
|
</button>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">步驟詳情</CardTitle>
|
|
{selectedId && (
|
|
<CardDescription className="font-mono text-xs">{selectedId}</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!detail ? (
|
|
<p className="text-sm text-muted-foreground">選一筆紀錄查看截圖</p>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{detail.run.steps.map((step) => (
|
|
<div key={`${step.at}-${step.step}`} className="rounded-lg border p-3">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<p className="text-sm font-semibold">{step.step}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{new Date(step.at).toLocaleTimeString("zh-TW")}
|
|
</p>
|
|
</div>
|
|
{step.url && (
|
|
<a
|
|
href={step.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground underline"
|
|
>
|
|
<ExternalLink className="h-3 w-3" />
|
|
{step.url}
|
|
</a>
|
|
)}
|
|
{step.screenshot && (
|
|
<Image
|
|
src={`/api/debug/runs/${selectedId}/${step.screenshot}`}
|
|
alt={step.step}
|
|
width={1200}
|
|
height={800}
|
|
unoptimized
|
|
className="mt-3 max-h-[420px] w-full rounded-md border object-contain bg-background"
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|