Files
sitemente/app/mission/page.tsx
T

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&apos;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">&ldquo;{feedback}&rdquo;</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>
);
}