584 lines
24 KiB
TypeScript
584 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import Link from "next/link";
|
|
import Card from "@/components/ui/Card";
|
|
|
|
interface MarketData {
|
|
btc?: string;
|
|
eth?: string;
|
|
sol?: string;
|
|
btcChange?: string;
|
|
ethChange?: string;
|
|
solChange?: string;
|
|
}
|
|
|
|
interface MorningBrief {
|
|
id: number;
|
|
date: string;
|
|
location: string;
|
|
weather: string;
|
|
marketData: MarketData;
|
|
aiNewsHeadlines: string[];
|
|
skillsToLearn: string[];
|
|
euAiActStatus: string;
|
|
sitementeStatus: { serverUptime?: string; demoPages?: string; leadStatus?: string };
|
|
warmLeads: { name: string; business: string; note: string }[];
|
|
dayPriorities: string[];
|
|
skippedTasks: string[];
|
|
autoExecutionSummary: string;
|
|
}
|
|
|
|
interface CompletedTask {
|
|
task: string;
|
|
completed: boolean;
|
|
}
|
|
|
|
interface ProjectProgress {
|
|
project: string;
|
|
progress: number;
|
|
}
|
|
|
|
interface EodBrief {
|
|
id: number;
|
|
date: string;
|
|
marketUpdate: MarketData;
|
|
completedTasks: CompletedTask[];
|
|
tomorrowPriorities: string[];
|
|
councilFeedback: Record<string, string>;
|
|
projectProgress: ProjectProgress[];
|
|
}
|
|
|
|
interface Bug {
|
|
description: string;
|
|
severity: string;
|
|
status: string;
|
|
}
|
|
|
|
interface AmunSession {
|
|
id: number;
|
|
date: string;
|
|
testsRun: string[];
|
|
bugsFound: Bug[];
|
|
codeQualityScore: number;
|
|
recommendations: string[];
|
|
}
|
|
|
|
function changeColor(val?: string) {
|
|
if (!val) return "text-slate-500";
|
|
const n = parseFloat(val);
|
|
if (isNaN(n)) return "text-slate-500";
|
|
return n >= 0 ? "text-emerald-400" : "text-red-400";
|
|
}
|
|
|
|
function progressColor(p: number) {
|
|
if (p >= 80) return "bg-emerald-500";
|
|
if (p >= 50) return "bg-amber-500";
|
|
if (p >= 25) return "bg-orange-500";
|
|
return "bg-red-500";
|
|
}
|
|
|
|
function buildSnapshot(morning?: MorningBrief, eod?: EodBrief, amun?: AmunSession): string {
|
|
const parts: string[] = [];
|
|
|
|
if (morning) {
|
|
const date = new Date(morning.date + "T12:00:00").toLocaleDateString("en-GB", {
|
|
weekday: "long", day: "numeric", month: "long",
|
|
});
|
|
parts.push(`📅 **${date}** — ${morning.location}, ${morning.weather}.`);
|
|
|
|
const md = morning.marketData;
|
|
if (md?.btc) {
|
|
const btcStr = md.btcChange
|
|
? `BTC at ${md.btc} (${parseFloat(md.btcChange) >= 0 ? "+" : ""}${md.btcChange}%)`
|
|
: `BTC at ${md.btc}`;
|
|
const ethStr = md.eth ? `, ETH at ${md.eth}` : "";
|
|
const solStr = md.sol ? `, SOL at ${md.sol}` : "";
|
|
parts.push(`📈 Markets: ${btcStr}${ethStr}${solStr}.`);
|
|
}
|
|
|
|
const headlines = morning.aiNewsHeadlines?.filter((h) => h.trim()) || [];
|
|
if (headlines.length > 0) {
|
|
parts.push(`🤖 AI News: ${headlines.slice(0, 2).join("; ")}.`);
|
|
}
|
|
|
|
const priorities = morning.dayPriorities?.filter((p) => p.trim()) || [];
|
|
if (priorities.length > 0) {
|
|
parts.push(`⚡ ${priorities.length} priorities set. Top: "${priorities[0]}".`);
|
|
}
|
|
|
|
const skipped = morning.skippedTasks?.filter((t) => t.trim()) || [];
|
|
if (skipped.length > 0) {
|
|
parts.push(`⏭ ${skipped.length} task(s) carried over from yesterday.`);
|
|
}
|
|
|
|
if (morning.autoExecutionSummary?.trim()) {
|
|
parts.push(`🦅 Horus auto-executing: ${morning.autoExecutionSummary.slice(0, 120)}${morning.autoExecutionSummary.length > 120 ? "…" : ""}`);
|
|
}
|
|
}
|
|
|
|
if (eod) {
|
|
const completed = eod.completedTasks?.filter((t) => t.completed) || [];
|
|
const total = eod.completedTasks?.length || 0;
|
|
if (total > 0) {
|
|
parts.push(`✅ EOD: ${completed.length}/${total} tasks completed.`);
|
|
}
|
|
|
|
const cf = eod.councilFeedback || {};
|
|
if (cf.horus) parts.push(`🦅 Horus: "${cf.horus}"`);
|
|
if (cf.amun) parts.push(`⚙️ Amun: "${cf.amun}"`);
|
|
}
|
|
|
|
if (amun) {
|
|
const openBugs = amun.bugsFound?.filter((b) => b.status === "open") || [];
|
|
const tests = amun.testsRun?.filter((t) => t.trim()) || [];
|
|
parts.push(
|
|
`🧪 Amun: ${tests.length} test(s) run, ${openBugs.length} open bug(s), code quality ${amun.codeQualityScore}%.`
|
|
);
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return "No data logged yet. Start by filing a Morning Brief.";
|
|
}
|
|
|
|
return parts.join(" ");
|
|
}
|
|
|
|
export default function MissionControlPage() {
|
|
const [morning, setMorning] = useState<MorningBrief | null>(null);
|
|
const [eod, setEod] = useState<EodBrief | null>(null);
|
|
const [amun, setAmun] = useState<AmunSession | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadLatest = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [mRes, eRes, aRes] = await Promise.all([
|
|
fetch("/api/morning?limit=1"),
|
|
fetch("/api/eod?limit=1"),
|
|
fetch("/api/amun?limit=1"),
|
|
]);
|
|
const [mData, eData, aData] = await Promise.all([mRes.json(), eRes.json(), aRes.json()]);
|
|
|
|
const parseJson = <T,>(val: unknown, fallback: T): T => {
|
|
if (typeof val === "string") {
|
|
try { return JSON.parse(val) as T; } catch { return fallback; }
|
|
}
|
|
return (val as T) ?? fallback;
|
|
};
|
|
|
|
if (Array.isArray(mData) && mData[0]) {
|
|
const m = mData[0];
|
|
setMorning({
|
|
...m,
|
|
marketData: parseJson(m.marketData, {}),
|
|
aiNewsHeadlines: parseJson(m.aiNewsHeadlines, []),
|
|
skillsToLearn: parseJson(m.skillsToLearn, []),
|
|
sitementeStatus: parseJson(m.sitementeStatus, {}),
|
|
warmLeads: parseJson(m.warmLeads, []),
|
|
dayPriorities: parseJson(m.dayPriorities, []),
|
|
skippedTasks: parseJson(m.skippedTasks, []),
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(eData) && eData[0]) {
|
|
const e = eData[0];
|
|
setEod({
|
|
...e,
|
|
marketUpdate: parseJson(e.marketUpdate, {}),
|
|
completedTasks: parseJson(e.completedTasks, []),
|
|
tomorrowPriorities: parseJson(e.tomorrowPriorities, []),
|
|
councilFeedback: parseJson(e.councilFeedback, {}),
|
|
projectProgress: parseJson(e.projectProgress, []),
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(aData) && aData[0]) {
|
|
const a = aData[0];
|
|
setAmun({
|
|
...a,
|
|
testsRun: parseJson(a.testsRun, []),
|
|
bugsFound: parseJson(a.bugsFound, []),
|
|
recommendations: parseJson(a.recommendations, []),
|
|
});
|
|
}
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadLatest();
|
|
}, [loadLatest]);
|
|
|
|
const snapshot = buildSnapshot(morning ?? undefined, eod ?? undefined, amun ?? undefined);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6 flex items-center justify-center min-h-screen">
|
|
<p className="text-slate-600">Loading Mission Control…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const noData = !morning && !eod && !amun;
|
|
|
|
return (
|
|
<div className="p-6 max-w-4xl mx-auto">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
|
<span className="text-amber-400">▶</span> Horus Mission Control
|
|
</h1>
|
|
<p className="text-sm text-slate-500 mt-0.5">Latest intelligence snapshot across all agents</p>
|
|
</div>
|
|
<button
|
|
onClick={loadLatest}
|
|
className="px-3 py-1.5 text-xs text-slate-400 border border-[#2a2d3a] rounded-lg hover:border-amber-500/30 hover:text-amber-400 transition-colors"
|
|
>
|
|
↻ Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{noData ? (
|
|
<div className="text-center py-16">
|
|
<p className="text-slate-500 mb-4">No briefs logged yet.</p>
|
|
<div className="flex justify-center gap-3">
|
|
<Link href="/morning" className="px-4 py-2 bg-amber-500/10 border border-amber-500/30 text-amber-400 rounded-lg text-sm hover:bg-amber-500/20 transition-colors">
|
|
☀ Log Morning Brief
|
|
</Link>
|
|
<Link href="/eod" className="px-4 py-2 bg-blue-500/10 border border-blue-500/30 text-blue-400 rounded-lg text-sm hover:bg-blue-500/20 transition-colors">
|
|
🌙 Log EOD Brief
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{/* Today Snapshot */}
|
|
<div className="bg-gradient-to-r from-amber-500/5 to-orange-500/5 border border-amber-500/20 rounded-xl p-5">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="w-6 h-6 rounded-md bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-black font-bold text-xs">H</div>
|
|
<h2 className="text-sm font-semibold text-amber-300">Today Snapshot — Horus Narrative</h2>
|
|
</div>
|
|
<p className="text-sm text-slate-300 leading-7 break-words whitespace-pre-line">{snapshot.replace(/ (📅|📈|🤖|⚡|⏭|🦅|✅|🧪)/g, "\n$1")}</p>
|
|
</div>
|
|
|
|
{/* Morning Brief Summary */}
|
|
{morning && (
|
|
<>
|
|
<div className="flex items-center gap-3 pt-1">
|
|
<span className="text-xs font-semibold text-amber-400 uppercase tracking-widest">Morning Brief</span>
|
|
<div className="flex-1 h-px bg-amber-500/15" />
|
|
<span className="text-xs text-slate-600">{morning.date}</span>
|
|
</div>
|
|
<Card title="☀ Morning Brief" subtitle={morning.date} accent="amber">
|
|
<div className="space-y-4">
|
|
{/* Location & Weather */}
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<span className="text-slate-400">📍 {morning.location}</span>
|
|
<span className="text-slate-400">🌤 {morning.weather}</span>
|
|
<span
|
|
className={`px-2 py-0.5 rounded text-xs border ${
|
|
morning.euAiActStatus === "OK"
|
|
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
|
|
: morning.euAiActStatus === "Risk"
|
|
? "bg-red-500/10 border-red-500/30 text-red-400"
|
|
: "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
|
}`}
|
|
>
|
|
🇪🇺 {morning.euAiActStatus}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Market */}
|
|
{(morning.marketData?.btc || morning.marketData?.eth || morning.marketData?.sol) && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Market</p>
|
|
<div className="flex gap-4">
|
|
{(["btc", "eth", "sol"] as const).map((coin) => {
|
|
const price = morning.marketData[coin];
|
|
const change = morning.marketData[`${coin}Change` as keyof MarketData];
|
|
if (!price) return null;
|
|
return (
|
|
<div key={coin} className="flex items-center gap-1.5">
|
|
<span className="text-xs text-slate-500 uppercase font-mono">{coin}</span>
|
|
<span className="text-xs text-slate-300">{price}</span>
|
|
{change && (
|
|
<span className={`text-xs ${changeColor(change)}`}>
|
|
{parseFloat(change) >= 0 ? "+" : ""}{change}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* AI News */}
|
|
{morning.aiNewsHeadlines?.filter((h) => h.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">AI News</p>
|
|
<ul className="space-y-1">
|
|
{morning.aiNewsHeadlines.filter((h) => h.trim()).map((h, i) => (
|
|
<li key={i} className="text-xs text-slate-400 flex gap-2">
|
|
<span className="text-purple-500/50">•</span> {h}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Priorities */}
|
|
{morning.dayPriorities?.filter((p) => p.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Day Priorities</p>
|
|
<ol className="space-y-1">
|
|
{morning.dayPriorities.filter((p) => p.trim()).map((p, i) => (
|
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
|
<span className="text-amber-500/50 font-mono">{i + 1}.</span> {p}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
)}
|
|
|
|
{/* Skipped Tasks */}
|
|
{morning.skippedTasks?.filter((t) => t.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Skipped Tasks</p>
|
|
<ul className="space-y-1">
|
|
{morning.skippedTasks.filter((t) => t.trim()).map((t, i) => (
|
|
<li key={i} className="text-xs text-slate-500 flex gap-2">
|
|
<span className="text-slate-700">⏭</span> {t}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Auto-Execution */}
|
|
{morning.autoExecutionSummary?.trim() && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Auto-Execution</p>
|
|
<p className="text-xs text-amber-300/80 bg-amber-500/5 border border-amber-500/10 rounded-lg p-3">
|
|
🦅 {morning.autoExecutionSummary}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
{/* EOD Brief Summary */}
|
|
{eod && (
|
|
<>
|
|
<div className="flex items-center gap-3 pt-1">
|
|
<span className="text-xs font-semibold text-blue-400 uppercase tracking-widest">End-of-Day Brief</span>
|
|
<div className="flex-1 h-px bg-blue-500/15" />
|
|
<span className="text-xs text-slate-600">{eod.date}</span>
|
|
</div>
|
|
<Card title="🌙 End-of-Day Brief" subtitle={eod.date} accent="blue">
|
|
<div className="space-y-4">
|
|
{/* Market Update */}
|
|
{(eod.marketUpdate?.btc || eod.marketUpdate?.eth || eod.marketUpdate?.sol) && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Market Update</p>
|
|
<div className="flex gap-4">
|
|
{(["btc", "eth", "sol"] as const).map((coin) => {
|
|
const price = eod.marketUpdate[coin];
|
|
const change = eod.marketUpdate[`${coin}Change` as keyof MarketData];
|
|
if (!price) return null;
|
|
return (
|
|
<div key={coin} className="flex items-center gap-1.5">
|
|
<span className="text-xs text-slate-500 uppercase font-mono">{coin}</span>
|
|
<span className="text-xs text-slate-300">{price}</span>
|
|
{change && (
|
|
<span className={`text-xs ${changeColor(change)}`}>
|
|
{parseFloat(change) >= 0 ? "+" : ""}{change}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Completed Tasks */}
|
|
{eod.completedTasks?.filter((t) => t.task.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">
|
|
Completed Tasks ({eod.completedTasks.filter((t) => t.completed).length}/{eod.completedTasks.filter((t) => t.task.trim()).length})
|
|
</p>
|
|
<ul className="space-y-1">
|
|
{eod.completedTasks.filter((t) => t.task.trim()).map((t, i) => (
|
|
<li key={i} className={`text-xs flex gap-2 ${t.completed ? "text-slate-400 line-through" : "text-slate-300"}`}>
|
|
<span>{t.completed ? "✓" : "○"}</span> {t.task}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tomorrow Priorities */}
|
|
{eod.tomorrowPriorities?.filter((p) => p.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Tomorrow's Priorities</p>
|
|
<ol className="space-y-1">
|
|
{eod.tomorrowPriorities.filter((p) => p.trim()).map((p, i) => (
|
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
|
<span className="text-amber-500/50 font-mono">{i + 1}.</span> {p}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</div>
|
|
)}
|
|
|
|
{/* Council Feedback */}
|
|
{Object.keys(eod.councilFeedback || {}).filter((k) => eod.councilFeedback[k]?.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Council Feedback</p>
|
|
<div className="space-y-2">
|
|
{Object.entries(eod.councilFeedback).filter(([, v]) => v?.trim()).map(([agent, feedback]) => (
|
|
<div key={agent} className="flex gap-2">
|
|
<span className="text-xs text-slate-500 capitalize w-12 flex-shrink-0">
|
|
{agent === "horus" ? "🦅" : agent === "amun" ? "⚙️" : "🤖"} {agent}:
|
|
</span>
|
|
<span className="text-xs text-slate-300 italic">“{feedback}”</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Project Progress */}
|
|
{eod.projectProgress?.filter((p) => p.project.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Project Progress</p>
|
|
<div className="space-y-2">
|
|
{eod.projectProgress.filter((p) => p.project.trim()).map((proj) => (
|
|
<div key={proj.project}>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="text-xs text-slate-400">{proj.project}</span>
|
|
<span className="text-xs text-slate-500 font-mono">{proj.progress}%</span>
|
|
</div>
|
|
<div className="h-1.5 bg-[#0f1117] rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${progressColor(proj.progress)}`}
|
|
style={{ width: `${proj.progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
)}
|
|
|
|
{/* Amun Session Summary */}
|
|
{amun && (
|
|
<>
|
|
<div className="flex items-center gap-3 pt-1">
|
|
<span className="text-xs font-semibold text-purple-400 uppercase tracking-widest">Amun Dev Session</span>
|
|
<div className="flex-1 h-px bg-purple-500/15" />
|
|
<span className="text-xs text-slate-600">{amun.date}</span>
|
|
</div>
|
|
<Card title="⚙️ Amun Dev Session" subtitle={amun.date} accent="purple">
|
|
<div className="space-y-4">
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
|
<p className="text-xs text-slate-600 mb-1">Tests Run</p>
|
|
<p className="text-lg font-bold text-white">{amun.testsRun?.filter((t) => t.trim()).length || 0}</p>
|
|
</div>
|
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
|
<p className="text-xs text-slate-600 mb-1">Open Bugs</p>
|
|
<p className={`text-lg font-bold ${amun.bugsFound?.filter((b) => b.status === "open").length > 0 ? "text-red-400" : "text-emerald-400"}`}>
|
|
{amun.bugsFound?.filter((b) => b.status === "open").length || 0}
|
|
</p>
|
|
</div>
|
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
|
<p className="text-xs text-slate-600 mb-1">Code Quality</p>
|
|
<p className={`text-lg font-bold ${amun.codeQualityScore >= 80 ? "text-emerald-400" : amun.codeQualityScore >= 60 ? "text-amber-400" : "text-red-400"}`}>
|
|
{amun.codeQualityScore}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tests */}
|
|
{amun.testsRun?.filter((t) => t.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Tests</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{amun.testsRun.filter((t) => t.trim()).map((t, i) => (
|
|
<span key={i} className="px-2 py-0.5 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs rounded font-mono">
|
|
{t}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bugs */}
|
|
{amun.bugsFound?.length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Bugs Found</p>
|
|
<div className="space-y-1.5">
|
|
{amun.bugsFound.map((bug, i) => (
|
|
<div key={i} className="flex items-center gap-2">
|
|
<span
|
|
className={`px-1.5 py-0.5 rounded text-xs border ${
|
|
bug.severity === "critical"
|
|
? "text-red-400 bg-red-500/10 border-red-500/30"
|
|
: bug.severity === "high"
|
|
? "text-orange-400 bg-orange-500/10 border-orange-500/30"
|
|
: bug.severity === "medium"
|
|
? "text-amber-400 bg-amber-500/10 border-amber-500/30"
|
|
: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30"
|
|
}`}
|
|
>
|
|
{bug.severity}
|
|
</span>
|
|
<span className={`text-xs ${bug.status === "closed" ? "text-slate-600 line-through" : "text-slate-300"}`}>
|
|
{bug.description}
|
|
</span>
|
|
<span className={`ml-auto text-xs ${bug.status === "open" ? "text-red-400" : "text-emerald-400"}`}>
|
|
{bug.status}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recommendations */}
|
|
{amun.recommendations?.filter((r) => r.trim()).length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Recommendations</p>
|
|
<ul className="space-y-1">
|
|
{amun.recommendations.filter((r) => r.trim()).map((r, i) => (
|
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
|
<span className="text-amber-500/50">→</span> {r}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|