haixunMaster/app/(dashboard)/debug/page.tsx

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>
);
}