BMHQ Upgrade: Add Auto-Run, Execution Logs, Change Log, Brainown panels
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const STORAGE_DIR = path.join(process.cwd(), "data", "agent-outputs");
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(STORAGE_DIR)) {
|
||||||
|
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentDir(agent: string) {
|
||||||
|
const dir = path.join(STORAGE_DIR, agent);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const agent = searchParams.get("agent");
|
||||||
|
const date = searchParams.get("date");
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
// Return list of all agent directories
|
||||||
|
ensureDir();
|
||||||
|
const agents = fs.readdirSync(STORAGE_DIR).filter(f =>
|
||||||
|
fs.statSync(path.join(STORAGE_DIR, f)).isDirectory()
|
||||||
|
);
|
||||||
|
return NextResponse.json(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentDir = getAgentDir(agent);
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
// Return specific file
|
||||||
|
const file = path.join(agentDir, `${date}.md`);
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
const content = fs.readFileSync(file, "utf-8");
|
||||||
|
return NextResponse.json({ agent, date, content });
|
||||||
|
}
|
||||||
|
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return list of files for agent
|
||||||
|
const files = fs.readdirSync(agentDir)
|
||||||
|
.filter(f => f.endsWith('.md'))
|
||||||
|
.map(f => f.replace('.md', ''))
|
||||||
|
.sort()
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
return NextResponse.json(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, agent, date, content } = body;
|
||||||
|
|
||||||
|
if (action === "save") {
|
||||||
|
const agentDir = getAgentDir(agent);
|
||||||
|
const file = path.join(agentDir, `${date}.md`);
|
||||||
|
fs.writeFileSync(file, content);
|
||||||
|
return NextResponse.json({ success: true, file });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
const agentDir = getAgentDir(agent);
|
||||||
|
const file = path.join(agentDir, `${date}.md`);
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
fs.unlinkSync(file);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const STORAGE_DIR = path.join(process.cwd(), "data");
|
||||||
|
const CHANGELOG_FILE = path.join(STORAGE_DIR, "changelog.json");
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(STORAGE_DIR)) {
|
||||||
|
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJSON(file: string, defaultValue: any = []) {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(defaultValue));
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJSON(file: string, data: any) {
|
||||||
|
ensureDir();
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const agent = searchParams.get("agent");
|
||||||
|
const type = searchParams.get("type");
|
||||||
|
const limit = parseInt(searchParams.get("limit") || "50");
|
||||||
|
|
||||||
|
let changelog = readJSON(CHANGELOG_FILE, []);
|
||||||
|
|
||||||
|
if (agent) {
|
||||||
|
changelog = changelog.filter((c: any) => c.agent === agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
changelog = changelog.filter((c: any) => c.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
changelog = changelog.slice(0, limit);
|
||||||
|
|
||||||
|
return NextResponse.json(changelog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, entry, entryId } = body;
|
||||||
|
|
||||||
|
const changelog = readJSON(CHANGELOG_FILE, []);
|
||||||
|
|
||||||
|
if (action === "add") {
|
||||||
|
const newEntry = {
|
||||||
|
id: `changelog-${Date.now()}`,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
...entry
|
||||||
|
};
|
||||||
|
changelog.unshift(newEntry);
|
||||||
|
writeJSON(CHANGELOG_FILE, changelog);
|
||||||
|
return NextResponse.json({ success: true, changelog });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
const filtered = changelog.filter((c: any) => c.id !== entryId);
|
||||||
|
writeJSON(CHANGELOG_FILE, filtered);
|
||||||
|
return NextResponse.json({ success: true, changelog: filtered });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const STORAGE_DIR = path.join(process.cwd(), "data");
|
||||||
|
const LOGS_FILE = path.join(STORAGE_DIR, "execution-logs.json");
|
||||||
|
const OUTPUTS_FILE = path.join(STORAGE_DIR, "agent-outputs.json");
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(STORAGE_DIR)) {
|
||||||
|
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJSON(file: string, defaultValue: any = []) {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(defaultValue));
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJSON(file: string, data: any) {
|
||||||
|
ensureDir();
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const agent = searchParams.get("agent");
|
||||||
|
const templateId = searchParams.get("templateId");
|
||||||
|
const limit = parseInt(searchParams.get("limit") || "50");
|
||||||
|
|
||||||
|
let logs = readJSON(LOGS_FILE, []);
|
||||||
|
|
||||||
|
// Filter by agent
|
||||||
|
if (agent) {
|
||||||
|
logs = logs.filter((l: any) => l.agent === agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by template
|
||||||
|
if (templateId) {
|
||||||
|
logs = logs.filter((l: any) => l.templateId === templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit
|
||||||
|
logs = logs.slice(0, limit);
|
||||||
|
|
||||||
|
return NextResponse.json(logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, logId, output, status, agent } = body;
|
||||||
|
|
||||||
|
const logs = readJSON(LOGS_FILE, []);
|
||||||
|
|
||||||
|
if (action === "update") {
|
||||||
|
const idx = logs.findIndex((l: any) => l.id === logId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
if (status) logs[idx].status = status;
|
||||||
|
if (output) logs[idx].output = output;
|
||||||
|
if (status === "completed" || status === "failed") {
|
||||||
|
logs[idx].completedAt = new Date().toISOString();
|
||||||
|
}
|
||||||
|
writeJSON(LOGS_FILE, logs);
|
||||||
|
return NextResponse.json({ success: true, log: logs[idx] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "clear") {
|
||||||
|
// Clear old logs (keep last 100)
|
||||||
|
const filtered = logs.slice(0, 100);
|
||||||
|
writeJSON(LOGS_FILE, filtered);
|
||||||
|
return NextResponse.json({ success: true, logs: filtered });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const STORAGE_DIR = path.join(process.cwd(), "data");
|
||||||
|
const TEMPLATES_FILE = path.join(STORAGE_DIR, "recurring-templates.json");
|
||||||
|
const LOGS_FILE = path.join(STORAGE_DIR, "execution-logs.json");
|
||||||
|
const CHANGELOG_FILE = path.join(STORAGE_DIR, "changelog.json");
|
||||||
|
const OUTPUTS_FILE = path.join(STORAGE_DIR, "agent-outputs.json");
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(STORAGE_DIR)) {
|
||||||
|
fs.mkdirSync(STORAGE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJSON(file: string, defaultValue: any = []) {
|
||||||
|
ensureDir();
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(defaultValue));
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJSON(file: string, data: any) {
|
||||||
|
ensureDir();
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates CRUD
|
||||||
|
export async function GET() {
|
||||||
|
const templates = readJSON(TEMPLATES_FILE, []);
|
||||||
|
return NextResponse.json(templates);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, template, templateId, enabled } = body;
|
||||||
|
|
||||||
|
const templates = readJSON(TEMPLATES_FILE, []);
|
||||||
|
|
||||||
|
if (action === "toggle") {
|
||||||
|
const idx = templates.findIndex((t: any) => t.id === templateId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
templates[idx].enabled = enabled;
|
||||||
|
writeJSON(TEMPLATES_FILE, templates);
|
||||||
|
return NextResponse.json({ success: true, templates });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "save") {
|
||||||
|
const idx = templates.findIndex((t: any) => t.id === template.id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
templates[idx] = { ...templates[idx], ...template };
|
||||||
|
} else {
|
||||||
|
templates.push({ ...template, runCount: 0, enabled: false });
|
||||||
|
}
|
||||||
|
writeJSON(TEMPLATES_FILE, templates);
|
||||||
|
return NextResponse.json({ success: true, templates });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "delete") {
|
||||||
|
const filtered = templates.filter((t: any) => t.id !== templateId);
|
||||||
|
writeJSON(TEMPLATES_FILE, filtered);
|
||||||
|
return NextResponse.json({ success: true, templates: filtered });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "run-now") {
|
||||||
|
// Trigger immediate execution
|
||||||
|
const idx = templates.findIndex((t: any) => t.id === templateId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const template = templates[idx];
|
||||||
|
|
||||||
|
// Create execution log
|
||||||
|
const logs = readJSON(LOGS_FILE, []);
|
||||||
|
const logId = `log-${Date.now()}`;
|
||||||
|
const newLog = {
|
||||||
|
id: logId,
|
||||||
|
templateId: template.id,
|
||||||
|
agent: template.agent,
|
||||||
|
taskTitle: template.taskTemplate.title,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
status: "running",
|
||||||
|
output: "Agent executing..."
|
||||||
|
};
|
||||||
|
logs.unshift(newLog);
|
||||||
|
writeJSON(LOGS_FILE, logs);
|
||||||
|
|
||||||
|
// Update template
|
||||||
|
templates[idx].lastRun = new Date().toISOString();
|
||||||
|
templates[idx].runCount = (templates[idx].runCount || 0) + 1;
|
||||||
|
writeJSON(TEMPLATES_FILE, templates);
|
||||||
|
|
||||||
|
// TODO: Actually dispatch to OpenClaw agent here
|
||||||
|
// For now, simulate completion after delay
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
templates,
|
||||||
|
executionLog: newLog
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { RecurringTemplate, defaultRecurringTemplates } from "@/lib/mission-control/recurring";
|
||||||
|
|
||||||
|
const AGENT_COLORS: Record<string, string> = {
|
||||||
|
thoth: "#8b5cf6",
|
||||||
|
"thoth-trading": "#f59e0b",
|
||||||
|
ptah: "#10b981",
|
||||||
|
seshat: "#ec4899",
|
||||||
|
anubis: "#6366f1",
|
||||||
|
sekhmet: "#ef4444"
|
||||||
|
};
|
||||||
|
|
||||||
|
const AGENT_NAMES: Record<string, string> = {
|
||||||
|
thoth: "Thoth",
|
||||||
|
"thoth-trading": "Thoth Trading",
|
||||||
|
ptah: "Ptah",
|
||||||
|
seshat: "Seshat",
|
||||||
|
anubis: "Anubis",
|
||||||
|
sekhmet: "Sekhmet"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AutoRunPanel() {
|
||||||
|
const [templates, setTemplates] = useState<RecurringTemplate[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [running, setRunning] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTemplates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchTemplates = async () => {
|
||||||
|
const res = await fetch("/api/recurring");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Merge with defaults if empty
|
||||||
|
if (data.length === 0) {
|
||||||
|
await fetch("/api/recurring", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: "save",
|
||||||
|
template: defaultRecurringTemplates[0]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
setTemplates(defaultRecurringTemplates);
|
||||||
|
} else {
|
||||||
|
setTemplates(data);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTemplate = async (templateId: string, enabled: boolean) => {
|
||||||
|
await fetch("/api/recurring", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "toggle", templateId, enabled })
|
||||||
|
});
|
||||||
|
fetchTemplates();
|
||||||
|
};
|
||||||
|
|
||||||
|
const runNow = async (templateId: string) => {
|
||||||
|
setRunning(templateId);
|
||||||
|
await fetch("/api/recurring", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "run-now", templateId })
|
||||||
|
});
|
||||||
|
setRunning(null);
|
||||||
|
fetchTemplates();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSchedule = (template: RecurringTemplate) => {
|
||||||
|
const { schedule } = template;
|
||||||
|
switch (schedule.type) {
|
||||||
|
case "hourly": return "Hourly";
|
||||||
|
case "daily": return `Daily @ ${schedule.time}`;
|
||||||
|
case "weekly":
|
||||||
|
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
return `Weekly: ${days[schedule.dayOfWeek || 0]} @ ${schedule.time}`;
|
||||||
|
case "monthly": return `Monthly: Day ${schedule.dayOfMonth}`;
|
||||||
|
default: return schedule.type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4 text-white/50">Loading templates...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-lg font-semibold">🔄 Auto-Run Templates</h3>
|
||||||
|
<span className="text-sm text-white/50">
|
||||||
|
{templates.filter(t => t.enabled).length} active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<div
|
||||||
|
key={template.id}
|
||||||
|
className={`p-4 rounded-lg border ${
|
||||||
|
template.enabled
|
||||||
|
? "bg-white/10 border-green-500/30"
|
||||||
|
: "bg-white/5 border-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span
|
||||||
|
className="px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
style={{ backgroundColor: AGENT_COLORS[template.agent] || "#666" }}
|
||||||
|
>
|
||||||
|
{AGENT_NAMES[template.agent] || template.agent}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/70 text-sm">
|
||||||
|
{formatSchedule(template)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="font-medium">{template.name}</h4>
|
||||||
|
<p className="text-sm text-white/50">{template.description}</p>
|
||||||
|
|
||||||
|
{template.preInstructions && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="text-xs text-white/40 cursor-pointer">
|
||||||
|
View pre-instructions
|
||||||
|
</summary>
|
||||||
|
<p className="text-xs text-white/60 mt-1 p-2 bg-black/20 rounded">
|
||||||
|
{template.preInstructions}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleTemplate(template.id, !template.enabled)}
|
||||||
|
className={`px-3 py-1 rounded text-sm font-medium transition ${
|
||||||
|
template.enabled
|
||||||
|
? "bg-green-600 hover:bg-green-700"
|
||||||
|
: "bg-white/10 hover:bg-white/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{template.enabled ? "✓ ON" : "○ OFF"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => runNow(template.id)}
|
||||||
|
disabled={running === template.id}
|
||||||
|
className="px-3 py-1 bg-brand-pink/80 hover:bg-brand-pink rounded text-sm font-medium disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{running === template.id ? "⏳" : "▶ Run"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mt-3 text-xs text-white/40">
|
||||||
|
<span>Run count: {template.runCount || 0}</span>
|
||||||
|
{template.lastRun && (
|
||||||
|
<span>Last: {new Date(template.lastRun).toLocaleString()}</span>
|
||||||
|
)}
|
||||||
|
{template.nextRun && (
|
||||||
|
<span>Next: {new Date(template.nextRun).toLocaleString()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{templates.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-white/50">
|
||||||
|
No recurring templates. Create one to automate your agents.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface AgentOutput {
|
||||||
|
agent: string;
|
||||||
|
date: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AGENT_NAMES: Record<string, string> = {
|
||||||
|
thoth: "Thoth",
|
||||||
|
"thoth-trading": "Thoth Trading",
|
||||||
|
ptah: "Ptah",
|
||||||
|
seshat: "Seshat",
|
||||||
|
anubis: "Anubis",
|
||||||
|
sekhmet: "Sekhmet",
|
||||||
|
horus: "Horus"
|
||||||
|
};
|
||||||
|
|
||||||
|
const AGENT_COLORS: Record<string, string> = {
|
||||||
|
thoth: "#8b5cf6",
|
||||||
|
"thoth-trading": "#f59e0b",
|
||||||
|
ptah: "#10b981",
|
||||||
|
seshat: "#ec4899",
|
||||||
|
anubis: "#6366f1",
|
||||||
|
sekhmet: "#ef4444",
|
||||||
|
horus: "#ff7bc0"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BrainownPanel() {
|
||||||
|
const [agents, setAgents] = useState<string[]>([]);
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||||||
|
const [dates, setDates] = useState<string[]>([]);
|
||||||
|
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAgents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchAgents = async () => {
|
||||||
|
const res = await fetch("/api/agent-outputs");
|
||||||
|
const data = await res.json();
|
||||||
|
setAgents(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDates = async (agent: string) => {
|
||||||
|
const res = await fetch(`/api/agent-outputs?agent=${agent}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setDates(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchContent = async (agent: string, date: string) => {
|
||||||
|
const res = await fetch(`/api/agent-outputs?agent=${agent}&date=${date}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.content) {
|
||||||
|
setContent(data.content);
|
||||||
|
} else {
|
||||||
|
setContent(`# ${AGENT_NAMES[agent] || agent} - ${date}\n\nStart writing your notes here...`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentSelect = (agent: string) => {
|
||||||
|
setSelectedAgent(agent);
|
||||||
|
setSelectedDate(null);
|
||||||
|
setContent("");
|
||||||
|
fetchDates(agent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (date: string) => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
if (selectedAgent) {
|
||||||
|
fetchContent(selectedAgent, date);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveContent = async () => {
|
||||||
|
if (!selectedAgent || !selectedDate) return;
|
||||||
|
|
||||||
|
await fetch("/api/agent-outputs", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: "save",
|
||||||
|
agent: selectedAgent,
|
||||||
|
date: selectedDate,
|
||||||
|
content
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewNote = () => {
|
||||||
|
if (!selectedAgent) return;
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
setSelectedDate(today);
|
||||||
|
setContent(`# ${AGENT_NAMES[selectedAgent]} - ${today}\n\n`);
|
||||||
|
setEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4 text-white/50">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold">🧠 Brainown</h3>
|
||||||
|
{selectedAgent && (
|
||||||
|
<button
|
||||||
|
onClick={createNewNote}
|
||||||
|
className="px-3 py-1 bg-brand-pink hover:bg-[#ff7bc0] rounded text-sm font-medium"
|
||||||
|
>
|
||||||
|
+ New Note
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{/* Agent List */}
|
||||||
|
<div className="col-span-1 space-y-2">
|
||||||
|
<h4 className="text-sm text-white/50 font-medium">Agents</h4>
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<button
|
||||||
|
key={agent}
|
||||||
|
onClick={() => handleAgentSelect(agent)}
|
||||||
|
className={`w-full p-2 rounded text-left text-sm transition ${
|
||||||
|
selectedAgent === agent
|
||||||
|
? "bg-brand-pink/20 border border-brand-pink"
|
||||||
|
: "bg-white/5 hover:bg-white/10 border border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="inline-block w-2 h-2 rounded-full mr-2"
|
||||||
|
style={{ backgroundColor: AGENT_COLORS[agent] || "#666" }}
|
||||||
|
/>
|
||||||
|
{AGENT_NAMES[agent] || agent}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{agents.length === 0 && (
|
||||||
|
<p className="text-xs text-white/50">No agent outputs yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date List */}
|
||||||
|
<div className="col-span-1 space-y-2">
|
||||||
|
<h4 className="text-sm text-white/50 font-medium">Notes</h4>
|
||||||
|
{dates.map((date) => (
|
||||||
|
<button
|
||||||
|
key={date}
|
||||||
|
onClick={() => handleDateSelect(date)}
|
||||||
|
className={`w-full p-2 rounded text-left text-sm transition ${
|
||||||
|
selectedDate === date
|
||||||
|
? "bg-brand-pink/20 border border-brand-pink"
|
||||||
|
: "bg-white/5 hover:bg-white/10 border border-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📝 {date}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedAgent && dates.length === 0 && (
|
||||||
|
<p className="text-xs text-white/50">No notes yet</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Editor */}
|
||||||
|
<div className="col-span-2">
|
||||||
|
{selectedDate ? (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<h4 className="text-sm text-white/50">
|
||||||
|
{AGENT_NAMES[selectedAgent]} - {selectedDate}
|
||||||
|
</h4>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{editing ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditing(false)}
|
||||||
|
className="px-2 py-1 bg-white/10 rounded text-xs"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveContent}
|
||||||
|
className="px-2 py-1 bg-green-600 rounded text-xs"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
className="px-2 py-1 bg-white/10 rounded text-xs"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editing ? (
|
||||||
|
<textarea
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
className="flex-1 w-full bg-black/30 border border-white/20 rounded p-3 text-white text-sm font-mono resize-none"
|
||||||
|
placeholder="Write your notes in Markdown..."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 bg-black/30 border border-white/20 rounded p-3 text-white text-sm overflow-y-auto prose prose-invert prose-sm max-w-none">
|
||||||
|
<pre className="whitespace-pre-wrap font-sans">{content}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-64 flex items-center justify-center text-white/50">
|
||||||
|
Select an agent and note to view
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface ChangeLogEntry {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
agent: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_ICONS: Record<string, string> = {
|
||||||
|
skill_update: "🧠",
|
||||||
|
template_change: "📝",
|
||||||
|
new_feature: "✨",
|
||||||
|
bug_fix: "🐛"
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_COLORS: Record<string, string> = {
|
||||||
|
skill_update: "#8b5cf6",
|
||||||
|
template_change: "#ec4899",
|
||||||
|
new_feature: "#10b981",
|
||||||
|
bug_fix: "#ef4444"
|
||||||
|
};
|
||||||
|
|
||||||
|
const AGENT_NAMES: Record<string, string> = {
|
||||||
|
thoth: "Thoth",
|
||||||
|
"thoth-trading": "Thoth Trading",
|
||||||
|
ptah: "Ptah",
|
||||||
|
seshat: "Seshat",
|
||||||
|
anubis: "Anubis",
|
||||||
|
sekhmet: "Sekhmet",
|
||||||
|
horus: "Horus"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChangeLogPanel() {
|
||||||
|
const [entries, setEntries] = useState<ChangeLogEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const [newEntry, setNewEntry] = useState({
|
||||||
|
agent: "horus",
|
||||||
|
type: "new_feature",
|
||||||
|
description: "",
|
||||||
|
version: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchChangelog();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchChangelog = async () => {
|
||||||
|
const res = await fetch("/api/changelog?limit=50");
|
||||||
|
const data = await res.json();
|
||||||
|
setEntries(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEntry = async () => {
|
||||||
|
if (!newEntry.description.trim()) return;
|
||||||
|
|
||||||
|
await fetch("/api/changelog", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: "add",
|
||||||
|
entry: newEntry
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewEntry({ agent: "horus", type: "new_feature", description: "", version: "" });
|
||||||
|
setShowAdd(false);
|
||||||
|
fetchChangelog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEntry = async (entryId: string) => {
|
||||||
|
if (!confirm("Delete this entry?")) return;
|
||||||
|
|
||||||
|
await fetch("/api/changelog", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "delete", entryId })
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchChangelog();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4 text-white/50">Loading changelog...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold">📝 Change Log</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdd(!showAdd)}
|
||||||
|
className="px-3 py-1 bg-brand-pink hover:bg-[#ff7bc0] rounded text-sm font-medium"
|
||||||
|
>
|
||||||
|
{showAdd ? "✕ Cancel" : "+ Add Entry"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Entry Form */}
|
||||||
|
{showAdd && (
|
||||||
|
<div className="p-4 bg-white/10 rounded-lg space-y-3">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<select
|
||||||
|
value={newEntry.agent}
|
||||||
|
onChange={(e) => setNewEntry({ ...newEntry, agent: e.target.value })}
|
||||||
|
className="bg-black/30 border border-white/20 rounded px-3 py-2 text-white"
|
||||||
|
>
|
||||||
|
{Object.entries(AGENT_NAMES).map(([id, name]) => (
|
||||||
|
<option key={id} value={id}>{name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={newEntry.type}
|
||||||
|
onChange={(e) => setNewEntry({ ...newEntry, type: e.target.value })}
|
||||||
|
className="bg-black/30 border border-white/20 rounded px-3 py-2 text-white"
|
||||||
|
>
|
||||||
|
<option value="new_feature">✨ New Feature</option>
|
||||||
|
<option value="skill_update">🧠 Skill Update</option>
|
||||||
|
<option value="template_change">📝 Template Change</option>
|
||||||
|
<option value="bug_fix">🐛 Bug Fix</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newEntry.version}
|
||||||
|
onChange={(e) => setNewEntry({ ...newEntry, version: e.target.value })}
|
||||||
|
placeholder="Version (optional, e.g., v2.0)"
|
||||||
|
className="w-full bg-black/30 border border-white/20 rounded px-3 py-2 text-white placeholder-white/50"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={newEntry.description}
|
||||||
|
onChange={(e) => setNewEntry({ ...newEntry, description: e.target.value })}
|
||||||
|
placeholder="What changed?"
|
||||||
|
rows={3}
|
||||||
|
className="w-full bg-black/30 border border-white/20 rounded px-3 py-2 text-white placeholder-white/50"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={addEntry}
|
||||||
|
className="w-full py-2 bg-green-600 hover:bg-green-700 rounded font-medium"
|
||||||
|
>
|
||||||
|
Save Entry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Entries List */}
|
||||||
|
<div className="space-y-3 max-h-[500px] overflow-y-auto">
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
className="p-3 bg-white/5 rounded-lg border border-white/10"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="px-2 py-0.5 rounded text-xs"
|
||||||
|
style={{ backgroundColor: TYPE_COLORS[entry.type] || "#666" }}
|
||||||
|
>
|
||||||
|
{TYPE_ICONS[entry.type] || "📌"} {entry.type.replace("_", " ")}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/50 text-xs">
|
||||||
|
{new Date(entry.date).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteEntry(entry.id)}
|
||||||
|
className="text-white/30 hover:text-red-400 text-xs"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-2 text-white/90">{entry.description}</p>
|
||||||
|
|
||||||
|
<div className="flex gap-3 mt-2 text-xs text-white/50">
|
||||||
|
<span>{AGENT_NAMES[entry.agent] || entry.agent}</span>
|
||||||
|
{entry.version && <span>v{entry.version}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{entries.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-white/50">
|
||||||
|
No changes recorded yet. Add an entry to track agent improvements.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface ExecutionLog {
|
||||||
|
id: string;
|
||||||
|
templateId: string;
|
||||||
|
agent: string;
|
||||||
|
taskTitle: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
status: string;
|
||||||
|
output?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AGENT_COLORS: Record<string, string> = {
|
||||||
|
thoth: "#8b5cf6",
|
||||||
|
"thoth-trading": "#f59e0b",
|
||||||
|
ptah: "#10b981",
|
||||||
|
seshat: "#ec4899",
|
||||||
|
anubis: "#6366f1",
|
||||||
|
sekhmet: "#ef4444"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ExecutionLogsPanel() {
|
||||||
|
const [logs, setLogs] = useState<ExecutionLog[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filterAgent, setFilterAgent] = useState<string>("all");
|
||||||
|
const [selectedLog, setSelectedLog] = useState<ExecutionLog | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs();
|
||||||
|
const interval = setInterval(fetchLogs, 30000); // Refresh every 30s
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
const res = await fetch("/api/execution-logs?limit=50");
|
||||||
|
const data = await res.json();
|
||||||
|
setLogs(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "running": return "text-yellow-400 bg-yellow-400/20";
|
||||||
|
case "completed": return "text-green-400 bg-green-400/20";
|
||||||
|
case "failed": return "text-red-400 bg-red-400/20";
|
||||||
|
default: return "text-white/50 bg-white/10";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredLogs = filterAgent === "all"
|
||||||
|
? logs
|
||||||
|
: logs.filter(l => l.agent === filterAgent);
|
||||||
|
|
||||||
|
const agents = [...new Set(logs.map(l => l.agent))];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4 text-white/50">Loading logs...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-lg font-semibold">📊 Execution Logs</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<select
|
||||||
|
value={filterAgent}
|
||||||
|
onChange={(e) => setFilterAgent(e.target.value)}
|
||||||
|
className="bg-black/30 border border-white/20 rounded px-3 py-1 text-sm text-white"
|
||||||
|
>
|
||||||
|
<option value="all">All Agents</option>
|
||||||
|
{agents.map(agent => (
|
||||||
|
<option key={agent} value={agent}>{agent}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={fetchLogs}
|
||||||
|
className="px-3 py-1 bg-white/10 hover:bg-white/20 rounded text-sm"
|
||||||
|
>
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log List */}
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{filteredLogs.map((log) => (
|
||||||
|
<div
|
||||||
|
key={log.id}
|
||||||
|
onClick={() => setSelectedLog(log)}
|
||||||
|
className={`p-3 rounded-lg cursor-pointer transition ${
|
||||||
|
selectedLog?.id === log.id
|
||||||
|
? "bg-brand-pink/20 border border-brand-pink"
|
||||||
|
: "bg-white/5 border border-white/10 hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className="px-2 py-0.5 rounded text-xs"
|
||||||
|
style={{ backgroundColor: AGENT_COLORS[log.agent] || "#666" }}
|
||||||
|
>
|
||||||
|
{log.agent}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">{log.taskTitle}</span>
|
||||||
|
</div>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${getStatusColor(log.status)}`}>
|
||||||
|
{log.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 mt-1 text-xs text-white/40">
|
||||||
|
<span>{new Date(log.startedAt).toLocaleString()}</span>
|
||||||
|
{log.completedAt && (
|
||||||
|
<span>Duration: {Math.round((new Date(log.completedAt).getTime() - new Date(log.startedAt).getTime()) / 1000)}s</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredLogs.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-white/50">
|
||||||
|
No execution logs yet. Run a task to see logs here.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log Detail */}
|
||||||
|
{selectedLog && (
|
||||||
|
<div className="mt-4 p-4 bg-black/30 rounded-lg border border-white/10">
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<h4 className="font-medium">{selectedLog.taskTitle}</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedLog(null)}
|
||||||
|
className="text-white/50 hover:text-white"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-white/50">Agent:</span>{" "}
|
||||||
|
<span style={{ color: AGENT_COLORS[selectedLog.agent] }}>
|
||||||
|
{selectedLog.agent}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/50">Status:</span>{" "}
|
||||||
|
<span className={getStatusColor(selectedLog.status)}>
|
||||||
|
{selectedLog.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-white/50">Started:</span>{" "}
|
||||||
|
{new Date(selectedLog.startedAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
{selectedLog.completedAt && (
|
||||||
|
<div>
|
||||||
|
<span className="text-white/50">Completed:</span>{" "}
|
||||||
|
{new Date(selectedLog.completedAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedLog.output && (
|
||||||
|
<div>
|
||||||
|
<span className="text-white/50 text-sm">Output:</span>
|
||||||
|
<pre className="mt-1 p-2 bg-black/50 rounded text-xs text-white/70 overflow-x-auto">
|
||||||
|
{selectedLog.output}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.error && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-red-400 text-sm">Error:</span>
|
||||||
|
<pre className="mt-1 p-2 bg-red-900/20 rounded text-xs text-red-300 overflow-x-auto">
|
||||||
|
{selectedLog.error}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,10 @@ import { TaskHistoryPanel } from "./TaskHistoryPanel";
|
|||||||
import { TradingPanel } from "./TradingPanel";
|
import { TradingPanel } from "./TradingPanel";
|
||||||
import AIManagement from "@/components/ai-management/AIManagement";
|
import AIManagement from "@/components/ai-management/AIManagement";
|
||||||
import Council from "@/components/council/Council";
|
import Council from "@/components/council/Council";
|
||||||
|
import AutoRunPanel from "./AutoRunPanel";
|
||||||
|
import ExecutionLogsPanel from "./ExecutionLogsPanel";
|
||||||
|
import ChangeLogPanel from "./ChangeLogPanel";
|
||||||
|
import BrainownPanel from "./BrainownPanel";
|
||||||
|
|
||||||
interface SidebarItem {
|
interface SidebarItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -59,6 +63,12 @@ const sidebarCategories: SidebarCategory[] = [
|
|||||||
{ id: "daily-feedback", name: "Daily Feedback", icon: "📝", category: "daily-feedback" },
|
{ id: "daily-feedback", name: "Daily Feedback", icon: "📝", category: "daily-feedback" },
|
||||||
{ id: "ai-settings", name: "AI Settings", icon: "🤖", category: "council-settings" },
|
{ id: "ai-settings", name: "AI Settings", icon: "🤖", category: "council-settings" },
|
||||||
]},
|
]},
|
||||||
|
{ id: "automation", name: "Automation", icon: "⚡", items: [
|
||||||
|
{ id: "autorun", name: "Auto-Run", icon: "🔄", category: "autorun" },
|
||||||
|
{ id: "execution-logs", name: "Exec Logs", icon: "📊", category: "execution-logs" },
|
||||||
|
{ id: "changelog", name: "Change Log", icon: "📝", category: "changelog" },
|
||||||
|
{ id: "brainown", name: "Brainown", icon: "🧠", category: "brainown" },
|
||||||
|
]},
|
||||||
{ 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: "briefs", name: "All Briefs", icon: "📋", category: "briefs" },
|
||||||
@@ -426,6 +436,31 @@ export default function MissionControlDashboard({ onLogout }: MissionControlDash
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* BMHQ Automation Panels */}
|
||||||
|
{currentItem?.category === "autorun" && (
|
||||||
|
<div className="rounded-xl border border-green-500/30 bg-gradient-to-br from-[#1a2a1a] to-[#1a2a2a] p-6">
|
||||||
|
<AutoRunPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "execution-logs" && (
|
||||||
|
<div className="rounded-xl border border-blue-500/30 bg-gradient-to-br from-[#1a1a2a] to-[#1a2a3a] p-6">
|
||||||
|
<ExecutionLogsPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "changelog" && (
|
||||||
|
<div className="rounded-xl border border-purple-500/30 bg-gradient-to-br from-[#2a1a2a] to-[#2a1a3a] p-6">
|
||||||
|
<ChangeLogPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentItem?.category === "brainown" && (
|
||||||
|
<div className="rounded-xl border border-pink-500/30 bg-gradient-to-br from-[#2a1a2a] to-[#3a2a3a] p-6">
|
||||||
|
<BrainownPanel />
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"videoId": "uUN1oy2PRHo",
|
||||||
|
"videoUrl": "https://www.youtube.com/watch?v=uUN1oy2PRHo",
|
||||||
|
"title": "Managing OpenClaw Agents Like The Creator",
|
||||||
|
"transcript": "[Transcript pending - YouTube API blocked on server. Please paste transcript manually]",
|
||||||
|
"categories": [
|
||||||
|
"OpenClaw",
|
||||||
|
"Business",
|
||||||
|
"Personal Growth"
|
||||||
|
],
|
||||||
|
"savedAt": "2026-02-27T14:25:38.296Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"videoId": "b6X-FaRBrLc",
|
||||||
|
"videoUrl": "https://www.youtube.com/watch?v=b6X-FaRBrLc",
|
||||||
|
"title": "OpenClaw AI Agent Setup Tutorial",
|
||||||
|
"transcript": "[Transcript pending - YouTube API blocked on server. Please paste transcript manually]",
|
||||||
|
"categories": [
|
||||||
|
"OpenClaw",
|
||||||
|
"Inspiration",
|
||||||
|
"Technical"
|
||||||
|
],
|
||||||
|
"savedAt": "2026-02-27T14:25:33.289Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Auto-Run Recurring Task System
|
||||||
|
|
||||||
|
export interface RecurringTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
agent: string; // thoth, ptah, seshat, anubis, etc.
|
||||||
|
project: string;
|
||||||
|
schedule: {
|
||||||
|
type: 'hourly' | 'daily' | 'weekly' | 'monthly';
|
||||||
|
time?: string; // HH:MM format for daily
|
||||||
|
dayOfWeek?: number; // 0-6 for weekly
|
||||||
|
dayOfMonth?: number; // 1-31 for monthly
|
||||||
|
timezone: string; // Europe/Madrid
|
||||||
|
};
|
||||||
|
taskTemplate: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||||
|
};
|
||||||
|
preInstructions?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
lastRun?: string;
|
||||||
|
nextRun?: string;
|
||||||
|
runCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionLog {
|
||||||
|
id: string;
|
||||||
|
templateId: string;
|
||||||
|
agent: string;
|
||||||
|
taskTitle: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt?: string;
|
||||||
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||||
|
output?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeLogEntry {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
agent: string;
|
||||||
|
type: 'skill_update' | 'template_change' | 'new_feature' | 'bug_fix';
|
||||||
|
description: string;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined SiteMente Agent Tasks
|
||||||
|
export const defaultRecurringTemplates: RecurringTemplate[] = [
|
||||||
|
// Thoth - Daily Research
|
||||||
|
{
|
||||||
|
id: 'thoth-daily-research',
|
||||||
|
name: 'Thoth - Daily SiteMente Research',
|
||||||
|
description: 'Research market trends, competitors, and opportunities',
|
||||||
|
agent: 'thoth',
|
||||||
|
project: 'sitemente',
|
||||||
|
schedule: { type: 'daily', time: '09:00', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Daily SiteMente Research',
|
||||||
|
description: 'Research: competitors, AI trends, pricing, local businesses',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
preInstructions: 'Focus on: 1) Competitor changes, 2) New AI features, 3) Local lead opportunities',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
},
|
||||||
|
// Ptah - Infra Monitoring
|
||||||
|
{
|
||||||
|
id: 'ptah-infra-monitor',
|
||||||
|
name: 'Ptah - Infrastructure Monitoring',
|
||||||
|
description: 'Check server health, uptime, and security',
|
||||||
|
agent: 'ptah',
|
||||||
|
project: 'infrastructure',
|
||||||
|
schedule: { type: 'hourly', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Infra Health Check',
|
||||||
|
description: 'Check: CPU, memory, disk, services, SSL certs, security logs',
|
||||||
|
priority: 'critical'
|
||||||
|
},
|
||||||
|
preInstructions: 'Report any issues immediately to Horus',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
},
|
||||||
|
// Seshat - Content Pipeline
|
||||||
|
{
|
||||||
|
id: 'seshat-content-pipeline',
|
||||||
|
name: 'Seshat - Content Pipeline',
|
||||||
|
description: 'Generate and schedule content',
|
||||||
|
agent: 'seshat',
|
||||||
|
project: 'sitemente',
|
||||||
|
schedule: { type: 'daily', time: '09:00', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Daily Content Development',
|
||||||
|
description: 'Create: blog post, social content, newsletter draft',
|
||||||
|
priority: 'medium'
|
||||||
|
},
|
||||||
|
preInstructions: 'Focus on SEO, value for local businesses',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
},
|
||||||
|
// Anubis - Outreach Scan
|
||||||
|
{
|
||||||
|
id: 'anubis-outreach-scan',
|
||||||
|
name: 'Anubis - Outreach Opportunities',
|
||||||
|
description: 'Find new leads and opportunities',
|
||||||
|
agent: 'anubis',
|
||||||
|
project: 'sitemente',
|
||||||
|
schedule: { type: 'weekly', dayOfWeek: 1, time: '10:00', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Weekly Outreach Scan',
|
||||||
|
description: 'Find: new restaurants, clinics, real estate agencies to contact',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
preInstructions: 'Focus on Benalmádena and Costa del Sol area',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
},
|
||||||
|
// Thoth Trading - Market Scan
|
||||||
|
{
|
||||||
|
id: 'thoth-trading-market',
|
||||||
|
name: 'Thoth Trading - Market Scan',
|
||||||
|
description: 'Scan crypto markets for opportunities',
|
||||||
|
agent: 'thoth-trading',
|
||||||
|
project: 'trading',
|
||||||
|
schedule: { type: 'hourly', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Market Scan',
|
||||||
|
description: 'Check: BTC, ETH, SOL prices and trends',
|
||||||
|
priority: 'medium'
|
||||||
|
},
|
||||||
|
preInstructions: 'Report significant moves (>5%)',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
},
|
||||||
|
// Sekhmet - Risk Alerts
|
||||||
|
{
|
||||||
|
id: 'sekhmet-risk-alerts',
|
||||||
|
name: 'Sekhmet - Risk Alerts',
|
||||||
|
description: 'Monitor trading positions and risk',
|
||||||
|
agent: 'sekhmet',
|
||||||
|
project: 'trading',
|
||||||
|
schedule: { type: 'hourly', timezone: 'Europe/Madrid' },
|
||||||
|
taskTemplate: {
|
||||||
|
title: 'Risk Check',
|
||||||
|
description: 'Check: open positions, drawdown, risk levels',
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
preInstructions: 'Alert if drawdown >10% or positions at risk',
|
||||||
|
enabled: false,
|
||||||
|
runCount: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user