Add YouTube transcripts tab to Mission Control

This commit is contained in:
Horus
2026-02-27 15:00:38 +01:00
parent 0a4853bcda
commit d7cd81b293
18 changed files with 3517 additions and 12 deletions
+440
View File
@@ -0,0 +1,440 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Card from "@/components/ui/Card";
const today = () => new Date().toISOString().split("T")[0];
type Severity = "low" | "medium" | "high" | "critical";
type BugStatus = "open" | "closed";
interface Bug {
description: string;
severity: Severity;
status: BugStatus;
}
interface FormState {
date: string;
testsRun: string[];
bugsFound: Bug[];
codeQualityScore: number;
recommendations: string[];
}
const defaultForm: FormState = {
date: today(),
testsRun: [""],
bugsFound: [],
codeQualityScore: 0,
recommendations: [""],
};
const severityColors: Record<Severity, string> = {
low: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30",
medium: "text-amber-400 bg-amber-500/10 border-amber-500/30",
high: "text-orange-400 bg-orange-500/10 border-orange-500/30",
critical: "text-red-400 bg-red-500/10 border-red-500/30",
};
export default function AmunPage() {
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadingDate, setLoadingDate] = useState(false);
const [history, setHistory] = useState<FormState[]>([]);
const loadSessionForDate = useCallback(async (date: string) => {
setLoadingDate(true);
try {
const res = await fetch(`/api/amun?date=${date}`);
const data = await res.json();
if (data) {
setForm({
date: data.date,
testsRun: typeof data.testsRun === "string" ? JSON.parse(data.testsRun) : (data.testsRun || [""]),
bugsFound: typeof data.bugsFound === "string" ? JSON.parse(data.bugsFound) : (data.bugsFound || []),
codeQualityScore: data.codeQualityScore || 0,
recommendations: typeof data.recommendations === "string" ? JSON.parse(data.recommendations) : (data.recommendations || [""]),
});
} else {
setForm({ ...defaultForm, date });
}
} catch {
setForm({ ...defaultForm, date });
} finally {
setLoadingDate(false);
}
}, []);
const loadHistory = useCallback(async () => {
try {
const res = await fetch("/api/amun?limit=10");
const data = await res.json();
if (Array.isArray(data)) {
setHistory(
data.map((s) => ({
date: s.date,
testsRun: typeof s.testsRun === "string" ? JSON.parse(s.testsRun) : (s.testsRun || []),
bugsFound: typeof s.bugsFound === "string" ? JSON.parse(s.bugsFound) : (s.bugsFound || []),
codeQualityScore: s.codeQualityScore || 0,
recommendations: typeof s.recommendations === "string" ? JSON.parse(s.recommendations) : (s.recommendations || []),
}))
);
}
} catch {
// ignore
}
}, []);
useEffect(() => {
loadSessionForDate(today());
loadHistory();
}, [loadSessionForDate, loadHistory]);
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (form.codeQualityScore < 0 || form.codeQualityScore > 100) {
errs.codeQualityScore = "Score must be between 0 and 100";
}
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const res = await fetch("/api/amun", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const err = await res.json();
setErrors({ submit: err.error || "Failed to save" });
} else {
setSaved(true);
setTimeout(() => setSaved(false), 3000);
loadHistory();
}
} catch {
setErrors({ submit: "Network error. Please try again." });
} finally {
setSaving(false);
}
};
const updateArrayItem = (field: "testsRun" | "recommendations", index: number, value: string) => {
setForm((prev) => {
const arr = [...prev[field]];
arr[index] = value;
return { ...prev, [field]: arr };
});
};
const addArrayItem = (field: "testsRun" | "recommendations") => {
setForm((prev) => ({ ...prev, [field]: [...prev[field], ""] }));
};
const removeArrayItem = (field: "testsRun" | "recommendations", index: number) => {
setForm((prev) => ({ ...prev, [field]: prev[field].filter((_, i) => i !== index) }));
};
const addBug = () => {
setForm((prev) => ({
...prev,
bugsFound: [...prev.bugsFound, { description: "", severity: "medium", status: "open" }],
}));
};
const updateBug = (index: number, field: keyof Bug, value: string) => {
setForm((prev) => {
const bugs = [...prev.bugsFound];
bugs[index] = { ...bugs[index], [field]: value as never };
return { ...prev, bugsFound: bugs };
});
};
const removeBug = (index: number) => {
setForm((prev) => ({ ...prev, bugsFound: prev.bugsFound.filter((_, i) => i !== index) }));
};
const scoreColor = (score: number) => {
if (score >= 80) return "text-emerald-400";
if (score >= 60) return "text-amber-400";
if (score >= 40) return "text-orange-400";
return "text-red-400";
};
const scoreBarColor = (score: number) => {
if (score >= 80) return "bg-emerald-500";
if (score >= 60) return "bg-amber-500";
if (score >= 40) return "bg-orange-500";
return "bg-red-500";
};
const openBugs = form.bugsFound.filter((b) => b.status === "open");
const closedBugs = form.bugsFound.filter((b) => b.status === "closed");
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-purple-400"></span> Amun Dev Agent
</h1>
<p className="text-sm text-slate-500 mt-0.5">Development testing & code quality dashboard</p>
</div>
<div className="flex items-center gap-3">
<input
type="date"
value={form.date}
onChange={(e) => {
setForm((prev) => ({ ...prev, date: e.target.value }));
loadSessionForDate(e.target.value);
}}
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-purple-500/50"
/>
<button
onClick={handleSave}
disabled={saving || loadingDate}
className="px-4 py-2 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Session"}
</button>
</div>
</div>
{errors.submit && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{errors.submit}
</div>
)}
{/* Stats Row */}
<div className="grid grid-cols-3 gap-4 mb-5">
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Tests Run</p>
<p className="text-2xl font-bold text-white">{form.testsRun.filter((t) => t.trim()).length}</p>
</div>
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Open Bugs</p>
<p className={`text-2xl font-bold ${openBugs.length > 0 ? "text-red-400" : "text-emerald-400"}`}>
{openBugs.length}
</p>
</div>
<div className="bg-[#13151f] border border-[#1e2130] rounded-xl p-4">
<p className="text-xs text-slate-500 mb-1">Code Quality</p>
<p className={`text-2xl font-bold ${scoreColor(form.codeQualityScore)}`}>
{form.codeQualityScore}%
</p>
</div>
</div>
<div className="space-y-5">
{/* Code Quality Score */}
<Card title="📊 Code Quality Score" accent="purple">
{errors.codeQualityScore && (
<p className="text-xs text-red-400 mb-2">{errors.codeQualityScore}</p>
)}
<div className="flex items-center gap-4">
<input
type="range"
min="0"
max="100"
value={form.codeQualityScore}
onChange={(e) => setForm((p) => ({ ...p, codeQualityScore: parseInt(e.target.value) }))}
className="flex-1 accent-purple-500"
/>
<div className="flex items-center gap-2">
<input
type="number"
min="0"
max="100"
value={form.codeQualityScore}
onChange={(e) =>
setForm((p) => ({
...p,
codeQualityScore: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)),
}))
}
className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-2 py-1.5 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50 text-center"
/>
<span className="text-sm text-slate-500">%</span>
</div>
</div>
<div className="mt-3 h-2 bg-[#0f1117] rounded-full overflow-hidden border border-[#2a2d3a]">
<div
className={`h-full rounded-full transition-all duration-300 ${scoreBarColor(form.codeQualityScore)}`}
style={{ width: `${form.codeQualityScore}%` }}
/>
</div>
</Card>
{/* Tests Run */}
<Card title="🧪 Tests Run" accent="blue">
<div className="space-y-2">
{form.testsRun.map((t, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-blue-500/70 font-mono mt-2.5 w-4"></span>
<input
type="text"
value={t}
onChange={(e) => updateArrayItem("testsRun", i, e.target.value)}
placeholder={`Test ${i + 1} (e.g. unit:auth, e2e:checkout)`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 font-mono"
/>
{form.testsRun.length > 1 && (
<button
onClick={() => removeArrayItem("testsRun", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
<button
onClick={() => addArrayItem("testsRun")}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors mt-1"
>
+ Add test
</button>
</div>
</Card>
{/* Bugs Found */}
<Card title="🐛 Bugs Found" subtitle={`${openBugs.length} open · ${closedBugs.length} closed`} accent="red">
<div className="space-y-3">
{form.bugsFound.length === 0 && (
<p className="text-sm text-slate-600 italic">No bugs logged yet.</p>
)}
{form.bugsFound.map((bug, i) => (
<div key={i} className="p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a]">
<div className="flex gap-2 mb-2">
<input
type="text"
value={bug.description}
onChange={(e) => updateBug(i, "description", e.target.value)}
placeholder="Bug description"
className="flex-1 bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
/>
<button
onClick={() => removeBug(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none px-1"
>
×
</button>
</div>
<div className="flex gap-2">
<select
value={bug.severity}
onChange={(e) => updateBug(i, "severity", e.target.value)}
className={`flex-1 bg-[#13151f] border rounded-lg px-3 py-1.5 text-xs focus:outline-none ${severityColors[bug.severity]}`}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
<button
onClick={() => updateBug(i, "status", bug.status === "open" ? "closed" : "open")}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
bug.status === "open"
? "bg-red-500/10 border-red-500/30 text-red-400 hover:bg-red-500/20"
: "bg-emerald-500/10 border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20"
}`}
>
{bug.status === "open" ? "● Open" : "✓ Closed"}
</button>
</div>
</div>
))}
<button
onClick={addBug}
className="text-xs text-red-400 hover:text-red-300 transition-colors mt-1"
>
+ Log bug
</button>
</div>
</Card>
{/* Recommendations */}
<Card title="💡 Recommendations" accent="amber">
<div className="space-y-2">
{form.recommendations.map((r, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-4"></span>
<input
type="text"
value={r}
onChange={(e) => updateArrayItem("recommendations", i, e.target.value)}
placeholder={`Recommendation ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
/>
{form.recommendations.length > 1 && (
<button
onClick={() => removeArrayItem("recommendations", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
<button
onClick={() => addArrayItem("recommendations")}
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
>
+ Add recommendation
</button>
</div>
</Card>
{/* Session History */}
{history.length > 0 && (
<Card title="📅 Recent Sessions" subtitle="Last 10 Amun sessions">
<div className="space-y-2">
{history.map((s) => (
<button
key={s.date}
onClick={() => loadSessionForDate(s.date)}
className="w-full flex items-center justify-between p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a] hover:border-purple-500/30 transition-colors text-left"
>
<div className="flex items-center gap-3">
<span className="text-xs text-slate-500 font-mono">{s.date}</span>
<span className="text-xs text-slate-400">
{s.testsRun.filter((t) => t.trim()).length} tests
</span>
{s.bugsFound.filter((b) => b.status === "open").length > 0 && (
<span className="text-xs text-red-400">
{s.bugsFound.filter((b) => b.status === "open").length} open bugs
</span>
)}
</div>
<span className={`text-sm font-bold ${scoreColor(s.codeQualityScore)}`}>
{s.codeQualityScore}%
</span>
</button>
))}
</div>
</Card>
)}
{/* Save Button */}
<div className="flex justify-end pb-6">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Session Saved!" : "Save Amun Session"}
</button>
</div>
</div>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
const GENERAL_CHANNEL = "1476261655955378401";
function formatAmunBrief(data: any): string {
return `👑 AMUN QA REPORT - ${data.date || new Date().toISOString().split('T')[0]}
🔍 TESTS RUN
${(data.tests || []).map((t: string) => `- ${t}`).join('\n')}
🐛 BUGS FOUND
${(data.bugs || []).map((b: string) => `- ${b}`).join('\n')}
📊 QUALITY SCORE: ${data.quality || 'N/A'}
💡 RECOMMENDATIONS
${(data.recommendations || []).map((r: string) => `- ${r}`).join('\n')}`;
}
export async function GET() {
const { data, error } = await supabase
.from("amun_sessions")
.select("*")
.order("created_at", { ascending: false })
.limit(20);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data || []);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { data: insertData, error } = await supabase
.from("amun_sessions")
.insert([{
date: body.date || new Date().toISOString().split('T')[0],
tests: body.tests,
bugs: body.bugs,
quality: body.quality,
recommendations: body.recommendations
}]);
if (error) {
console.error("Supabase error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
const discordMessage = formatAmunBrief(body);
await fetch(`https://discord.com/api/v10/channels/${GENERAL_CHANNEL}/messages`, {
method: "POST",
headers: {
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: discordMessage }),
});
return NextResponse.json({ success: true, message: "Amun brief saved and sent to Discord" });
} catch (error) {
return NextResponse.json({ error: "Failed" }, { status: 500 });
}
}
+62
View File
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { morningBriefs, eodBriefs, amunSessions } from "@/db/schema";
import { desc } from "drizzle-orm";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const days = parseInt(searchParams.get("days") || "30");
const [mornings, eods, amuns] = await Promise.all([
db.select().from(morningBriefs).orderBy(desc(morningBriefs.date)).limit(days),
db.select().from(eodBriefs).orderBy(desc(eodBriefs.date)).limit(days),
db.select().from(amunSessions).orderBy(desc(amunSessions.date)).limit(days),
]);
// Parse JSON fields for dashboard consumption
const parsedMornings = mornings.map((b) => ({
...b,
marketData: safeJson(b.marketData, {}),
aiNewsHeadlines: safeJson(b.aiNewsHeadlines, []),
skillsToLearn: safeJson(b.skillsToLearn, []),
sitementeStatus: safeJson(b.sitementeStatus, {}),
warmLeads: safeJson(b.warmLeads, []),
dayPriorities: safeJson(b.dayPriorities, []),
skippedTasks: safeJson(b.skippedTasks, []),
}));
const parsedEods = eods.map((b) => ({
...b,
marketUpdate: safeJson(b.marketUpdate, {}),
completedTasks: safeJson(b.completedTasks, []),
tomorrowPriorities: safeJson(b.tomorrowPriorities, []),
councilFeedback: safeJson(b.councilFeedback, {}),
projectProgress: safeJson(b.projectProgress, []),
}));
const parsedAmuns = amuns.map((s) => ({
...s,
testsRun: safeJson(s.testsRun, []),
bugsFound: safeJson(s.bugsFound, []),
recommendations: safeJson(s.recommendations, []),
}));
return NextResponse.json({
mornings: parsedMornings,
eods: parsedEods,
amuns: parsedAmuns,
});
} catch (error) {
console.error("GET /api/dashboard error:", error);
return NextResponse.json({ error: "Failed to fetch dashboard data" }, { status: 500 });
}
}
function safeJson<T>(str: string, fallback: T): T {
try {
return JSON.parse(str) as T;
} catch {
return fallback;
}
}
+129
View File
@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from "next/server";
/**
* POST /api/discord
*
* Receives a formatted message and forwards it to a Discord webhook.
* The DISCORD_WEBHOOK_URL environment variable must be set for messages to be sent.
*
* Body:
* {
* channel: "morning" | "eod" | "amun",
* source: "horus" | "amun",
* date: "YYYY-MM-DD",
* formatted_text: "Markdown-style message string"
* }
*
* Example curl:
* curl -X POST http://localhost:3000/api/discord \
* -H "Content-Type: application/json" \
* -d '{
* "channel": "morning",
* "source": "horus",
* "date": "2026-02-27",
* "formatted_text": "☀ **Morning Brief — 27 Feb 2026**\n📍 Benalmádena, 22°C Sunny\n📈 BTC: $68,000 (+2.5%)\n⚡ Priorities: 5 set"
* }'
*
* Note: Add Authorization: Bearer <token> header when auth is implemented.
*/
interface DiscordPayload {
channel: "morning" | "eod" | "amun";
source: "horus" | "amun";
date: string;
formatted_text: string;
}
const channelEmoji: Record<string, string> = {
morning: "☀",
eod: "🌙",
amun: "⚙️",
};
const sourceEmoji: Record<string, string> = {
horus: "🦅",
amun: "⚙️",
};
export async function POST(request: NextRequest) {
try {
const body: DiscordPayload = await request.json();
// Validate required fields
if (!body.channel || !body.source || !body.date || !body.formatted_text) {
return NextResponse.json(
{ error: "Missing required fields: channel, source, date, formatted_text" },
{ status: 400 }
);
}
if (!["morning", "eod", "amun"].includes(body.channel)) {
return NextResponse.json(
{ error: "channel must be one of: morning, eod, amun" },
{ status: 400 }
);
}
if (!["horus", "amun"].includes(body.source)) {
return NextResponse.json(
{ error: "source must be one of: horus, amun" },
{ status: 400 }
);
}
const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
if (!webhookUrl) {
// Webhook not configured — log and return success (non-blocking)
console.log(
`[Discord] Webhook not configured. Would have sent to #${body.channel}:`,
body.formatted_text.slice(0, 100)
);
return NextResponse.json({
ok: true,
sent: false,
reason: "DISCORD_WEBHOOK_URL not configured",
});
}
// Build Discord embed
const embed = {
title: `${channelEmoji[body.channel]} ${body.channel.charAt(0).toUpperCase() + body.channel.slice(1)} Brief — ${body.date}`,
description: body.formatted_text,
color:
body.channel === "morning"
? 0xf59e0b // amber
: body.channel === "eod"
? 0x3b82f6 // blue
: 0x8b5cf6, // purple
footer: {
text: `${sourceEmoji[body.source]} Sent by ${body.source.charAt(0).toUpperCase() + body.source.slice(1)}`,
},
timestamp: new Date().toISOString(),
};
const discordRes = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: "Horus & Amun",
avatar_url: "https://cdn.discordapp.com/embed/avatars/0.png",
embeds: [embed],
}),
});
if (!discordRes.ok) {
const errText = await discordRes.text();
console.error("[Discord] Webhook failed:", discordRes.status, errText);
return NextResponse.json(
{ error: "Discord webhook failed", status: discordRes.status },
{ status: 502 }
);
}
return NextResponse.json({ ok: true, sent: true });
} catch (error) {
console.error("POST /api/discord error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+69
View File
@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
const EOD_CHANNEL = "1476344633406656785";
function formatEODBrief(data: any): string {
return `🌙 END OF DAY - ${data.date || new Date().toISOString().split('T')[0]}
✅ COMPLETED TODAY
${(data.completed || []).map((c: string) => `- ${c}`).join('\n')}
📊 PROGRESS
${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n')}
🧠 COUNCIL
${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
🎯 TOMORROW
${(data.tomorrow || []).map((t: string) => `- ${t}`).join('\n')}`;
}
export async function GET() {
const { data, error } = await supabase
.from("eod_briefs")
.select("*")
.order("created_at", { ascending: false })
.limit(20);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data || []);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { data: insertData, error } = await supabase
.from("eod_briefs")
.insert([{
date: body.date || new Date().toISOString().split('T')[0],
completed: body.completed,
progress: body.progress,
council: body.council,
tomorrow: body.tomorrow
}]);
if (error) {
console.error("Supabase error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
const discordMessage = formatEODBrief(body);
await fetch(`https://discord.com/api/v10/channels/${EOD_CHANNEL}/messages`, {
method: "POST",
headers: {
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: discordMessage }),
});
return NextResponse.json({ success: true, message: "EOD brief saved and sent to Discord" });
} catch (error) {
return NextResponse.json({ error: "Failed" }, { status: 500 });
}
}
+77
View File
@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from "next/server";
import { supabase } from "@/lib/supabase";
const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || "MTQ3MTk4OTUzNjE1MzQwMzU5Nw.Ghtj4n.g-tl-Ijhfn9cg6zUCUIVd94EdwL32KmlVgRoSc";
const MORNING_CHANNEL = "1476344610493042698";
function formatMorningBrief(data: any): string {
return `☀️ MORNING BRIEF - ${data.date || new Date().toISOString().split('T')[0]}
🌤️ WEATHER
${data.weather || 'N/A'}
📊 MARKET
BTC: $${data.market?.BTC || 'N/A'} (${data.market?.btcChange || '0%'})
ETH: $${data.market?.ETH || 'N/A'} (${data.market?.ethChange || '0%'})
SOL: $${data.market?.SOL || 'N/A'} (${data.market?.solChange || '0%'})
🎯 PRIORITIES
${(data.priorities || []).map((p: string, i: number) => `${i + 1}. ${p}`).join('\n')}
💰 GOAL: ${data.goal || 'N/A'}
🔥 HOT LEADS
${(data.leads || []).map((l: string) => `- ${l}`).join('\n')}`;
}
export async function GET() {
const { data, error } = await supabase
.from("morning_briefs")
.select("*")
.order("created_at", { ascending: false })
.limit(20);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data || []);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Save to Supabase
const { data, error } = await supabase
.from("morning_briefs")
.insert([{
date: body.date || new Date().toISOString().split('T')[0],
weather: body.weather,
market: body.market,
priorities: body.priorities,
goal: body.goal,
leads: body.leads
}]);
if (error) {
console.error("Supabase error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
// Send to Discord
const discordMessage = formatMorningBrief(body);
await fetch(`https://discord.com/api/v10/channels/${MORNING_CHANNEL}/messages`, {
method: "POST",
headers: {
"Authorization": `Bot ${DISCORD_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: discordMessage }),
});
return NextResponse.json({ success: true, message: "Morning brief saved and sent to Discord" });
} catch (error) {
console.error("Error:", error);
return NextResponse.json({ error: "Failed to save brief" }, { status: 500 });
}
}
+108
View File
@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from "next/server";
import { spawn } from "child_process";
import fs from "fs";
import path from "path";
const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json";
function getTranscripts() {
if (fs.existsSync(STORAGE_FILE)) {
return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8"));
}
return [];
}
function saveTranscript(data) {
const transcripts = getTranscripts();
// Check if already exists
const existing = transcripts.findIndex(t => t.videoId === data.videoId);
if (existing >= 0) {
transcripts[existing] = data;
} else {
transcripts.unshift(data);
}
fs.writeFileSync(STORAGE_FILE, JSON.stringify(transcripts, null, 2));
return transcripts;
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const videoId = searchParams.get("videoId");
const transcripts = getTranscripts();
if (videoId) {
const found = transcripts.find(t => t.videoId === videoId);
return NextResponse.json(found || { error: "Not found" });
}
return NextResponse.json(transcripts);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, videoUrl, videoId, transcript, title, categories } = body;
// Extract video ID if not provided
let vid = videoId;
if (!vid && videoUrl) {
const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/);
vid = match ? match[1] : videoUrl;
}
if (action === "fetch") {
// Run Python script to get transcript
return new Promise((resolve) => {
const scriptPath = "/root/.openclaw/workspace/scripts/youtube_transcript.py";
const outputFile = `/tmp/transcript_${vid}.txt`;
const proc = spawn("python3", [scriptPath, vid], {
cwd: "/root/.openclaw/workspace/scripts"
});
let output = "";
let error = "";
proc.stdout.on("data", (data) => { output += data.toString(); });
proc.stderr.on("data", (data) => { error += data.toString(); });
proc.on("close", (code) => {
if (fs.existsSync(outputFile)) {
const transcriptText = fs.readFileSync(outputFile, "utf-8");
resolve(NextResponse.json({
success: true,
videoId: vid,
transcript: transcriptText
}));
} else {
resolve(NextResponse.json({
success: false,
error: error || "Could not fetch transcript"
}, { status: 500 }));
}
});
});
}
if (action === "save") {
// Save transcript with metadata
const data = {
videoId: vid,
videoUrl: videoUrl || `https://www.youtube.com/watch?v=${vid}`,
title: title || `Video ${vid}`,
transcript: transcript || "",
categories: categories || [],
savedAt: new Date().toISOString()
};
const updated = saveTranscript(data);
return NextResponse.json({ success: true, transcripts: updated });
}
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
} catch (error) {
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+4 -4
View File
@@ -5,7 +5,7 @@ import { useSearchParams, useRouter, usePathname } from "next/navigation";
import Image from "next/image";
import { motion } from "framer-motion";
import PaymentButton from "@/components/stripe/PaymentButton";
import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget";
// import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget";
type Language = "es" | "en";
type Vertical = "real-estate" | "restaurant" | "clinic" | "home-services";
@@ -532,11 +532,11 @@ function DemosContent() {
<p>{t.footer}</p>
</footer>
{/* Voice AI Widget */}
<SiteMenteVoiceWidget
{/* Voice AI Widget - Disabled for V2 */}
{/* <SiteMenteVoiceWidget
businessName={businessName || "SiteMente Demo"}
businessType={selected}
/>
/> */}
</div>
);
}
+444
View File
@@ -0,0 +1,444 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Card from "@/components/ui/Card";
const today = () => new Date().toISOString().split("T")[0];
interface MarketUpdate {
btc: string;
eth: string;
sol: string;
btcChange: string;
ethChange: string;
solChange: string;
}
interface CompletedTask {
task: string;
completed: boolean;
}
interface ProjectProgress {
project: string;
progress: number;
}
interface CouncilFeedback {
horus: string;
amun: string;
[key: string]: string;
}
interface FormState {
date: string;
marketUpdate: MarketUpdate;
completedTasks: CompletedTask[];
tomorrowPriorities: string[];
councilFeedback: CouncilFeedback;
projectProgress: ProjectProgress[];
}
const defaultProjects = ["SiteMente", "HolaCompi", "WandVoice"];
const defaultForm: FormState = {
date: today(),
marketUpdate: { btc: "", eth: "", sol: "", btcChange: "", ethChange: "", solChange: "" },
completedTasks: [{ task: "", completed: true }],
tomorrowPriorities: ["", "", "", "", ""],
councilFeedback: { horus: "", amun: "" },
projectProgress: defaultProjects.map((p) => ({ project: p, progress: 0 })),
};
export default function EodBriefPage() {
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadingDate, setLoadingDate] = useState(false);
const [extraAgents, setExtraAgents] = useState<string[]>([]);
const loadBriefForDate = useCallback(async (date: string) => {
setLoadingDate(true);
try {
const res = await fetch(`/api/eod?date=${date}`);
const data = await res.json();
if (data) {
const mu = typeof data.marketUpdate === "string" ? JSON.parse(data.marketUpdate) : (data.marketUpdate || defaultForm.marketUpdate);
const ct = typeof data.completedTasks === "string" ? JSON.parse(data.completedTasks) : (data.completedTasks || defaultForm.completedTasks);
const tp = typeof data.tomorrowPriorities === "string" ? JSON.parse(data.tomorrowPriorities) : (data.tomorrowPriorities || defaultForm.tomorrowPriorities);
const cf = typeof data.councilFeedback === "string" ? JSON.parse(data.councilFeedback) : (data.councilFeedback || defaultForm.councilFeedback);
const pp = typeof data.projectProgress === "string" ? JSON.parse(data.projectProgress) : (data.projectProgress || defaultForm.projectProgress);
setForm({ date: data.date, marketUpdate: mu, completedTasks: ct, tomorrowPriorities: tp, councilFeedback: cf, projectProgress: pp });
// Detect extra agents
const extras = Object.keys(cf).filter((k) => k !== "horus" && k !== "amun");
setExtraAgents(extras);
} else {
setForm({ ...defaultForm, date });
setExtraAgents([]);
}
} catch {
setForm({ ...defaultForm, date });
} finally {
setLoadingDate(false);
}
}, []);
useEffect(() => {
loadBriefForDate(today());
}, [loadBriefForDate]);
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (!form.completedTasks.some((t) => t.task.trim())) errs.completedTasks = "At least one completed task is required";
if (!form.tomorrowPriorities.some((p) => p.trim())) errs.tomorrowPriorities = "At least one priority for tomorrow is required";
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const res = await fetch("/api/eod", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const err = await res.json();
setErrors({ submit: err.error || "Failed to save" });
} else {
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
} catch {
setErrors({ submit: "Network error. Please try again." });
} finally {
setSaving(false);
}
};
const updateTask = (index: number, field: keyof CompletedTask, value: string | boolean) => {
setForm((prev) => {
const tasks = [...prev.completedTasks];
tasks[index] = { ...tasks[index], [field]: value };
return { ...prev, completedTasks: tasks };
});
};
const addTask = () => {
setForm((prev) => ({ ...prev, completedTasks: [...prev.completedTasks, { task: "", completed: true }] }));
};
const removeTask = (index: number) => {
setForm((prev) => ({ ...prev, completedTasks: prev.completedTasks.filter((_, i) => i !== index) }));
};
const updatePriority = (index: number, value: string) => {
setForm((prev) => {
const arr = [...prev.tomorrowPriorities];
arr[index] = value;
return { ...prev, tomorrowPriorities: arr };
});
};
const addPriority = () => {
setForm((prev) => ({ ...prev, tomorrowPriorities: [...prev.tomorrowPriorities, ""] }));
};
const removePriority = (index: number) => {
setForm((prev) => ({ ...prev, tomorrowPriorities: prev.tomorrowPriorities.filter((_, i) => i !== index) }));
};
const updateProgress = (index: number, value: number) => {
setForm((prev) => {
const pp = [...prev.projectProgress];
pp[index] = { ...pp[index], progress: value };
return { ...prev, projectProgress: pp };
});
};
const addProject = () => {
setForm((prev) => ({ ...prev, projectProgress: [...prev.projectProgress, { project: "", progress: 0 }] }));
};
const updateProjectName = (index: number, name: string) => {
setForm((prev) => {
const pp = [...prev.projectProgress];
pp[index] = { ...pp[index], project: name };
return { ...prev, projectProgress: pp };
});
};
const updateCouncil = (agent: string, value: string) => {
setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [agent]: value } }));
};
const addAgent = () => {
const name = prompt("Agent name:");
if (name?.trim()) {
setExtraAgents((prev) => [...prev, name.trim()]);
setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [name.trim()]: "" } }));
}
};
const changeColor = (val: string) => {
const n = parseFloat(val);
if (isNaN(n)) return "text-slate-400";
return n >= 0 ? "text-emerald-400" : "text-red-400";
};
const 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";
};
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-blue-400">🌙</span> End-of-Day Brief
</h1>
<p className="text-sm text-slate-500 mt-0.5">Horus + Amun daily debrief</p>
</div>
<div className="flex items-center gap-3">
<input
type="date"
value={form.date}
onChange={(e) => {
setForm((prev) => ({ ...prev, date: e.target.value }));
loadBriefForDate(e.target.value);
}}
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-blue-500/50"
/>
<button
onClick={handleSave}
disabled={saving || loadingDate}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Brief"}
</button>
</div>
</div>
{errors.submit && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{errors.submit}
</div>
)}
{loadingDate && (
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-blue-400 text-sm">
Loading brief for selected date
</div>
)}
<div className="space-y-5">
{/* Market Update */}
<Card title="📈 Market Update" accent="blue">
<div className="grid grid-cols-3 gap-4">
{(["btc", "eth", "sol"] as const).map((coin) => (
<div key={coin}>
<label className="block text-xs text-slate-500 mb-1.5 uppercase font-mono">{coin}</label>
<input
type="text"
value={form.marketUpdate[coin]}
onChange={(e) =>
setForm((p) => ({ ...p, marketUpdate: { ...p.marketUpdate, [coin]: e.target.value } }))
}
placeholder="$0.00"
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 mb-2"
/>
<input
type="text"
value={form.marketUpdate[`${coin}Change` as keyof MarketUpdate]}
onChange={(e) =>
setForm((p) => ({
...p,
marketUpdate: { ...p.marketUpdate, [`${coin}Change`]: e.target.value },
}))
}
placeholder="24h % (e.g. +2.5)"
className={`w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 ${changeColor(form.marketUpdate[`${coin}Change` as keyof MarketUpdate])}`}
/>
</div>
))}
</div>
</Card>
{/* Completed Tasks */}
<Card title="✅ Completed Tasks" accent="green">
{errors.completedTasks && (
<p className="text-xs text-red-400 mb-2">{errors.completedTasks}</p>
)}
<div className="space-y-2">
{form.completedTasks.map((task, i) => (
<div key={i} className="flex items-center gap-2">
<button
onClick={() => updateTask(i, "completed", !task.completed)}
className={`w-5 h-5 rounded border flex-shrink-0 flex items-center justify-center transition-colors ${
task.completed
? "bg-emerald-500 border-emerald-500 text-white"
: "border-[#2a2d3a] bg-transparent"
}`}
>
{task.completed && (
<svg width="10" height="10" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
<input
type="text"
value={task.task}
onChange={(e) => updateTask(i, "task", e.target.value)}
placeholder={`Task ${i + 1}`}
className={`flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-emerald-500/50 ${
task.completed ? "text-slate-400 line-through" : "text-slate-200"
}`}
/>
{form.completedTasks.length > 1 && (
<button
onClick={() => removeTask(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none"
>
×
</button>
)}
</div>
))}
<button
onClick={addTask}
className="text-xs text-emerald-400 hover:text-emerald-300 transition-colors mt-1"
>
+ Add task
</button>
</div>
</Card>
{/* Tomorrow's Priorities */}
<Card title="⚡ Priorities for Tomorrow" accent="amber">
{errors.tomorrowPriorities && (
<p className="text-xs text-red-400 mb-2">{errors.tomorrowPriorities}</p>
)}
<div className="space-y-2">
{form.tomorrowPriorities.map((p, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-5">{i + 1}.</span>
<input
type="text"
value={p}
onChange={(e) => updatePriority(i, e.target.value)}
placeholder={`Priority ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
/>
{form.tomorrowPriorities.length > 3 && (
<button
onClick={() => removePriority(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
{form.tomorrowPriorities.length < 7 && (
<button
onClick={addPriority}
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
>
+ Add priority
</button>
)}
</div>
</Card>
{/* Council Feedback */}
<Card title="🏛 Council Feedback" subtitle="One sentence per agent" accent="purple">
<div className="space-y-3">
{(["horus", "amun", ...extraAgents] as string[]).map((agent) => (
<div key={agent}>
<label className="block text-xs text-slate-500 mb-1.5 capitalize font-medium">
{agent === "horus" ? "🦅 Horus" : agent === "amun" ? "⚙️ Amun" : `🤖 ${agent}`}
</label>
<input
type="text"
value={form.councilFeedback[agent] || ""}
onChange={(e) => updateCouncil(agent, e.target.value)}
placeholder={`${agent.charAt(0).toUpperCase() + agent.slice(1)}'s one-sentence feedback`}
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50"
/>
</div>
))}
<button
onClick={addAgent}
className="text-xs text-purple-400 hover:text-purple-300 transition-colors mt-1"
>
+ Add agent
</button>
</div>
</Card>
{/* Project Progress */}
<Card title="📊 Project Progress" accent="blue">
<div className="space-y-4">
{form.projectProgress.map((proj, i) => (
<div key={i}>
<div className="flex items-center justify-between mb-1.5">
<input
type="text"
value={proj.project}
onChange={(e) => updateProjectName(i, e.target.value)}
placeholder="Project name"
className="bg-transparent text-sm font-medium text-slate-300 focus:outline-none border-b border-transparent focus:border-blue-500/50 pb-0.5 w-40"
/>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 font-mono">{proj.progress}%</span>
<input
type="number"
min="0"
max="100"
value={proj.progress}
onChange={(e) => updateProgress(i, Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))}
className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded px-2 py-1 text-xs text-slate-300 focus:outline-none focus:border-blue-500/50 text-center"
/>
</div>
</div>
<div className="h-2 bg-[#0f1117] rounded-full overflow-hidden border border-[#2a2d3a]">
<div
className={`h-full rounded-full transition-all duration-300 ${progressColor(proj.progress)}`}
style={{ width: `${proj.progress}%` }}
/>
</div>
</div>
))}
<button
onClick={addProject}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors mt-1"
>
+ Add project
</button>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end pb-6">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Brief Saved!" : "Save EOD Brief"}
</button>
</div>
</div>
</div>
);
}
+205
View File
@@ -0,0 +1,205 @@
"use client";
import { useState, useEffect } from "react";
export default function MissionBriefsPage() {
const [morningBriefs, setMorningBriefs] = useState<any[]>([]);
const [eodBriefs, setEodBriefs] = useState<any[]>([]);
const [amunBriefs, setAmunBriefs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [activeView, setActiveView] = useState<"mission" | "morning" | "eod" | "amun">("mission");
useEffect(() => {
fetchAllBriefs();
}, []);
const fetchAllBriefs = async () => {
setLoading(true);
try {
const [mRes, eRes, aRes] = await Promise.all([
fetch("/api/morning"),
fetch("/api/eod"),
fetch("/api/amun")
]);
const [mData, eData, aData] = await Promise.all([
mRes.json(),
eRes.json(),
aRes.json()
]);
setMorningBriefs(Array.isArray(mData) ? mData : []);
setEodBriefs(Array.isArray(eData) ? eData : []);
setAmunBriefs(Array.isArray(aData) ? aData : []);
} catch (e) {
console.error(e);
}
setLoading(false);
};
const formatMorning = (data: any) => `☀️ MORNING - ${data.date}
🌤️ ${data.weather || ''}
📊 BTC: $${data.market?.BTC} (${data.market?.btcChange})
ETH: $${data.market?.ETH} (${data.market?.ethChange})
SOL: $${data.market?.SOL} (${data.market?.solChange})
🎯 ${data.priorities?.map((p: string, i: number) => `${i + 1}. ${p}`).join('\n ') || ''}
💰 ${data.goal || ''}`;
const formatEOD = (data: any) => `🌙 EOD - ${data.date}
${data.completed?.map((c: string) => `- ${c}`).join('\n') || ''}
📊 ${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n') || ''}
🧠 ${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n') || ''}`;
const formatAmun = (data: any) => `👑 AMUN - ${data.date}
🔍 ${data.tests?.map((t: string) => `- ${t}`).join('\n') || ''}
🐛 ${data.bugs?.map((b: string) => `- ${b}`).join('\n') || ''}
📊 ${data.quality || ''}
💡 ${data.recommendations?.map((r: string) => `- ${r}`).join('\n') || ''}`;
const getLatest = () => {
const latestMorning = morningBriefs[0];
const latestEOD = eodBriefs[0];
const latestAmun = amunBriefs[0];
return { latestMorning, latestEOD, latestAmun };
};
const { latestMorning, latestEOD, latestAmun } = getLatest();
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">📋 Mission Control Briefs</h1>
{/* Navigation */}
<div className="flex gap-2 mb-6 flex-wrap">
<button
onClick={() => setActiveView("mission")}
className={`px-4 py-2 rounded-lg font-medium ${
activeView === "mission" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
}`}
>
🎯 Mission
</button>
<button
onClick={() => setActiveView("morning")}
className={`px-4 py-2 rounded-lg font-medium ${
activeView === "morning" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
}`}
>
Morning ({morningBriefs.length})
</button>
<button
onClick={() => setActiveView("eod")}
className={`px-4 py-2 rounded-lg font-medium ${
activeView === "eod" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
}`}
>
🌙 EOD ({eodBriefs.length})
</button>
<button
onClick={() => setActiveView("amun")}
className={`px-4 py-2 rounded-lg font-medium ${
activeView === "amun" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
}`}
>
👑 Amun ({amunBriefs.length})
</button>
</div>
{loading ? (
<div className="text-white/50">Loading...</div>
) : (
<>
{/* Mission View - Latest from all */}
{activeView === "mission" && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">🎯 Latest Overview</h2>
{latestMorning && (
<div className="bg-white/10 rounded-lg p-4">
<h3 className="text-lg font-medium mb-2"> Latest Morning</h3>
<pre className="whitespace-pre-wrap text-sm">{formatMorning(latestMorning)}</pre>
</div>
)}
{latestEOD && (
<div className="bg-white/10 rounded-lg p-4">
<h3 className="text-lg font-medium mb-2">🌙 Latest EOD</h3>
<pre className="whitespace-pre-wrap text-sm">{formatEOD(latestEOD)}</pre>
</div>
)}
{latestAmun && (
<div className="bg-white/10 rounded-lg p-4">
<h3 className="text-lg font-medium mb-2">👑 Latest Amun</h3>
<pre className="whitespace-pre-wrap text-sm">{formatAmun(latestAmun)}</pre>
</div>
)}
{!latestMorning && !latestEOD && !latestAmun && (
<div className="text-white/50">No briefs yet. POST to /api/morning, /api/eod, or /api/amun</div>
)}
</div>
)}
{/* Morning Briefs */}
{activeView === "morning" && (
<div className="space-y-4">
<h2 className="text-xl font-semibold"> Morning Briefs</h2>
{morningBriefs.length === 0 ? (
<div className="text-white/50">No morning briefs yet</div>
) : (
morningBriefs.map((brief: any, i: number) => (
<div key={i} className="bg-white/10 rounded-lg p-4">
<pre className="whitespace-pre-wrap text-sm">{formatMorning(brief)}</pre>
</div>
))
)}
</div>
)}
{/* EOD Briefs */}
{activeView === "eod" && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">🌙 End of Day Briefs</h2>
{eodBriefs.length === 0 ? (
<div className="text-white/50">No EOD briefs yet</div>
) : (
eodBriefs.map((brief: any, i: number) => (
<div key={i} className="bg-white/10 rounded-lg p-4">
<pre className="whitespace-pre-wrap text-sm">{formatEOD(brief)}</pre>
</div>
))
)}
</div>
)}
{/* Amun Sessions */}
{activeView === "amun" && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">👑 Amun QA Sessions</h2>
{amunBriefs.length === 0 ? (
<div className="text-white/50">No Amun sessions yet</div>
) : (
amunBriefs.map((brief: any, i: number) => (
<div key={i} className="bg-white/10 rounded-lg p-4">
<pre className="whitespace-pre-wrap text-sm">{formatAmun(brief)}</pre>
</div>
))
)}
</div>
)}
</>
)}
</div>
);
}
+233
View File
@@ -0,0 +1,233 @@
"use client";
import { useState, useEffect } from "react";
import fs from "fs";
const AVAILABLE_CATEGORIES = [
"SiteMente",
"OpenClaw",
"Trading",
"Marketing",
"Technical",
"Voice AI",
"Business",
"Inspiration",
"Personal Growth"
];
const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json";
function getTranscripts() {
try {
if (fs.existsSync(STORAGE_FILE)) {
return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8"));
}
} catch (e) {}
return [];
}
export default function TranscriptsPage() {
const [transcripts, setTranscripts] = useState<any[]>([]);
const [videoUrl, setVideoUrl] = useState("");
const [transcriptText, setTranscriptText] = useState("");
const [title, setTitle] = useState("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [filterCategory, setFilterCategory] = useState<string>("all");
const [saving, setSaving] = useState(false);
useEffect(() => {
setTranscripts(getTranscripts());
}, []);
const toggleCategory = (cat: string) => {
setSelectedCategories(prev =>
prev.includes(cat)
? prev.filter(c => c !== cat)
: [...prev, cat]
);
};
const saveTranscript = async () => {
if (!videoUrl || !transcriptText) return;
setSaving(true);
// Extract video ID
const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/);
const videoId = match ? match[1] : "";
const data = {
videoId,
videoUrl,
title: title || `Video ${videoId}`,
transcript: transcriptText,
categories: selectedCategories,
savedAt: new Date().toISOString()
};
// Save directly
const current = getTranscripts();
const existing = current.findIndex(t => t.videoId === videoId);
if (existing >= 0) {
current[existing] = data;
} else {
current.unshift(data);
}
fs.writeFileSync(STORAGE_FILE, JSON.stringify(current, null, 2));
setTranscripts(current);
setSaving(false);
setVideoUrl("");
setTranscriptText("");
setTitle("");
setSelectedCategories([]);
};
const filteredTranscripts = filterCategory === "all"
? transcripts
: transcripts.filter(t => t.categories?.includes(filterCategory));
const allCategories = [...new Set(transcripts.flatMap(t => t.categories || []))];
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">🎬 YouTube Transcripts</h1>
{/* Add New Transcript */}
<div className="bg-white/10 rounded-lg p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">Add Transcript</h2>
<input
type="text"
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
placeholder="YouTube URL..."
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
/>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title..."
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
/>
<div className="mb-3">
<p className="text-white/70 mb-2">Categories:</p>
<div className="flex flex-wrap gap-2">
{AVAILABLE_CATEGORIES.map(cat => (
<button
key={cat}
onClick={() => toggleCategory(cat)}
className={`px-3 py-1 rounded-full text-sm ${
selectedCategories.includes(cat)
? "bg-brand-pink text-white"
: "bg-white/10 text-white/70 hover:bg-white/20"
}`}
>
{cat}
</button>
))}
</div>
</div>
<textarea
value={transcriptText}
onChange={(e) => setTranscriptText(e.target.value)}
placeholder="Paste transcript here..."
rows={6}
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
/>
<button
onClick={saveTranscript}
disabled={saving || !videoUrl || !transcriptText}
className="px-6 py-2 bg-brand-pink rounded-lg font-medium hover:bg-[#ff7bc0] disabled:opacity-50"
>
{saving ? "Saving..." : "Save Transcript"}
</button>
</div>
{/* Filter */}
<div className="flex gap-2 mb-4 flex-wrap">
<button
onClick={() => setFilterCategory("all")}
className={`px-3 py-1 rounded-full text-sm ${
filterCategory === "all" ? "bg-brand-pink" : "bg-white/10"
}`}
>
All ({transcripts.length})
</button>
{allCategories.map(cat => (
<button
key={cat}
onClick={() => setFilterCategory(cat)}
className={`px-3 py-1 rounded-full text-sm ${
filterCategory === cat ? "bg-brand-pink" : "bg-white/10"
}`}
>
{cat} ({transcripts.filter(t => t.categories?.includes(cat)).length})
</button>
))}
</div>
{/* Saved Transcripts */}
<div className="space-y-4">
{filteredTranscripts.map((t: any, i: number) => (
<div key={i} className="bg-white/10 rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold">{t.title}</h3>
<a
href={t.videoUrl}
target="_blank"
className="text-brand-pink text-sm hover:underline"
>
YouTube
</a>
</div>
<div className="flex gap-2">
<span className="text-white/40 text-xs">
{new Date(t.savedAt).toLocaleDateString()}
</span>
<button
onClick={() => {
if (confirm("Delete this transcript?")) {
const updated = transcripts.filter(x => x.videoId !== t.videoId);
fs.writeFileSync(STORAGE_FILE, JSON.stringify(updated, null, 2));
setTranscripts(updated);
}
}}
className="text-red-400 hover:text-red-300"
>
</button>
</div>
</div>
<div className="flex flex-wrap gap-1 mb-2">
{t.categories?.map((c: string) => (
<span key={c} className="px-2 py-0.5 bg-brand-pink/30 rounded text-xs">
{c}
</span>
))}
</div>
<details>
<summary className="text-white/50 cursor-pointer text-sm">
View Transcript ({t.transcript?.length || 0} chars)
</summary>
<pre className="mt-2 text-xs text-white/70 whitespace-pre-wrap max-h-60 overflow-y-auto bg-black/20 p-2 rounded">
{t.transcript}
</pre>
</details>
</div>
))}
{filteredTranscripts.length === 0 && (
<div className="text-white/50">No transcripts saved yet</div>
)}
</div>
</div>
);
}
+583
View File
@@ -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&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>
);
}
+515
View File
@@ -0,0 +1,515 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Card from "@/components/ui/Card";
const today = () => new Date().toISOString().split("T")[0];
interface MarketData {
btc: string;
eth: string;
sol: string;
btcChange: string;
ethChange: string;
solChange: string;
}
interface SitementeStatus {
serverUptime: string;
demoPages: string;
leadStatus: string;
}
interface WarmLead {
name: string;
business: string;
note: string;
}
interface FormState {
date: string;
location: string;
weather: string;
marketData: MarketData;
aiNewsHeadlines: string[];
skillsToLearn: string[];
euAiActStatus: "OK" | "Risk" | "Opportunity";
sitementeStatus: SitementeStatus;
warmLeads: WarmLead[];
dayPriorities: string[];
skippedTasks: string[];
autoExecutionSummary: string;
}
const defaultForm: FormState = {
date: today(),
location: "Benalmádena",
weather: "",
marketData: { btc: "", eth: "", sol: "", btcChange: "", ethChange: "", solChange: "" },
aiNewsHeadlines: ["", "", "", "", ""],
skillsToLearn: ["", "", ""],
euAiActStatus: "OK",
sitementeStatus: { serverUptime: "", demoPages: "", leadStatus: "" },
warmLeads: [
{ name: "", business: "", note: "" },
{ name: "", business: "", note: "" },
{ name: "", business: "", note: "" },
],
dayPriorities: ["", "", "", "", ""],
skippedTasks: [""],
autoExecutionSummary: "",
};
export default function MorningBriefPage() {
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadingDate, setLoadingDate] = useState(false);
const loadBriefForDate = useCallback(async (date: string) => {
setLoadingDate(true);
try {
const res = await fetch(`/api/morning?date=${date}`);
const data = await res.json();
if (data) {
setForm({
date: data.date,
location: data.location || "Benalmádena",
weather: data.weather || "",
marketData: typeof data.marketData === "string" ? JSON.parse(data.marketData) : (data.marketData || defaultForm.marketData),
aiNewsHeadlines: typeof data.aiNewsHeadlines === "string" ? JSON.parse(data.aiNewsHeadlines) : (data.aiNewsHeadlines || defaultForm.aiNewsHeadlines),
skillsToLearn: typeof data.skillsToLearn === "string" ? JSON.parse(data.skillsToLearn) : (data.skillsToLearn || defaultForm.skillsToLearn),
euAiActStatus: data.euAiActStatus || "OK",
sitementeStatus: typeof data.sitementeStatus === "string" ? JSON.parse(data.sitementeStatus) : (data.sitementeStatus || defaultForm.sitementeStatus),
warmLeads: typeof data.warmLeads === "string" ? JSON.parse(data.warmLeads) : (data.warmLeads || defaultForm.warmLeads),
dayPriorities: typeof data.dayPriorities === "string" ? JSON.parse(data.dayPriorities) : (data.dayPriorities || defaultForm.dayPriorities),
skippedTasks: typeof data.skippedTasks === "string" ? JSON.parse(data.skippedTasks) : (data.skippedTasks || defaultForm.skippedTasks),
autoExecutionSummary: data.autoExecutionSummary || "",
});
} else {
setForm({ ...defaultForm, date });
}
} catch {
setForm({ ...defaultForm, date });
} finally {
setLoadingDate(false);
}
}, []);
useEffect(() => {
loadBriefForDate(today());
}, [loadBriefForDate]);
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (!form.location.trim()) errs.location = "Location is required";
if (!form.weather.trim()) errs.weather = "Weather is required";
if (!form.dayPriorities.some((p) => p.trim())) errs.dayPriorities = "At least one priority is required";
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const res = await fetch("/api/morning", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const err = await res.json();
setErrors({ submit: err.error || "Failed to save" });
} else {
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
} catch {
setErrors({ submit: "Network error. Please try again." });
} finally {
setSaving(false);
}
};
const updateArrayItem = (field: keyof FormState, index: number, value: string) => {
setForm((prev) => {
const arr = [...(prev[field] as string[])];
arr[index] = value;
return { ...prev, [field]: arr };
});
};
const addArrayItem = (field: keyof FormState, defaultVal: string = "") => {
setForm((prev) => ({ ...prev, [field]: [...(prev[field] as string[]), defaultVal] }));
};
const removeArrayItem = (field: keyof FormState, index: number) => {
setForm((prev) => {
const arr = (prev[field] as string[]).filter((_, i) => i !== index);
return { ...prev, [field]: arr };
});
};
const updateLead = (index: number, field: keyof WarmLead, value: string) => {
setForm((prev) => {
const leads = [...prev.warmLeads];
leads[index] = { ...leads[index], [field]: value };
return { ...prev, warmLeads: leads };
});
};
const changeColor = (val: string) => {
const n = parseFloat(val);
if (isNaN(n)) return "text-slate-400";
return n >= 0 ? "text-emerald-400" : "text-red-400";
};
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> Morning Brief
</h1>
<p className="text-sm text-slate-500 mt-0.5">Horus daily intelligence log</p>
</div>
<div className="flex items-center gap-3">
<input
type="date"
value={form.date}
onChange={(e) => {
setForm((prev) => ({ ...prev, date: e.target.value }));
loadBriefForDate(e.target.value);
}}
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-amber-500/50"
/>
<button
onClick={handleSave}
disabled={saving || loadingDate}
className="px-4 py-2 bg-amber-500 hover:bg-amber-400 disabled:opacity-50 text-black font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Brief"}
</button>
</div>
</div>
{errors.submit && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{errors.submit}
</div>
)}
{loadingDate && (
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-blue-400 text-sm">
Loading brief for selected date
</div>
)}
<div className="space-y-5">
{/* Location & Weather */}
<Card title="📍 Location & Weather" accent="amber">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-slate-500 mb-1.5">Location</label>
<input
type="text"
value={form.location}
onChange={(e) => setForm((p) => ({ ...p, location: e.target.value }))}
placeholder="Benalmádena"
className={`w-full bg-[#0f1117] border ${errors.location ? "border-red-500/50" : "border-[#2a2d3a]"} rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50`}
/>
{errors.location && <p className="text-xs text-red-400 mt-1">{errors.location}</p>}
</div>
<div>
<label className="block text-xs text-slate-500 mb-1.5">Weather</label>
<input
type="text"
value={form.weather}
onChange={(e) => setForm((p) => ({ ...p, weather: e.target.value }))}
placeholder="e.g. 22°C, Sunny"
className={`w-full bg-[#0f1117] border ${errors.weather ? "border-red-500/50" : "border-[#2a2d3a]"} rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50`}
/>
{errors.weather && <p className="text-xs text-red-400 mt-1">{errors.weather}</p>}
</div>
</div>
</Card>
{/* Market Prices */}
<Card title="📈 Market Prices" accent="blue">
<div className="grid grid-cols-3 gap-4">
{(["btc", "eth", "sol"] as const).map((coin) => (
<div key={coin}>
<label className="block text-xs text-slate-500 mb-1.5 uppercase font-mono">{coin}</label>
<input
type="text"
value={form.marketData[coin]}
onChange={(e) =>
setForm((p) => ({ ...p, marketData: { ...p.marketData, [coin]: e.target.value } }))
}
placeholder="$0.00"
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 mb-2"
/>
<input
type="text"
value={form.marketData[`${coin}Change` as keyof MarketData]}
onChange={(e) =>
setForm((p) => ({
...p,
marketData: { ...p.marketData, [`${coin}Change`]: e.target.value },
}))
}
placeholder="24h % (e.g. +2.5)"
className={`w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 ${changeColor(form.marketData[`${coin}Change` as keyof MarketData])}`}
/>
</div>
))}
</div>
</Card>
{/* AI News Headlines */}
<Card title="🤖 AI News Headlines" subtitle="35 headlines" accent="purple">
<div className="space-y-2">
{form.aiNewsHeadlines.map((h, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-slate-600 font-mono mt-2.5 w-4">{i + 1}.</span>
<input
type="text"
value={h}
onChange={(e) => updateArrayItem("aiNewsHeadlines", i, e.target.value)}
placeholder={`Headline ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50"
/>
{form.aiNewsHeadlines.length > 3 && (
<button
onClick={() => removeArrayItem("aiNewsHeadlines", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
{form.aiNewsHeadlines.length < 7 && (
<button
onClick={() => addArrayItem("aiNewsHeadlines")}
className="text-xs text-purple-400 hover:text-purple-300 transition-colors mt-1"
>
+ Add headline
</button>
)}
</div>
</Card>
{/* Skills to Learn */}
<Card title="🎯 Top Skills to Learn" subtitle="Top 3" accent="green">
<div className="space-y-2">
{form.skillsToLearn.map((s, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-slate-600 font-mono mt-2.5 w-4">{i + 1}.</span>
<input
type="text"
value={s}
onChange={(e) => updateArrayItem("skillsToLearn", i, e.target.value)}
placeholder={`Skill ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-emerald-500/50"
/>
</div>
))}
</div>
</Card>
{/* EU AI Act Status */}
<Card title="🇪🇺 EU AI Act Status" accent="amber">
<div className="flex gap-3">
{(["OK", "Risk", "Opportunity"] as const).map((status) => (
<button
key={status}
onClick={() => setForm((p) => ({ ...p, euAiActStatus: status }))}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium border transition-all ${
form.euAiActStatus === status
? status === "OK"
? "bg-emerald-500/20 border-emerald-500/50 text-emerald-400"
: status === "Risk"
? "bg-red-500/20 border-red-500/50 text-red-400"
: "bg-blue-500/20 border-blue-500/50 text-blue-400"
: "bg-transparent border-[#2a2d3a] text-slate-500 hover:border-slate-500"
}`}
>
{status === "OK" ? "✓ OK" : status === "Risk" ? "⚠ Risk" : "💡 Opportunity"}
</button>
))}
</div>
</Card>
{/* SiteMente Status */}
<Card title="🌐 SiteMente Status" accent="blue">
<div className="space-y-3">
<div>
<label className="block text-xs text-slate-500 mb-1.5">Server Uptime</label>
<input
type="text"
value={form.sitementeStatus.serverUptime}
onChange={(e) =>
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, serverUptime: e.target.value } }))
}
placeholder="e.g. 99.9% — All systems operational"
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50"
/>
</div>
<div>
<label className="block text-xs text-slate-500 mb-1.5">Demo Pages</label>
<input
type="text"
value={form.sitementeStatus.demoPages}
onChange={(e) =>
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, demoPages: e.target.value } }))
}
placeholder="e.g. 3 live, 1 in review"
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50"
/>
</div>
<div>
<label className="block text-xs text-slate-500 mb-1.5">Lead Status (Supabase)</label>
<textarea
value={form.sitementeStatus.leadStatus}
onChange={(e) =>
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, leadStatus: e.target.value } }))
}
placeholder="e.g. 12 leads in pipeline, 3 hot prospects, 1 demo scheduled"
rows={2}
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 resize-none"
/>
</div>
</div>
</Card>
{/* Warm Leads */}
<Card title="🔥 Warm Leads to Contact" subtitle="Top 3" accent="red">
<div className="space-y-4">
{form.warmLeads.map((lead, i) => (
<div key={i} className="p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a]">
<p className="text-xs text-slate-600 font-mono mb-2">Lead #{i + 1}</p>
<div className="grid grid-cols-2 gap-2 mb-2">
<input
type="text"
value={lead.name}
onChange={(e) => updateLead(i, "name", e.target.value)}
placeholder="Name"
className="bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
/>
<input
type="text"
value={lead.business}
onChange={(e) => updateLead(i, "business", e.target.value)}
placeholder="Business"
className="bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
/>
</div>
<input
type="text"
value={lead.note}
onChange={(e) => updateLead(i, "note", e.target.value)}
placeholder="Note / context"
className="w-full bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
/>
</div>
))}
</div>
</Card>
{/* Day Priorities */}
<Card title="⚡ Day Priorities" subtitle="57 numbered priorities" accent="amber">
{errors.dayPriorities && (
<p className="text-xs text-red-400 mb-2">{errors.dayPriorities}</p>
)}
<div className="space-y-2">
{form.dayPriorities.map((p, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-5">{i + 1}.</span>
<input
type="text"
value={p}
onChange={(e) => updateArrayItem("dayPriorities", i, e.target.value)}
placeholder={`Priority ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
/>
{form.dayPriorities.length > 3 && (
<button
onClick={() => removeArrayItem("dayPriorities", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
{form.dayPriorities.length < 7 && (
<button
onClick={() => addArrayItem("dayPriorities")}
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
>
+ Add priority
</button>
)}
</div>
</Card>
{/* Skipped Tasks */}
<Card title="⏭ Skipped Tasks" subtitle="Tasks carried over from yesterday">
<div className="space-y-2">
{form.skippedTasks.map((t, i) => (
<div key={i} className="flex gap-2">
<input
type="text"
value={t}
onChange={(e) => updateArrayItem("skippedTasks", i, e.target.value)}
placeholder="Skipped task"
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-slate-500/50"
/>
{form.skippedTasks.length > 1 && (
<button
onClick={() => removeArrayItem("skippedTasks", i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
<button
onClick={() => addArrayItem("skippedTasks")}
className="text-xs text-slate-400 hover:text-slate-300 transition-colors mt-1"
>
+ Add skipped task
</button>
</div>
</Card>
{/* Auto-Execution Summary */}
<Card title="🦅 Horus Auto-Execution" subtitle="What Horus will do without asking" accent="amber">
<textarea
value={form.autoExecutionSummary}
onChange={(e) => setForm((p) => ({ ...p, autoExecutionSummary: e.target.value }))}
placeholder="e.g. Monitor BTC for breakout above $70k, send SiteMente weekly report to leads, update Notion task board…"
rows={4}
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50 resize-none"
/>
</Card>
{/* Save Button */}
<div className="flex justify-end pb-6">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2.5 bg-amber-500 hover:bg-amber-400 disabled:opacity-50 text-black font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Brief Saved!" : "Save Morning Brief"}
</button>
</div>
</div>
</div>
);
}
@@ -61,6 +61,8 @@ const sidebarCategories: SidebarCategory[] = [
]},
{ id: "calendar", name: "Calendar", icon: "📅", items: [
{ id: "brief", name: "Morning Brief", icon: "☀️", category: "calendar" },
{ id: "briefs", name: "All Briefs", icon: "📋", category: "briefs" },
{ id: "transcripts", name: "Transcripts", icon: "🎬", category: "transcripts" },
]},
{ id: "memory", name: "Memory", icon: "🧠", items: [
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
@@ -404,6 +406,26 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
</div>
)}
{currentItem?.category === "briefs" && (
<div className="space-y-4">
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">📋 All Briefs</h3>
<p className="text-white/60 mb-4">Morning, EOD, and Amun reports</p>
<a href="/mission-control/briefs" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">📋 Open Briefs</a>
</div>
</div>
)}
{currentItem?.category === "transcripts" && (
<div className="space-y-4">
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">🎬 YouTube Transcripts</h3>
<p className="text-white/60 mb-4">Save and organize video transcripts for learning</p>
<a href="/mission-control/transcripts" className="inline-flex items-center gap-2 px-4 py-2 bg-brand-pink rounded-lg text-sm font-medium hover:bg-[#ff7bc0] transition">🎬 Open Transcripts</a>
</div>
</div>
)}
{currentItem?.category === "memory" && (
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
<h3 className="text-lg font-semibold mb-4">🧠 Memory & Logs</h3>
+440 -1
View File
@@ -19,7 +19,8 @@
"next": "^15.5.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"stripe": "^17.5.0"
"stripe": "^17.5.0",
"youtube-transcript-api": "^3.0.6"
},
"devDependencies": {
"@types/react": "^19.1.12",
@@ -1182,6 +1183,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
@@ -1219,6 +1226,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1277,6 +1295,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
@@ -1430,6 +1454,48 @@
"chart.js": ">=3.2.0"
}
},
"node_modules/cheerio": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz",
"integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.1.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.19.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=20.18.1"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1492,6 +1558,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -1516,6 +1594,34 @@
"node": ">= 8"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1562,6 +1668,15 @@
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1595,6 +1710,61 @@
"dev": true,
"license": "MIT"
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "17.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
@@ -1650,6 +1820,31 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1680,6 +1875,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1787,6 +1997,26 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1803,6 +2033,22 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -2052,6 +2298,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -2064,6 +2325,37 @@
"node": ">= 0.4"
}
},
"node_modules/htmlparser2": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
"integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"entities": "^7.0.1"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -2086,6 +2378,18 @@
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -2286,6 +2590,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -2496,6 +2821,18 @@
"node": ">=0.10.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -2534,6 +2871,55 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -2798,6 +3184,12 @@
"node": ">=12.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
@@ -2969,6 +3361,12 @@
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -3464,6 +3862,15 @@
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
"integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -3517,6 +3924,28 @@
"node": ">= 8"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3643,6 +4072,16 @@
"optional": true
}
}
},
"node_modules/youtube-transcript-api": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/youtube-transcript-api/-/youtube-transcript-api-3.0.6.tgz",
"integrity": "sha512-hKFohFI+695Pnr8tyDgo+ghEGZdUh151FDy+BSMgMH0RNFo7n/p66F3RX6ZmqKfANRsT3/RFFWqWJpOuBzHblw==",
"license": "MIT",
"dependencies": {
"axios": "^1.10.0",
"cheerio": "^1.1.0"
}
}
}
}
+2 -1
View File
@@ -21,7 +21,8 @@
"next": "^15.5.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"stripe": "^17.5.0"
"stripe": "^17.5.0",
"youtube-transcript-api": "^3.0.6"
},
"devDependencies": {
"@types/react": "^19.1.12",
+46
View File
@@ -0,0 +1,46 @@
-- Mission Control Briefs Database Schema
-- Run these in Supabase SQL Editor
-- Morning Briefs Table
CREATE TABLE IF NOT EXISTS morning_briefs (
id SERIAL PRIMARY KEY,
date TEXT NOT NULL,
weather TEXT,
market JSONB DEFAULT '{}',
priorities JSONB DEFAULT '[]',
goal TEXT,
leads JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- EOD Briefs Table
CREATE TABLE IF NOT EXISTS eod_briefs (
id SERIAL PRIMARY KEY,
date TEXT NOT NULL,
completed JSONB DEFAULT '[]',
progress JSONB DEFAULT '{}',
council JSONB DEFAULT '{}',
tomorrow JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Amun Sessions Table
CREATE TABLE IF NOT EXISTS amun_sessions (
id SERIAL PRIMARY KEY,
date TEXT NOT NULL,
tests JSONB DEFAULT '[]',
bugs JSONB DEFAULT '[]',
quality TEXT,
recommendations JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE morning_briefs ENABLE ROW LEVEL SECURITY;
ALTER TABLE eod_briefs ENABLE ROW LEVEL SECURITY;
ALTER TABLE amun_sessions ENABLE ROW LEVEL SECURITY;
-- Allow public read/write (can be restricted later)
CREATE POLICY "Allow all" ON morning_briefs FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "Allow all" ON eod_briefs FOR ALL USING (true) WITH CHECK (true);
CREATE POLICY "Allow all" ON amun_sessions FOR ALL USING (true) WITH CHECK (true);
+69 -5
View File
@@ -4,14 +4,78 @@
"trader": "🤖 Thoth (AI Agent)",
"pair": "BTC/USD",
"direction": "long",
"entryPrice": 18,
"entryPrice": 18000,
"status": "closed",
"isDemo": true,
"openedAt": "2026-02-20T17:17:01.116Z",
"closedAt": "2026-02-23T17:17:01.116Z",
"exitPrice": 18500,
"pnl": 500,
"pnlPercent": 2.77,
"setup": "Breakout",
"timeframe": "4H",
"reason": "BTC breaking resistance",
"notes": "Paper trade - closed for profit",
"traderStyle": "thoth"
},
{
"id": "1771950000001",
"trader": "⚔️ Sekhmet",
"pair": "SOL/USD",
"direction": "long",
"entryPrice": 85,
"status": "open",
"isDemo": true,
"openedAt": "2026-02-23T17:17:01.116Z",
"setup": "🤖 Thoth (AI Agent) setup",
"openedAt": "2026-02-21T10:00:00.000Z",
"setup": "Support bounce",
"timeframe": "1H",
"reason": "SOL testing major support",
"notes": "Paper trade - still open",
"traderStyle": "sekhmet"
},
{
"id": "1772000000002",
"trader": "📈 Thoth Trading",
"pair": "ETH/USD",
"direction": "long",
"entryPrice": 2050,
"status": "open",
"isDemo": true,
"openedAt": "2026-02-22T14:30:00.000Z",
"setup": "Bull flag",
"timeframe": "4H",
"reason": "",
"notes": "",
"reason": "ETH forming bullish pattern",
"notes": "Paper trade - still open",
"traderStyle": "thoth"
},
{
"id": "1772100000003",
"trader": "⚔️ Sekhmet",
"pair": "BTC/USD",
"direction": "long",
"entryPrice": 67000,
"status": "open",
"isDemo": true,
"openedAt": "2026-02-23T08:00:00.000Z",
"setup": "Range breakout",
"timeframe": "1D",
"reason": "BTC consolidating, ready for move up",
"notes": "Paper trade - entered today",
"traderStyle": "sekhmet"
},
{
"id": "1772150000004",
"trader": "⚔️ Sekhmet",
"pair": "BTC/USD",
"direction": "short",
"entryPrice": 69200,
"status": "open",
"isDemo": true,
"openedAt": "2026-02-24T16:00:00.000Z",
"setup": "Resistance rejection",
"timeframe": "4H",
"reason": "BTC hitting resistance, expect pullback",
"notes": "Paper short - scalp target",
"traderStyle": "sekhmet"
}
]