"use client"; /** * Cross-cutting trace view. * * One place to reconstruct what the platform did: every run across the * organization, its step timeline, the tools it called, and the audit stream * beside it. * * Two deliberate constraints: * * - The task list is capped before fanning out. There is no "list all runs for * an organization" endpoint, so we read runs through their owning task; an * uncapped fan-out would turn one page load into hundreds of requests. * - No model or provider identity is rendered. Those never leave the platform * owner's console. */ import Link from "next/link"; import * as React from "react"; import type { AuditEvent, Run, RunDetail } from "@agy/contracts"; import { Badge, Button, Card, CardHint, CardTitle, EmptyState, ErrorState, LoadingState, Stat, Timeline, } from "@agy/ui"; import { ApiError, api } from "@/lib/api"; import { useSession } from "@/lib/session"; /** How many tasks to fan out into. Bounds the request count. */ const TASK_SCAN_LIMIT = 12; const RUN_TONES: Record = { queued: "neutral", planning: "info", running: "info", tool_calling: "info", sandbox_running: "info", waiting_approval: "warning", completed: "success", failed: "danger", cancelled: "neutral", timed_out: "danger", }; const AUDIT_LABELS: Record = { "auth.login": "ورود کاربر", "auth.signup": "ثبت‌نام کاربر", "org.create": "ساخت سازمان", "org.member_invite": "دعوت عضو", "org.member_update": "تغییر نقش عضو", "org.member_remove": "حذف عضو", "project.create": "ساخت پروژه", "project.repo_ensure": "ساخت مخزن", "project.preview_domain_create": "ساخت دامنهٔ تست", "project.preview_domain_delete": "حذف دامنهٔ تست", "employee.create": "ساخت کارمند", "employee.update": "ویرایش کارمند", "employee.prompt_version": "نسخهٔ پرامپت جدید", "employee.tool_permission": "تغییر دسترسی ابزار", "task.create": "ساخت کار", "run.start": "شروع اجرا", "run.cancel": "لغو اجرا", "approval.decide": "تصمیم تأیید", "billing.add_credit": "افزایش اعتبار", "billing.checkout": "شروع پرداخت", "billing.settle": "تسویهٔ پرداخت", "sandbox.workspace_create": "ساخت محیط ایزوله", "sandbox.destroy": "حذف محیط ایزوله", "sandbox.command": "اجرای دستور در sandbox", }; function auditLabel(action: string): string { return AUDIT_LABELS[action] ?? action; } function formatDuration(run: Run): string { if (!run.started_at) return "—"; const end = run.finished_at ? new Date(run.finished_at).getTime() : Date.now(); const ms = end - new Date(run.started_at).getTime(); if (ms < 1000) return `${ms}ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.round(ms / 60_000)}m`; } interface RunRow { run: Run; taskId: string; taskTitle: string; } export default function TracesPage() { const { activeOrganization } = useSession(); const orgId = activeOrganization?.id ?? null; const [rows, setRows] = React.useState([]); const [audit, setAudit] = React.useState([]); const [selected, setSelected] = React.useState(null); const [selectedId, setSelectedId] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [detailLoading, setDetailLoading] = React.useState(false); const load = React.useCallback(async () => { if (!orgId) { setLoading(false); return; } setLoading(true); setError(null); try { const [tasks, auditEvents] = await Promise.all([ api.listTasks(orgId), api.listAudit(orgId), ]); setAudit(auditEvents); const recent = tasks.slice(0, TASK_SCAN_LIMIT); const details = await Promise.all( recent.map(async (task) => { try { return { task, detail: await api.getTask(orgId, task.id) }; } catch { // One unreadable task must not blank the whole trace view. return { task, detail: null }; } }), ); const collected: RunRow[] = []; for (const { task, detail } of details) { for (const run of detail?.runs ?? []) { collected.push({ run, taskId: task.id, taskTitle: task.title }); } } collected.sort( (a, b) => new Date(b.run.created_at).getTime() - new Date(a.run.created_at).getTime(), ); setRows(collected); } catch (err) { setError(err instanceof ApiError ? err.message : "دریافت رد‌یابی ناموفق بود."); } finally { setLoading(false); } }, [orgId]); React.useEffect(() => { void load(); }, [load]); const onSelect = async (runId: string) => { if (!orgId) return; setSelectedId(runId); setDetailLoading(true); try { setSelected(await api.getRun(orgId, runId)); } catch (err) { setError(err instanceof ApiError ? err.message : "دریافت جزئیات اجرا ناموفق بود."); } finally { setDetailLoading(false); } }; if (!activeOrganization) { return ( ); } if (loading) return ; if (error) return ; const failed = rows.filter((r) => r.run.status === "failed" || r.run.status === "timed_out").length; return (

ردیابی و رویدادها

هر اجرا با خط زمانی گام‌ها، ابزارهای فراخوانی‌شده و رویدادهای audit. هویت مدل و ارائه‌دهنده در این نما نمایش داده نمی‌شود.

0 ? "danger" : "neutral"} />
اجراها جدیدترین اجراها در همهٔ کارها. {rows.length === 0 ? (

هنوز اجرایی ثبت نشده است.

) : (
    {rows.map(({ run, taskId, taskTitle }) => (
  • ))}
)}
{detailLoading && } {selected && !detailLoading && (
خط زمانی اجرا {selected.id}
{selected.status}
{selected.steps.length === 0 ? (

گامی ثبت نشده است.

) : ( ({ id: step.id, title: step.title || step.kind, meta: new Date(step.created_at).toLocaleString("fa-IR"), tone: step.kind === "error" ? ("danger" as const) : step.kind === "artifact" ? ("success" as const) : ("info" as const), }))} /> )}
{selected.error && (
)} {selected.tool_calls.length > 0 && (

ابزارها

    {selected.tool_calls.map((call) => (
  • {call.tool_key}
    {call.requires_approval && نیازمند تأیید} {call.status}
  • ))}
)} {selected.artifacts.length > 0 && (

خروجی‌ها

    {selected.artifacts.map((artifact) => (
  • {artifact.name} ·{" "} {artifact.kind} ·{" "} {artifact.size_bytes} بایت
  • ))}
)}
)} {!selected && !detailLoading && rows.length > 0 && ( )}
رویدادهای audit هر اقدام حساس با کنشگر و هدف ثبت می‌شود.
{audit.length === 0 ? (

رویدادی ثبت نشده است.

) : ( ({ id: event.id, title: auditLabel(event.action), meta: `${new Date(event.created_at).toLocaleString("fa-IR")} · ${ event.target_type }`, tone: event.action.includes("remove") || event.action.includes("cancel") ? ("danger" as const) : ("info" as const), }))} /> )}
); }