I mean, it's at a good place rn...

This commit is contained in:
Renn F
2026-04-20 15:10:54 +02:00
parent 0023c25d60
commit 8e201901c0
264 changed files with 36484 additions and 748 deletions
@@ -0,0 +1,100 @@
"use client";
import { Task, TaskStatus } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { AlertTriangle, Clock, ArrowRight } from "lucide-react";
import Link from "next/link";
interface ActiveBlockersPanelProps {
tasks: Task[] | undefined;
isLoading: boolean;
}
function formatDuration(date: string): string {
const start = new Date(date);
const now = new Date();
const diffMs = now.getTime() - start.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return "< 1h";
if (diffHours < 24) return `${diffHours}h`;
return `${diffDays}d`;
}
export function ActiveBlockersPanel({ tasks, isLoading }: ActiveBlockersPanelProps) {
// Filter blocked tasks and sort by how long they've been blocked
const blockedTasks = (tasks ?? [])
.filter((t) => t.status === TaskStatus.BLOCKED)
.sort((a, b) => {
const aTime = a.updated_at ? new Date(a.updated_at).getTime() : 0;
const bTime = b.updated_at ? new Date(b.updated_at).getTime() : 0;
return aTime - bTime; // Oldest first
})
.slice(0, 5);
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-500" />
Active Blockers
</CardTitle>
{blockedTasks.length > 0 && (
<Badge variant="destructive">{blockedTasks.length}</Badge>
)}
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
) : blockedTasks.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm">
<AlertTriangle className="h-8 w-8 mx-auto mb-2 opacity-50" />
No blocked tasks
</div>
) : (
<div className="space-y-3">
{blockedTasks.map((task) => (
<Link key={task.id} href={"/tasks/" + task.id}>
<div className="flex items-start gap-3 p-3 rounded-lg border border-red-200 bg-red-50 hover:bg-red-100 transition-colors">
<span className="text-lg">\uD83D\uDD34</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm truncate">
Task #{task.id.slice(0, 8)}
</span>
<Badge variant="outline" className="text-xs capitalize">
{task.team.replace(/_/g, " ")}
</Badge>
</div>
<p className="text-sm truncate">{task.title}</p>
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
Blocked for {formatDuration(task.updated_at ?? task.created_at)}
</div>
</div>
</div>
</Link>
))}
</div>
)}
<div className="mt-4 pt-3 border-t">
<Link href="/tasks?status=blocked">
<Button variant="ghost" size="sm" className="w-full">
View All Blocked
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
</Link>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,78 @@
"use client";
import { CheckCircle, Play, Pause, AlertTriangle, User, Clock } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils";
export interface Activity {
id: string;
agent_id: string;
action: string;
task_id?: string;
task_title?: string;
timestamp: string;
}
interface ActivityItemProps {
activity: Activity;
}
const actionIcons: Record<string, React.ReactNode> = {
completed: <CheckCircle className="h-4 w-4 text-green-500" />,
started: <Play className="h-4 w-4 text-blue-500" />,
paused: <Pause className="h-4 w-4 text-yellow-500" />,
blocked: <AlertTriangle className="h-4 w-4 text-red-500" />,
claimed: <User className="h-4 w-4 text-purple-500" />,
passed_qa: <CheckCircle className="h-4 w-4 text-green-500" />,
failed_qa: <AlertTriangle className="h-4 w-4 text-red-500" />,
};
const actionLabels: Record<string, string> = {
completed: "completed",
started: "started",
paused: "paused",
blocked: "blocked on",
claimed: "claimed",
passed_qa: "passed QA on",
failed_qa: "failed QA on",
};
function formatTime(timestamp: string): string {
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
if (diffMins < 1) return "just now";
if (diffMins < 60) return `${diffMins}m ago`;
const diffHours = Math.floor(diffMins / 60);
if (diffHours < 24) return `${diffHours}h ago`;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
export function ActivityItem({ activity }: ActivityItemProps) {
const action = activity.action || "unknown";
const icon = actionIcons[action] || <Clock className="h-4 w-4" />;
const label = actionLabels[action] || action;
return (
<div className="flex items-start gap-3 py-2">
<div className="mt-0.5">{icon}</div>
<div className="flex-1 min-w-0">
<p className="text-sm">
<span className="font-medium">{getAgentDisplayName(activity.agent_id)}</span>
{" "}
<span className="text-muted-foreground">{label}</span>
{activity.task_title && (
<>
{" "}
<span className="font-medium">{activity.task_title}</span>
</>
)}
</p>
<span className="text-xs text-muted-foreground">
{activity.timestamp ? formatTime(activity.timestamp) : "N/A"}
</span>
</div>
</div>
);
}
@@ -0,0 +1,101 @@
"use client";
import { AuditorFlag, FlagSeverity } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Shield, ArrowRight } from "lucide-react";
import Link from "next/link";
interface AuditorAlertsPanelProps {
alerts: AuditorFlag[] | undefined;
isLoading: boolean;
}
const severityColors: Record<FlagSeverity, string> = {
[FlagSeverity.INFO]: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
[FlagSeverity.WARNING]: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
[FlagSeverity.URGENT]: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
};
const severityEmoji: Record<FlagSeverity, string> = {
[FlagSeverity.INFO]: "\uD83D\uDFE2",
[FlagSeverity.WARNING]: "\uD83D\uDFE1",
[FlagSeverity.URGENT]: "\uD83D\uDD34",
};
export function AuditorAlertsPanel({ alerts, isLoading }: AuditorAlertsPanelProps) {
// Filter to show only unresolved, sorted by severity
const unresolvedAlerts = (alerts ?? [])
.filter((a) => !a.resolved_at)
.sort((a, b) => {
const order: Record<FlagSeverity, number> = {
[FlagSeverity.URGENT]: 0,
[FlagSeverity.WARNING]: 1,
[FlagSeverity.INFO]: 2,
};
return order[a.severity] - order[b.severity];
})
.slice(0, 5);
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Shield className="h-5 w-5" />
Auditor Alerts
</CardTitle>
{unresolvedAlerts.length > 0 && (
<Badge variant="destructive">{unresolvedAlerts.length}</Badge>
)}
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-12" />
<Skeleton className="h-12" />
<Skeleton className="h-12" />
</div>
) : unresolvedAlerts.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm">
<Shield className="h-8 w-8 mx-auto mb-2 opacity-50" />
No active alerts
</div>
) : (
<div className="space-y-3">
{unresolvedAlerts.map((alert) => (
<div
key={alert.id}
className="flex items-start gap-3 p-3 rounded-lg border bg-muted/30"
>
<span className="text-lg">{severityEmoji[alert.severity]}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm truncate">{alert.title}</span>
<Badge className={severityColors[alert.severity] + " text-xs"}>
{alert.severity}
</Badge>
</div>
<p className="text-xs text-muted-foreground line-clamp-1">
{alert.description}
</p>
</div>
</div>
))}
</div>
)}
<div className="mt-4 pt-3 border-t">
<Link href="/auditor">
<Button variant="ghost" size="sm" className="w-full">
View All Flags
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
</Link>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,280 @@
"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { tasksApi } from "@/lib/api";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { CheckCircle2, XCircle, Clock, FileText, ExternalLink } from "lucide-react";
import Link from "next/link";
import type { Task } from "@/types";
import { toast } from "sonner";
interface CeoApprovalQueueProps {
className?: string;
}
export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
const queryClient = useQueryClient();
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [actionType, setActionType] = useState<"approve" | "reject" | null>(null);
const [notes, setNotes] = useState("");
// Fetch tasks awaiting CEO approval
const { data: tasks, isLoading } = useQuery({
queryKey: ["tasks", "awaiting-ceo-approval"],
queryFn: () => tasksApi.getAwaitingCeoApproval(),
refetchInterval: 30000, // Refresh every 30 seconds
});
// Approve mutation
const approveMutation = useMutation({
mutationFn: ({ taskId, notes }: { taskId: string; notes?: string }) =>
tasksApi.ceoApprove(taskId, notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] });
toast.success("Task approved and completed");
closeDialog();
},
onError: (error) => {
toast.error(`Failed to approve: ${error instanceof Error ? error.message : "Unknown error"}`);
},
});
// Reject mutation
const rejectMutation = useMutation({
mutationFn: ({ taskId, notes }: { taskId: string; notes: string }) =>
tasksApi.ceoReject(taskId, notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] });
toast.success("Task rejected and sent back for revision");
closeDialog();
},
onError: (error) => {
toast.error(`Failed to reject: ${error instanceof Error ? error.message : "Unknown error"}`);
},
});
const openDialog = (task: Task, action: "approve" | "reject") => {
setSelectedTask(task);
setActionType(action);
setNotes("");
};
const closeDialog = () => {
setSelectedTask(null);
setActionType(null);
setNotes("");
};
const handleConfirm = () => {
if (!selectedTask) return;
if (actionType === "approve") {
approveMutation.mutate({ taskId: selectedTask.id, notes: notes || undefined });
} else if (actionType === "reject") {
if (!notes.trim()) {
toast.error("Rejection reason is required");
return;
}
rejectMutation.mutate({ taskId: selectedTask.id, notes });
}
};
const getPriorityBadge = (priority: number) => {
const variants: Record<number, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
0: { label: "P0", variant: "destructive" },
1: { label: "P1", variant: "destructive" },
2: { label: "P2", variant: "secondary" },
3: { label: "P3", variant: "outline" },
};
const { label, variant } = variants[priority] || { label: `P${priority}`, variant: "outline" as const };
return <Badge variant={variant}>{label}</Badge>;
};
if (isLoading) {
return (
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
CEO Approval Queue
</CardTitle>
<CardDescription>Tasks awaiting your approval</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-20 w-full" />
))}
</div>
</CardContent>
</Card>
);
}
const pendingTasks = tasks || [];
return (
<>
<Card className={className}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
CEO Approval Queue
{pendingTasks.length > 0 && (
<Badge variant="secondary" className="ml-2">
{pendingTasks.length}
</Badge>
)}
</CardTitle>
<CardDescription>Tasks escalated for your final approval</CardDescription>
</CardHeader>
<CardContent>
{pendingTasks.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CheckCircle2 className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No tasks awaiting approval</p>
</div>
) : (
<div className="space-y-3">
{pendingTasks.map((task) => (
<div
key={task.id}
className="flex items-start justify-between p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{getPriorityBadge(task.priority)}
<Badge variant="outline">{task.team}</Badge>
</div>
<Link
href={`/tasks/${task.id}`}
className="font-medium hover:underline line-clamp-1"
>
{task.title}
</Link>
{task.quick_context && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{task.quick_context}
</p>
)}
</div>
<div className="flex items-center gap-2 ml-4 flex-shrink-0">
<Link href={`/tasks/${task.id}`}>
<Button variant="ghost" size="sm">
<FileText className="h-4 w-4" />
</Button>
</Link>
<Button
variant="outline"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() => openDialog(task, "reject")}
>
<XCircle className="h-4 w-4 mr-1" />
Reject
</Button>
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => openDialog(task, "approve")}
>
<CheckCircle2 className="h-4 w-4 mr-1" />
Approve
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Confirmation Dialog */}
<Dialog open={!!selectedTask && !!actionType} onOpenChange={() => closeDialog()}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{actionType === "approve" ? "Approve Task" : "Reject Task"}
</DialogTitle>
<DialogDescription>
{actionType === "approve"
? "This will complete the task and notify the team."
: "This will send the task back for revision."}
</DialogDescription>
</DialogHeader>
{selectedTask && (
<div className="py-4">
<div className="flex items-center gap-2 mb-2">
{getPriorityBadge(selectedTask.priority)}
<Badge variant="outline">{selectedTask.team}</Badge>
</div>
<p className="font-medium">{selectedTask.title}</p>
{selectedTask.description && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-3">
{selectedTask.description}
</p>
)}
<Link
href={`/tasks/${selectedTask.id}`}
target="_blank"
className="text-sm text-primary flex items-center gap-1 mt-2 hover:underline"
>
View full details <ExternalLink className="h-3 w-3" />
</Link>
</div>
)}
<div className="space-y-2">
<Label htmlFor="notes">
{actionType === "approve" ? "Notes (optional)" : "Reason for rejection (required)"}
</Label>
<Textarea
id="notes"
placeholder={
actionType === "approve"
? "Add any notes about this approval..."
: "Explain what needs to be fixed..."
}
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={closeDialog}>
Cancel
</Button>
<Button
onClick={handleConfirm}
disabled={approveMutation.isPending || rejectMutation.isPending}
className={actionType === "approve" ? "bg-green-600 hover:bg-green-700" : ""}
variant={actionType === "reject" ? "destructive" : "default"}
>
{approveMutation.isPending || rejectMutation.isPending
? "Processing..."
: actionType === "approve"
? "Approve & Complete"
: "Reject & Request Revision"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -0,0 +1,89 @@
"use client";
import { useCeoOverview, useAuditorFlags, useRecentActivity } from "@/hooks/use-dashboard";
import { useTasks } from "@/hooks/use-tasks";
import { TeamHealthCards } from "./team-health-cards";
import { KeyMetricsPanel } from "./key-metrics-panel";
import { AuditorAlertsPanel } from "./auditor-alerts-panel";
import { ActiveBlockersPanel } from "./active-blockers-panel";
import { RecentActivityFeed } from "./recent-activity-feed";
import { QuickActionsBar } from "./quick-actions-bar";
import { CeoApprovalQueue } from "./ceo-approval-queue";
import type { Activity } from "./activity-item";
import { Button } from "@/components/ui/button";
import { RefreshCw, Settings } from "lucide-react";
import Link from "next/link";
export function CommandCenter() {
const { data: overview, isLoading: loadingOverview, refetch: refetchOverview } = useCeoOverview();
const { data: flags, isLoading: loadingFlags } = useAuditorFlags({ resolved: false });
const { data: tasks, isLoading: loadingTasks } = useTasks();
const { data: activity, isLoading: loadingActivity } = useRecentActivity(24);
const handleRefresh = () => {
refetchOverview();
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">RoboCo Command Center</h1>
<p className="text-muted-foreground">
Complete visibility into all operations
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleRefresh}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Link href="/settings">
<Button variant="ghost" size="icon">
<Settings className="h-5 w-5" />
</Button>
</Link>
</div>
</div>
{/* Team Health */}
<section>
<h2 className="text-lg font-semibold mb-4">Team Health</h2>
<TeamHealthCards
teams={overview?.health_status}
isLoading={loadingOverview}
/>
</section>
{/* CEO Approval Queue - Your primary action item */}
<section>
<CeoApprovalQueue />
</section>
{/* Metrics and Alerts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<KeyMetricsPanel
metrics={overview?.key_metrics}
isLoading={loadingOverview}
/>
<AuditorAlertsPanel alerts={flags} isLoading={loadingFlags} />
</div>
{/* Blockers and Activity Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ActiveBlockersPanel tasks={tasks} isLoading={loadingTasks} />
<RecentActivityFeed
activities={activity as Activity[] | undefined}
isLoading={loadingActivity}
/>
</div>
{/* Quick Actions */}
<section className="pt-4 border-t">
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2>
<QuickActionsBar />
</section>
</div>
);
}
@@ -0,0 +1,38 @@
"use client";
interface HealthIndicatorProps {
status: "ok" | "slow" | "critical";
size?: "sm" | "md" | "lg";
}
const statusEmoji: Record<string, string> = {
ok: "\uD83D\uDFE2",
slow: "\uD83D\uDFE1",
critical: "\uD83D\uDD34",
};
const statusLabel: Record<string, string> = {
ok: "OK",
slow: "SLOW",
critical: "CRITICAL",
};
const statusColor: Record<string, string> = {
ok: "text-green-600",
slow: "text-yellow-600",
critical: "text-red-600",
};
export function HealthIndicator({ status, size = "md" }: HealthIndicatorProps) {
const sizeClasses = {
sm: "text-sm",
md: "text-base",
lg: "text-lg",
};
return (
<span className={`${sizeClasses[size]} ${statusColor[status]} font-medium`}>
{statusEmoji[status]} {statusLabel[status]}
</span>
);
}
+11
View File
@@ -0,0 +1,11 @@
export { CommandCenter } from "./command-center";
export { TeamHealthCards } from "./team-health-cards";
export { TeamHealthCard } from "./team-health-card";
export { KeyMetricsPanel } from "./key-metrics-panel";
export { AuditorAlertsPanel } from "./auditor-alerts-panel";
export { ActiveBlockersPanel } from "./active-blockers-panel";
export { RecentActivityFeed } from "./recent-activity-feed";
export { ActivityItem } from "./activity-item";
export { QuickActionsBar } from "./quick-actions-bar";
export { HealthIndicator } from "./health-indicator";
export { CeoApprovalQueue } from "./ceo-approval-queue";
@@ -0,0 +1,91 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { TrendingUp, Clock, CheckCircle, Users, BarChart3 } from "lucide-react";
interface KeyMetricsProps {
metrics: Record<string, unknown> | undefined;
isLoading: boolean;
}
interface MetricItem {
key: string;
label: string;
icon: React.ReactNode;
format?: (value: number) => string;
}
const METRIC_CONFIG: MetricItem[] = [
{
key: "velocity_24h",
label: "Velocity (24h)",
icon: <TrendingUp className="h-4 w-4" />,
format: (v) => `${v} tasks`,
},
{
key: "velocity_7d",
label: "Velocity (7d)",
icon: <BarChart3 className="h-4 w-4" />,
format: (v) => `${v} tasks`,
},
{
key: "completion_rate",
label: "Completion Rate",
icon: <CheckCircle className="h-4 w-4" />,
format: (v) => `${Math.round(v * 100)}%`,
},
{
key: "avg_time_to_done",
label: "Avg. Time to Done",
icon: <Clock className="h-4 w-4" />,
format: (v) => `${(typeof v === "number" ? v : 0).toFixed(1)}h`,
},
{
key: "active_agents",
label: "Active Agents",
icon: <Users className="h-4 w-4" />,
format: (v) => `${v}`,
},
];
export function KeyMetricsPanel({ metrics, isLoading }: KeyMetricsProps) {
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Key Metrics</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
{METRIC_CONFIG.map((m) => (
<Skeleton key={m.key} className="h-6" />
))}
</div>
) : (
<div className="space-y-3">
{METRIC_CONFIG.map((m) => {
const rawValue = metrics?.[m.key];
const value = typeof rawValue === "number" ? rawValue : null;
return (
<div key={m.key} className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{m.icon}
{m.label}
</div>
<span className="font-medium">
{value != null
? m.format
? m.format(value)
: value
: "-"}
</span>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,42 @@
"use client";
import { Button } from "@/components/ui/button";
import { CreateTaskDialog } from "@/components/tasks/create-task-dialog";
import { Users, Megaphone, BookOpen, Shield } from "lucide-react";
import Link from "next/link";
export function QuickActionsBar() {
return (
<div className="flex flex-wrap gap-3">
<CreateTaskDialog />
<Link href="/agents">
<Button variant="outline">
<Users className="h-4 w-4 mr-2" />
Spawn Agent
</Button>
</Link>
<Link href="/communications">
<Button variant="outline">
<Megaphone className="h-4 w-4 mr-2" />
Broadcast Message
</Button>
</Link>
<Link href="/journals">
<Button variant="outline">
<BookOpen className="h-4 w-4 mr-2" />
View Journals
</Button>
</Link>
<Link href="/auditor">
<Button variant="outline">
<Shield className="h-4 w-4 mr-2" />
Auditor Report
</Button>
</Link>
</div>
);
}
@@ -0,0 +1,57 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Activity as ActivityIcon, ArrowRight } from "lucide-react";
import { ActivityItem, Activity } from "./activity-item";
import Link from "next/link";
interface RecentActivityFeedProps {
activities: Activity[] | undefined;
isLoading: boolean;
}
export function RecentActivityFeed({ activities, isLoading }: RecentActivityFeedProps) {
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<ActivityIcon className="h-5 w-5" />
Recent Activity
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-12" />
))}
</div>
) : !activities || activities.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm">
<ActivityIcon className="h-8 w-8 mx-auto mb-2 opacity-50" />
No recent activity
</div>
) : (
<ScrollArea className="h-[280px] pr-4">
<div className="divide-y">
{activities.slice(0, 10).map((activity) => (
<ActivityItem key={activity.id} activity={activity} />
))}
</div>
</ScrollArea>
)}
<div className="mt-4 pt-3 border-t">
<Link href="/notifications">
<Button variant="ghost" size="sm" className="w-full">
View Full Activity
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
</Link>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,67 @@
"use client";
import { TeamHealth } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { HealthIndicator } from "./health-indicator";
import { Users, AlertTriangle, TrendingUp } from "lucide-react";
interface TeamHealthCardProps {
health: TeamHealth;
}
export function TeamHealthCard({ health }: TeamHealthCardProps) {
const teamName = health.team.replace(/_/g, " ");
return (
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-lg capitalize">{teamName}</CardTitle>
<HealthIndicator status={health.status} size="sm" />
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Active Tasks */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Users className="h-4 w-4" />
Active
</div>
<span className="font-medium">{health.active_tasks}</span>
</div>
{/* Blocked Tasks */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<AlertTriangle className="h-4 w-4" />
Blocked
</div>
<span className={`font-medium ${health.blocked_tasks > 0 ? "text-red-600" : ""}`}>
{health.blocked_tasks}
</span>
</div>
{/* Completed This Week */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<TrendingUp className="h-4 w-4" />
Completed (7d)
</div>
<span className="font-medium">{health.completed_this_week}</span>
</div>
{/* Blocked Ratio */}
{health.blocked_ratio > 0 && (
<div className="pt-2 border-t">
<Badge
variant={health.blocked_ratio > 0.3 ? "destructive" : health.blocked_ratio > 0.1 ? "secondary" : "outline"}
>
{Math.round(health.blocked_ratio * 100)}% blocked
</Badge>
</div>
)}
</CardContent>
</Card>
);
}
@@ -0,0 +1,38 @@
"use client";
import { TeamHealth } from "@/types";
import { TeamHealthCard } from "./team-health-card";
import { Skeleton } from "@/components/ui/skeleton";
interface TeamHealthCardsProps {
teams: TeamHealth[] | undefined;
isLoading: boolean;
}
export function TeamHealthCards({ teams, isLoading }: TeamHealthCardsProps) {
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-48" />
))}
</div>
);
}
if (!teams || teams.length === 0) {
return (
<div className="text-center py-8 text-muted-foreground">
No team health data available
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{teams.map((health) => (
<TeamHealthCard key={health.team} health={health} />
))}
</div>
);
}