Add YouTube transcripts tab to Mission Control
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -5,7 +5,7 @@ import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import PaymentButton from "@/components/stripe/PaymentButton";
|
import PaymentButton from "@/components/stripe/PaymentButton";
|
||||||
import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget";
|
// import SiteMenteVoiceWidget from "@/components/SiteMenteVoiceWidget";
|
||||||
|
|
||||||
type Language = "es" | "en";
|
type Language = "es" | "en";
|
||||||
type Vertical = "real-estate" | "restaurant" | "clinic" | "home-services";
|
type Vertical = "real-estate" | "restaurant" | "clinic" | "home-services";
|
||||||
@@ -532,11 +532,11 @@ function DemosContent() {
|
|||||||
<p>{t.footer}</p>
|
<p>{t.footer}</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Voice AI Widget */}
|
{/* Voice AI Widget - Disabled for V2 */}
|
||||||
<SiteMenteVoiceWidget
|
{/* <SiteMenteVoiceWidget
|
||||||
businessName={businessName || "SiteMente Demo"}
|
businessName={businessName || "SiteMente Demo"}
|
||||||
businessType={selected}
|
businessType={selected}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Card from "@/components/ui/Card";
|
||||||
|
|
||||||
|
interface MarketData {
|
||||||
|
btc?: string;
|
||||||
|
eth?: string;
|
||||||
|
sol?: string;
|
||||||
|
btcChange?: string;
|
||||||
|
ethChange?: string;
|
||||||
|
solChange?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MorningBrief {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
location: string;
|
||||||
|
weather: string;
|
||||||
|
marketData: MarketData;
|
||||||
|
aiNewsHeadlines: string[];
|
||||||
|
skillsToLearn: string[];
|
||||||
|
euAiActStatus: string;
|
||||||
|
sitementeStatus: { serverUptime?: string; demoPages?: string; leadStatus?: string };
|
||||||
|
warmLeads: { name: string; business: string; note: string }[];
|
||||||
|
dayPriorities: string[];
|
||||||
|
skippedTasks: string[];
|
||||||
|
autoExecutionSummary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompletedTask {
|
||||||
|
task: string;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectProgress {
|
||||||
|
project: string;
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EodBrief {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
marketUpdate: MarketData;
|
||||||
|
completedTasks: CompletedTask[];
|
||||||
|
tomorrowPriorities: string[];
|
||||||
|
councilFeedback: Record<string, string>;
|
||||||
|
projectProgress: ProjectProgress[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Bug {
|
||||||
|
description: string;
|
||||||
|
severity: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AmunSession {
|
||||||
|
id: number;
|
||||||
|
date: string;
|
||||||
|
testsRun: string[];
|
||||||
|
bugsFound: Bug[];
|
||||||
|
codeQualityScore: number;
|
||||||
|
recommendations: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeColor(val?: string) {
|
||||||
|
if (!val) return "text-slate-500";
|
||||||
|
const n = parseFloat(val);
|
||||||
|
if (isNaN(n)) return "text-slate-500";
|
||||||
|
return n >= 0 ? "text-emerald-400" : "text-red-400";
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressColor(p: number) {
|
||||||
|
if (p >= 80) return "bg-emerald-500";
|
||||||
|
if (p >= 50) return "bg-amber-500";
|
||||||
|
if (p >= 25) return "bg-orange-500";
|
||||||
|
return "bg-red-500";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSnapshot(morning?: MorningBrief, eod?: EodBrief, amun?: AmunSession): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (morning) {
|
||||||
|
const date = new Date(morning.date + "T12:00:00").toLocaleDateString("en-GB", {
|
||||||
|
weekday: "long", day: "numeric", month: "long",
|
||||||
|
});
|
||||||
|
parts.push(`📅 **${date}** — ${morning.location}, ${morning.weather}.`);
|
||||||
|
|
||||||
|
const md = morning.marketData;
|
||||||
|
if (md?.btc) {
|
||||||
|
const btcStr = md.btcChange
|
||||||
|
? `BTC at ${md.btc} (${parseFloat(md.btcChange) >= 0 ? "+" : ""}${md.btcChange}%)`
|
||||||
|
: `BTC at ${md.btc}`;
|
||||||
|
const ethStr = md.eth ? `, ETH at ${md.eth}` : "";
|
||||||
|
const solStr = md.sol ? `, SOL at ${md.sol}` : "";
|
||||||
|
parts.push(`📈 Markets: ${btcStr}${ethStr}${solStr}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headlines = morning.aiNewsHeadlines?.filter((h) => h.trim()) || [];
|
||||||
|
if (headlines.length > 0) {
|
||||||
|
parts.push(`🤖 AI News: ${headlines.slice(0, 2).join("; ")}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorities = morning.dayPriorities?.filter((p) => p.trim()) || [];
|
||||||
|
if (priorities.length > 0) {
|
||||||
|
parts.push(`⚡ ${priorities.length} priorities set. Top: "${priorities[0]}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipped = morning.skippedTasks?.filter((t) => t.trim()) || [];
|
||||||
|
if (skipped.length > 0) {
|
||||||
|
parts.push(`⏭ ${skipped.length} task(s) carried over from yesterday.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (morning.autoExecutionSummary?.trim()) {
|
||||||
|
parts.push(`🦅 Horus auto-executing: ${morning.autoExecutionSummary.slice(0, 120)}${morning.autoExecutionSummary.length > 120 ? "…" : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eod) {
|
||||||
|
const completed = eod.completedTasks?.filter((t) => t.completed) || [];
|
||||||
|
const total = eod.completedTasks?.length || 0;
|
||||||
|
if (total > 0) {
|
||||||
|
parts.push(`✅ EOD: ${completed.length}/${total} tasks completed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cf = eod.councilFeedback || {};
|
||||||
|
if (cf.horus) parts.push(`🦅 Horus: "${cf.horus}"`);
|
||||||
|
if (cf.amun) parts.push(`⚙️ Amun: "${cf.amun}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amun) {
|
||||||
|
const openBugs = amun.bugsFound?.filter((b) => b.status === "open") || [];
|
||||||
|
const tests = amun.testsRun?.filter((t) => t.trim()) || [];
|
||||||
|
parts.push(
|
||||||
|
`🧪 Amun: ${tests.length} test(s) run, ${openBugs.length} open bug(s), code quality ${amun.codeQualityScore}%.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return "No data logged yet. Start by filing a Morning Brief.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MissionControlPage() {
|
||||||
|
const [morning, setMorning] = useState<MorningBrief | null>(null);
|
||||||
|
const [eod, setEod] = useState<EodBrief | null>(null);
|
||||||
|
const [amun, setAmun] = useState<AmunSession | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const loadLatest = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [mRes, eRes, aRes] = await Promise.all([
|
||||||
|
fetch("/api/morning?limit=1"),
|
||||||
|
fetch("/api/eod?limit=1"),
|
||||||
|
fetch("/api/amun?limit=1"),
|
||||||
|
]);
|
||||||
|
const [mData, eData, aData] = await Promise.all([mRes.json(), eRes.json(), aRes.json()]);
|
||||||
|
|
||||||
|
const parseJson = <T,>(val: unknown, fallback: T): T => {
|
||||||
|
if (typeof val === "string") {
|
||||||
|
try { return JSON.parse(val) as T; } catch { return fallback; }
|
||||||
|
}
|
||||||
|
return (val as T) ?? fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(mData) && mData[0]) {
|
||||||
|
const m = mData[0];
|
||||||
|
setMorning({
|
||||||
|
...m,
|
||||||
|
marketData: parseJson(m.marketData, {}),
|
||||||
|
aiNewsHeadlines: parseJson(m.aiNewsHeadlines, []),
|
||||||
|
skillsToLearn: parseJson(m.skillsToLearn, []),
|
||||||
|
sitementeStatus: parseJson(m.sitementeStatus, {}),
|
||||||
|
warmLeads: parseJson(m.warmLeads, []),
|
||||||
|
dayPriorities: parseJson(m.dayPriorities, []),
|
||||||
|
skippedTasks: parseJson(m.skippedTasks, []),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(eData) && eData[0]) {
|
||||||
|
const e = eData[0];
|
||||||
|
setEod({
|
||||||
|
...e,
|
||||||
|
marketUpdate: parseJson(e.marketUpdate, {}),
|
||||||
|
completedTasks: parseJson(e.completedTasks, []),
|
||||||
|
tomorrowPriorities: parseJson(e.tomorrowPriorities, []),
|
||||||
|
councilFeedback: parseJson(e.councilFeedback, {}),
|
||||||
|
projectProgress: parseJson(e.projectProgress, []),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(aData) && aData[0]) {
|
||||||
|
const a = aData[0];
|
||||||
|
setAmun({
|
||||||
|
...a,
|
||||||
|
testsRun: parseJson(a.testsRun, []),
|
||||||
|
bugsFound: parseJson(a.bugsFound, []),
|
||||||
|
recommendations: parseJson(a.recommendations, []),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLatest();
|
||||||
|
}, [loadLatest]);
|
||||||
|
|
||||||
|
const snapshot = buildSnapshot(morning ?? undefined, eod ?? undefined, amun ?? undefined);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex items-center justify-center min-h-screen">
|
||||||
|
<p className="text-slate-600">Loading Mission Control…</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noData = !morning && !eod && !amun;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||||||
|
<span className="text-amber-400">▶</span> Horus Mission Control
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-slate-500 mt-0.5">Latest intelligence snapshot across all agents</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={loadLatest}
|
||||||
|
className="px-3 py-1.5 text-xs text-slate-400 border border-[#2a2d3a] rounded-lg hover:border-amber-500/30 hover:text-amber-400 transition-colors"
|
||||||
|
>
|
||||||
|
↻ Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{noData ? (
|
||||||
|
<div className="text-center py-16">
|
||||||
|
<p className="text-slate-500 mb-4">No briefs logged yet.</p>
|
||||||
|
<div className="flex justify-center gap-3">
|
||||||
|
<Link href="/morning" className="px-4 py-2 bg-amber-500/10 border border-amber-500/30 text-amber-400 rounded-lg text-sm hover:bg-amber-500/20 transition-colors">
|
||||||
|
☀ Log Morning Brief
|
||||||
|
</Link>
|
||||||
|
<Link href="/eod" className="px-4 py-2 bg-blue-500/10 border border-blue-500/30 text-blue-400 rounded-lg text-sm hover:bg-blue-500/20 transition-colors">
|
||||||
|
🌙 Log EOD Brief
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Today Snapshot */}
|
||||||
|
<div className="bg-gradient-to-r from-amber-500/5 to-orange-500/5 border border-amber-500/20 rounded-xl p-5">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="w-6 h-6 rounded-md bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-black font-bold text-xs">H</div>
|
||||||
|
<h2 className="text-sm font-semibold text-amber-300">Today Snapshot — Horus Narrative</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-300 leading-7 break-words whitespace-pre-line">{snapshot.replace(/ (📅|📈|🤖|⚡|⏭|🦅|✅|🧪)/g, "\n$1")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Morning Brief Summary */}
|
||||||
|
{morning && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 pt-1">
|
||||||
|
<span className="text-xs font-semibold text-amber-400 uppercase tracking-widest">Morning Brief</span>
|
||||||
|
<div className="flex-1 h-px bg-amber-500/15" />
|
||||||
|
<span className="text-xs text-slate-600">{morning.date}</span>
|
||||||
|
</div>
|
||||||
|
<Card title="☀ Morning Brief" subtitle={morning.date} accent="amber">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Location & Weather */}
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<span className="text-slate-400">📍 {morning.location}</span>
|
||||||
|
<span className="text-slate-400">🌤 {morning.weather}</span>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 rounded text-xs border ${
|
||||||
|
morning.euAiActStatus === "OK"
|
||||||
|
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
|
||||||
|
: morning.euAiActStatus === "Risk"
|
||||||
|
? "bg-red-500/10 border-red-500/30 text-red-400"
|
||||||
|
: "bg-blue-500/10 border-blue-500/30 text-blue-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🇪🇺 {morning.euAiActStatus}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Market */}
|
||||||
|
{(morning.marketData?.btc || morning.marketData?.eth || morning.marketData?.sol) && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Market</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{(["btc", "eth", "sol"] as const).map((coin) => {
|
||||||
|
const price = morning.marketData[coin];
|
||||||
|
const change = morning.marketData[`${coin}Change` as keyof MarketData];
|
||||||
|
if (!price) return null;
|
||||||
|
return (
|
||||||
|
<div key={coin} className="flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-slate-500 uppercase font-mono">{coin}</span>
|
||||||
|
<span className="text-xs text-slate-300">{price}</span>
|
||||||
|
{change && (
|
||||||
|
<span className={`text-xs ${changeColor(change)}`}>
|
||||||
|
{parseFloat(change) >= 0 ? "+" : ""}{change}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI News */}
|
||||||
|
{morning.aiNewsHeadlines?.filter((h) => h.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">AI News</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{morning.aiNewsHeadlines.filter((h) => h.trim()).map((h, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-400 flex gap-2">
|
||||||
|
<span className="text-purple-500/50">•</span> {h}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Priorities */}
|
||||||
|
{morning.dayPriorities?.filter((p) => p.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Day Priorities</p>
|
||||||
|
<ol className="space-y-1">
|
||||||
|
{morning.dayPriorities.filter((p) => p.trim()).map((p, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
||||||
|
<span className="text-amber-500/50 font-mono">{i + 1}.</span> {p}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Skipped Tasks */}
|
||||||
|
{morning.skippedTasks?.filter((t) => t.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Skipped Tasks</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{morning.skippedTasks.filter((t) => t.trim()).map((t, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-500 flex gap-2">
|
||||||
|
<span className="text-slate-700">⏭</span> {t}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Auto-Execution */}
|
||||||
|
{morning.autoExecutionSummary?.trim() && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Auto-Execution</p>
|
||||||
|
<p className="text-xs text-amber-300/80 bg-amber-500/5 border border-amber-500/10 rounded-lg p-3">
|
||||||
|
🦅 {morning.autoExecutionSummary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* EOD Brief Summary */}
|
||||||
|
{eod && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 pt-1">
|
||||||
|
<span className="text-xs font-semibold text-blue-400 uppercase tracking-widest">End-of-Day Brief</span>
|
||||||
|
<div className="flex-1 h-px bg-blue-500/15" />
|
||||||
|
<span className="text-xs text-slate-600">{eod.date}</span>
|
||||||
|
</div>
|
||||||
|
<Card title="🌙 End-of-Day Brief" subtitle={eod.date} accent="blue">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Market Update */}
|
||||||
|
{(eod.marketUpdate?.btc || eod.marketUpdate?.eth || eod.marketUpdate?.sol) && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Market Update</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{(["btc", "eth", "sol"] as const).map((coin) => {
|
||||||
|
const price = eod.marketUpdate[coin];
|
||||||
|
const change = eod.marketUpdate[`${coin}Change` as keyof MarketData];
|
||||||
|
if (!price) return null;
|
||||||
|
return (
|
||||||
|
<div key={coin} className="flex items-center gap-1.5">
|
||||||
|
<span className="text-xs text-slate-500 uppercase font-mono">{coin}</span>
|
||||||
|
<span className="text-xs text-slate-300">{price}</span>
|
||||||
|
{change && (
|
||||||
|
<span className={`text-xs ${changeColor(change)}`}>
|
||||||
|
{parseFloat(change) >= 0 ? "+" : ""}{change}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Completed Tasks */}
|
||||||
|
{eod.completedTasks?.filter((t) => t.task.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">
|
||||||
|
Completed Tasks ({eod.completedTasks.filter((t) => t.completed).length}/{eod.completedTasks.filter((t) => t.task.trim()).length})
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{eod.completedTasks.filter((t) => t.task.trim()).map((t, i) => (
|
||||||
|
<li key={i} className={`text-xs flex gap-2 ${t.completed ? "text-slate-400 line-through" : "text-slate-300"}`}>
|
||||||
|
<span>{t.completed ? "✓" : "○"}</span> {t.task}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tomorrow Priorities */}
|
||||||
|
{eod.tomorrowPriorities?.filter((p) => p.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Tomorrow's Priorities</p>
|
||||||
|
<ol className="space-y-1">
|
||||||
|
{eod.tomorrowPriorities.filter((p) => p.trim()).map((p, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
||||||
|
<span className="text-amber-500/50 font-mono">{i + 1}.</span> {p}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Council Feedback */}
|
||||||
|
{Object.keys(eod.councilFeedback || {}).filter((k) => eod.councilFeedback[k]?.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Council Feedback</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(eod.councilFeedback).filter(([, v]) => v?.trim()).map(([agent, feedback]) => (
|
||||||
|
<div key={agent} className="flex gap-2">
|
||||||
|
<span className="text-xs text-slate-500 capitalize w-12 flex-shrink-0">
|
||||||
|
{agent === "horus" ? "🦅" : agent === "amun" ? "⚙️" : "🤖"} {agent}:
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-300 italic">“{feedback}”</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Project Progress */}
|
||||||
|
{eod.projectProgress?.filter((p) => p.project.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Project Progress</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{eod.projectProgress.filter((p) => p.project.trim()).map((proj) => (
|
||||||
|
<div key={proj.project}>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-xs text-slate-400">{proj.project}</span>
|
||||||
|
<span className="text-xs text-slate-500 font-mono">{proj.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-[#0f1117] rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full ${progressColor(proj.progress)}`}
|
||||||
|
style={{ width: `${proj.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Amun Session Summary */}
|
||||||
|
{amun && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3 pt-1">
|
||||||
|
<span className="text-xs font-semibold text-purple-400 uppercase tracking-widest">Amun Dev Session</span>
|
||||||
|
<div className="flex-1 h-px bg-purple-500/15" />
|
||||||
|
<span className="text-xs text-slate-600">{amun.date}</span>
|
||||||
|
</div>
|
||||||
|
<Card title="⚙️ Amun Dev Session" subtitle={amun.date} accent="purple">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
||||||
|
<p className="text-xs text-slate-600 mb-1">Tests Run</p>
|
||||||
|
<p className="text-lg font-bold text-white">{amun.testsRun?.filter((t) => t.trim()).length || 0}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
||||||
|
<p className="text-xs text-slate-600 mb-1">Open Bugs</p>
|
||||||
|
<p className={`text-lg font-bold ${amun.bugsFound?.filter((b) => b.status === "open").length > 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||||
|
{amun.bugsFound?.filter((b) => b.status === "open").length || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#0f1117] rounded-lg p-3 text-center">
|
||||||
|
<p className="text-xs text-slate-600 mb-1">Code Quality</p>
|
||||||
|
<p className={`text-lg font-bold ${amun.codeQualityScore >= 80 ? "text-emerald-400" : amun.codeQualityScore >= 60 ? "text-amber-400" : "text-red-400"}`}>
|
||||||
|
{amun.codeQualityScore}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tests */}
|
||||||
|
{amun.testsRun?.filter((t) => t.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Tests</p>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{amun.testsRun.filter((t) => t.trim()).map((t, i) => (
|
||||||
|
<span key={i} className="px-2 py-0.5 bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs rounded font-mono">
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bugs */}
|
||||||
|
{amun.bugsFound?.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Bugs Found</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{amun.bugsFound.map((bug, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`px-1.5 py-0.5 rounded text-xs border ${
|
||||||
|
bug.severity === "critical"
|
||||||
|
? "text-red-400 bg-red-500/10 border-red-500/30"
|
||||||
|
: bug.severity === "high"
|
||||||
|
? "text-orange-400 bg-orange-500/10 border-orange-500/30"
|
||||||
|
: bug.severity === "medium"
|
||||||
|
? "text-amber-400 bg-amber-500/10 border-amber-500/30"
|
||||||
|
: "text-emerald-400 bg-emerald-500/10 border-emerald-500/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{bug.severity}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs ${bug.status === "closed" ? "text-slate-600 line-through" : "text-slate-300"}`}>
|
||||||
|
{bug.description}
|
||||||
|
</span>
|
||||||
|
<span className={`ml-auto text-xs ${bug.status === "open" ? "text-red-400" : "text-emerald-400"}`}>
|
||||||
|
{bug.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
{amun.recommendations?.filter((r) => r.trim()).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-slate-600 mb-2 uppercase tracking-wide">Recommendations</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{amun.recommendations.filter((r) => r.trim()).map((r, i) => (
|
||||||
|
<li key={i} className="text-xs text-slate-300 flex gap-2">
|
||||||
|
<span className="text-amber-500/50">→</span> {r}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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="3–5 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="5–7 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: "calendar", name: "Calendar", icon: "📅", items: [
|
||||||
{ id: "brief", name: "Morning Brief", icon: "☀️", category: "calendar" },
|
{ 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: "memory", name: "Memory", icon: "🧠", items: [
|
||||||
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
|
{ id: "logs", name: "Session Logs", icon: "📝", category: "memory" },
|
||||||
@@ -404,6 +406,26 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
</div>
|
</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" && (
|
{currentItem?.category === "memory" && (
|
||||||
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
||||||
<h3 className="text-lg font-semibold mb-4">🧠 Memory & Logs</h3>
|
<h3 className="text-lg font-semibold mb-4">🧠 Memory & Logs</h3>
|
||||||
|
|||||||
Generated
+440
-1
@@ -19,7 +19,8 @@
|
|||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"stripe": "^17.5.0"
|
"stripe": "^17.5.0",
|
||||||
|
"youtube-transcript-api": "^3.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
@@ -1182,6 +1183,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.24",
|
"version": "10.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
|
||||||
@@ -1219,6 +1226,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1277,6 +1295,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/bowser": {
|
||||||
"version": "2.14.1",
|
"version": "2.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||||
@@ -1430,6 +1454,48 @@
|
|||||||
"chart.js": ">=3.2.0"
|
"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": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -1492,6 +1558,18 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -1516,6 +1594,34 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"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": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
@@ -1595,6 +1710,61 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.3.1",
|
"version": "17.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
@@ -1650,6 +1820,31 @@
|
|||||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -1680,6 +1875,21 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -1787,6 +1997,26 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -1803,6 +2033,22 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/formdata-polyfill": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
@@ -2052,6 +2298,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
@@ -2064,6 +2325,37 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
@@ -2086,6 +2378,18 @@
|
|||||||
"node": ">=20.0.0"
|
"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": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@@ -2286,6 +2590,27 @@
|
|||||||
"node": ">=8.6"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
@@ -2496,6 +2821,18 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -2534,6 +2871,55 @@
|
|||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||||
"license": "BlueOak-1.0.0"
|
"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": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
@@ -2798,6 +3184,12 @@
|
|||||||
"node": ">=12.0.0"
|
"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": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||||
@@ -2969,6 +3361,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -3464,6 +3862,15 @@
|
|||||||
"node": ">=14.17"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
@@ -3517,6 +3924,28 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -3643,6 +4072,16 @@
|
|||||||
"optional": true
|
"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
@@ -21,7 +21,8 @@
|
|||||||
"next": "^15.5.3",
|
"next": "^15.5.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"stripe": "^17.5.0"
|
"stripe": "^17.5.0",
|
||||||
|
"youtube-transcript-api": "^3.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
|
|||||||
@@ -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);
|
||||||
+70
-6
@@ -4,14 +4,78 @@
|
|||||||
"trader": "🤖 Thoth (AI Agent)",
|
"trader": "🤖 Thoth (AI Agent)",
|
||||||
"pair": "BTC/USD",
|
"pair": "BTC/USD",
|
||||||
"direction": "long",
|
"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",
|
"status": "open",
|
||||||
"isDemo": true,
|
"isDemo": true,
|
||||||
"openedAt": "2026-02-23T17:17:01.116Z",
|
"openedAt": "2026-02-21T10:00:00.000Z",
|
||||||
"setup": "🤖 Thoth (AI Agent) setup",
|
"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",
|
"timeframe": "4H",
|
||||||
"reason": "",
|
"reason": "ETH forming bullish pattern",
|
||||||
"notes": "",
|
"notes": "Paper trade - still open",
|
||||||
"traderStyle": "thoth"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user