import { listen } from "@tauri-apps/api/event"; import { useEffect, useState } from "react"; import { useParams, Link, useNavigate } from "react-router-dom"; import { getProject, deleteProject, listTrackers, listProcessedTickets, getProjectThroughput, } from "../../lib/api"; import type { Project, WatchedTracker, ProcessedTicket, ProjectThroughputStats, } from "../../lib/types"; import TrackerList from "../trackers/TrackerList"; import ConfirmModal from "../ui/ConfirmModal"; type ActivityLevel = "info" | "success" | "error"; interface ActivityItem { id: string; level: ActivityLevel; message: string; at: string; } interface PollingPayload { project_id: string; tracker_id: string; tracker_label: string; source: "manual" | "scheduled"; new_tickets_count?: number; error?: string; count?: number; } interface TicketProcessingPayload { project_id: string; ticket_id: string; artifact_id: number; step?: "analyst" | "developer"; error?: string; } export default function ProjectDashboard() { const { projectId } = useParams(); const navigate = useNavigate(); const [project, setProject] = useState(null); const [trackers, setTrackers] = useState([]); const [tickets, setTickets] = useState([]); const [throughput, setThroughput] = useState(null); const [activity, setActivity] = useState([]); const [activePolls, setActivePolls] = useState>({}); const [activeAgents, setActiveAgents] = useState>({}); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); function appendActivity(level: ActivityLevel, message: string) { const item: ActivityItem = { id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, level, message, at: new Date().toISOString(), }; setActivity((prev) => [item, ...prev].slice(0, 30)); } async function loadData() { if (!projectId) return; const [proj, trks, tkts, stats] = await Promise.all([ getProject(projectId), listTrackers(projectId), listProcessedTickets(projectId), getProjectThroughput(projectId), ]); setProject(proj); setTrackers(trks); setTickets(tkts); setThroughput(stats); } useEffect(() => { loadData(); }, [projectId]); useEffect(() => { if (!projectId) return; let unlistenFns: Array<() => void> = []; let cancelled = false; async function setup() { try { const [unlistenPollingStarted, unlistenPollingFinished, unlistenPollingError, unlistenTicketsDetected, unlistenTicketStarted, unlistenTicketDone, unlistenTicketError] = await Promise.all([ listen("polling-started", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActivePolls((prev) => ({ ...prev, [payload.tracker_id]: payload.tracker_label, })); appendActivity( "info", `Polling ${payload.source === "manual" ? "manuel" : "auto"} lancé sur "${payload.tracker_label}".` ); }), listen("polling-finished", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActivePolls((prev) => { const next = { ...prev }; delete next[payload.tracker_id]; return next; }); appendActivity( "success", `Polling terminé sur "${payload.tracker_label}" (${payload.new_tickets_count ?? 0} nouveau(x) ticket(s)).` ); void loadData(); }), listen("polling-error", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActivePolls((prev) => { const next = { ...prev }; delete next[payload.tracker_id]; return next; }); appendActivity( "error", `Erreur de polling sur "${payload.tracker_label}": ${payload.error ?? "erreur inconnue"}.` ); }), listen("new-tickets-detected", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; appendActivity( "success", `${payload.count ?? 0} nouveau(x) ticket(s) détecté(s) dans "${payload.tracker_label}".` ); void loadData(); }), listen("ticket-processing-started", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActiveAgents((prev) => ({ ...prev, [payload.ticket_id]: payload.step ?? "processing", })); appendActivity( "info", `Agent ${payload.step ?? "processing"} lancé pour le ticket #${payload.artifact_id}.` ); void loadData(); }), listen("ticket-processing-done", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActiveAgents((prev) => { const next = { ...prev }; delete next[payload.ticket_id]; return next; }); appendActivity( "success", `Pipeline agent terminé pour le ticket #${payload.artifact_id}.` ); void loadData(); }), listen("ticket-processing-error", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; setActiveAgents((prev) => { const next = { ...prev }; delete next[payload.ticket_id]; return next; }); appendActivity( "error", `Erreur agent sur le ticket #${payload.artifact_id}: ${payload.error ?? "erreur inconnue"}.` ); void loadData(); }), ]); if (cancelled) { unlistenPollingStarted(); unlistenPollingFinished(); unlistenPollingError(); unlistenTicketsDetected(); unlistenTicketStarted(); unlistenTicketDone(); unlistenTicketError(); return; } unlistenFns = [ unlistenPollingStarted, unlistenPollingFinished, unlistenPollingError, unlistenTicketsDetected, unlistenTicketStarted, unlistenTicketDone, unlistenTicketError, ]; } catch (err) { appendActivity( "error", `Écoute des événements live indisponible: ${err instanceof Error ? err.message : String(err)}` ); } } void setup(); return () => { cancelled = true; for (const unlisten of unlistenFns) { unlisten(); } }; }, [projectId]); async function handleDelete() { if (!projectId) return; setIsDeleteModalOpen(false); await deleteProject(projectId); window.dispatchEvent(new Event("orchai:refresh-projects")); navigate("/"); } function statusBadgeClass(status: string): string { switch (status) { case "Pending": return "bg-yellow-100 text-yellow-700"; case "Done": return "bg-green-100 text-green-700"; case "Error": return "bg-red-100 text-red-700"; default: return "bg-blue-100 text-blue-700"; } } function formatLeadTime(seconds: number | null): string { if (seconds === null || Number.isNaN(seconds)) { return "—"; } if (seconds < 60) { return `${Math.round(seconds)} s`; } if (seconds < 3600) { return `${Math.round(seconds / 60)} min`; } if (seconds < 86400) { return `${(seconds / 3600).toFixed(1)} h`; } return `${(seconds / 86400).toFixed(1)} j`; } if (!project) { return
Loading...
; } const recentTickets = tickets.slice(-10).reverse(); const activePollList = Object.entries(activePolls); const activeAgentList = Object.values(activeAgents); const done24h = throughput?.done_last_24h ?? 0; const error24h = throughput?.error_last_24h ?? 0; const resolved24h = done24h + error24h; const errorRate24h = resolved24h > 0 ? `${Math.round((error24h / resolved24h) * 100)}%` : "—"; return (

{project.name}

Edit
Path: {project.path}
{project.cloned_from && (
Cloned from: {project.cloned_from}
)}
Base branch: {project.base_branch}
Created: {new Date(project.created_at).toLocaleDateString()}

Throughput & santé (24h)

Backlog
{throughput?.backlog_count ?? 0}
Tickets encore en cours.
Done
{done24h}
Tickets finalisés en 24h.
Error
{error24h}
Taux d’erreur 24h: {errorRate24h}
Lead time moyen
{formatLeadTime(throughput?.avg_lead_time_seconds ?? null)}
Sur les tickets clos en 24h.

Orchestrateur IA

Modules
Active ou désactive les modules IA du projet.
Live agent
Discussion live avec un agent dans le contexte du repo.
Tâches
Crée une file de tâches traitées par des agents pré-définis.

Watched Trackers

setIsDeleteModalOpen(false)} onConfirm={() => void handleDelete()} />

Live Activity

Polling en cours: {activePollList.length}
Agents actifs: {activeAgentList.length}
{(activePollList.length > 0 || activeAgentList.length > 0) && (
{activePollList.map(([trackerId, label]) => (
Polling: {label}
))} {activeAgentList.map((step, i) => (
Agent: {step}
))}
)} {activity.length === 0 ? (
No live activity yet.
) : (
{activity.map((item) => (
{item.message}
{new Date(item.at).toLocaleTimeString()}
))}
)}

Recent Tickets

{tickets.length > 0 && ( View all ({tickets.length}) )}
{recentTickets.length === 0 ? (
No tickets processed yet.
) : (
{recentTickets.map((ticket) => (
#{ticket.artifact_id} {ticket.artifact_title}
{ticket.status} ))}
)}
); }