Add YouTube transcripts tab to Mission Control
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user