Compare commits
17 Commits
7e8d072880
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1a3f88f6 | ||
|
|
9f963a7914 | ||
|
|
634adcc005 | ||
|
|
05b05bc8a5 | ||
|
|
498e38697c | ||
|
|
457e652310 | ||
|
|
e92e576d22 | ||
|
|
f52cdd926c | ||
|
|
429e803ea6 | ||
|
|
457ad7234b | ||
|
|
25c749b6ee | ||
|
|
a6a627da17 | ||
|
|
d194771a3e | ||
|
|
f752b7a11c | ||
|
|
7ea4e022be | ||
|
|
555d2a001d | ||
|
|
37acbd2945 |
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY . .
|
||||||
|
RUN npm install && npm run build
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", ".next/standalone/server.js"]
|
||||||
@@ -4,29 +4,35 @@ import * as path from 'path';
|
|||||||
|
|
||||||
const DATA_DIR = '/app/data';
|
const DATA_DIR = '/app/data';
|
||||||
|
|
||||||
// Tasks API
|
// Types
|
||||||
|
type CronJob = { name: string; enabled: boolean; status: string };
|
||||||
|
type Task = { id: string; title: string; created: string; priority: string; status: string; assignee: string };
|
||||||
|
type Memory = { id: string; title: string; date: string; preview: string };
|
||||||
|
|
||||||
|
// Helper to read files
|
||||||
|
function readDir(dir: string, pattern: string = '.md'): string[] {
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
return fs.readdirSync(dir).filter(f => f.endsWith(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const type = searchParams.get('type') || 'tasks';
|
const type = searchParams.get('type') || 'tasks';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// TASKS
|
||||||
if (type === 'tasks') {
|
if (type === 'tasks') {
|
||||||
const tasksDir = path.join(DATA_DIR, 'shared-context/jelena-neo-tasks/archive');
|
const tasksDir = path.join(DATA_DIR, 'shared-context/jelena-neo-tasks/archive');
|
||||||
const tasks: any[] = [];
|
const tasks: Task[] = [];
|
||||||
|
|
||||||
if (fs.existsSync(tasksDir)) {
|
if (fs.existsSync(tasksDir)) {
|
||||||
const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
for (const file of readDir(tasksDir).slice(0, 20)) {
|
||||||
for (const file of files) {
|
|
||||||
const content = fs.readFileSync(path.join(tasksDir, file), 'utf-8');
|
const content = fs.readFileSync(path.join(tasksDir, file), 'utf-8');
|
||||||
const title = content.match(/^# Task: (.+)$/m)?.[1] || file;
|
|
||||||
const created = content.match(/Created: (.+)$/m)?.[1] || '';
|
|
||||||
const priority = content.match(/Priority: (.+)$/m)?.[1] || 'normal';
|
|
||||||
|
|
||||||
tasks.push({
|
tasks.push({
|
||||||
id: file.replace('.md', ''),
|
id: file.replace('.md', ''),
|
||||||
title,
|
title: content.match(/^# Task: (.+)$/m)?.[1] || file,
|
||||||
created,
|
created: content.match(/Created: (.+)$/m)?.[1] || '',
|
||||||
priority,
|
priority: content.match(/Priority: (.+)$/m)?.[1] || 'normal',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
assignee: 'neo'
|
assignee: 'neo'
|
||||||
});
|
});
|
||||||
@@ -35,43 +41,101 @@ export async function GET(request: Request) {
|
|||||||
return NextResponse.json(tasks);
|
return NextResponse.json(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRONS - from shared-context
|
||||||
if (type === 'crons') {
|
if (type === 'crons') {
|
||||||
const cronFile = path.join(DATA_DIR, 'cron/jobs.json');
|
// Try multiple paths for cron jobs
|
||||||
if (fs.existsSync(cronFile)) {
|
const cronPaths = [
|
||||||
const data = JSON.parse(fs.readFileSync(cronFile, 'utf-8'));
|
path.join(DATA_DIR, 'shared-context/.openclaw/cron/jobs.json'),
|
||||||
const jobs = (data.jobs || []).map((job: any) => ({
|
path.join(DATA_DIR, 'workspace-neo/.openclaw/cron/jobs.json'),
|
||||||
name: job.name,
|
];
|
||||||
enabled: job.enabled,
|
|
||||||
status: job.state?.lastStatus || 'unknown',
|
for (const cronFile of cronPaths) {
|
||||||
lastRun: job.state?.lastRunAtMs ? new Date(job.state.lastRunAtMs).toISOString() : null
|
if (fs.existsSync(cronFile)) {
|
||||||
}));
|
const data = JSON.parse(fs.readFileSync(cronFile, 'utf-8'));
|
||||||
return NextResponse.json(jobs);
|
const jobs: CronJob[] = (data.jobs || []).map((job: any) => ({
|
||||||
|
name: job.name,
|
||||||
|
enabled: job.enabled,
|
||||||
|
status: job.state?.lastStatus || 'unknown'
|
||||||
|
}));
|
||||||
|
return NextResponse.json(jobs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return NextResponse.json([]);
|
return NextResponse.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MEMORY
|
||||||
if (type === 'memory') {
|
if (type === 'memory') {
|
||||||
const memoryDir = path.join(DATA_DIR, 'memory');
|
const memoryDir = path.join(DATA_DIR, 'memory');
|
||||||
const memories: any[] = [];
|
const memories: Memory[] = [];
|
||||||
|
|
||||||
if (fs.existsSync(memoryDir)) {
|
if (fs.existsSync(memoryDir)) {
|
||||||
const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
|
for (const file of readDir(memoryDir).slice(0, 20)) {
|
||||||
for (const file of files.slice(0, 20)) {
|
|
||||||
const content = fs.readFileSync(path.join(memoryDir, file), 'utf-8');
|
const content = fs.readFileSync(path.join(memoryDir, file), 'utf-8');
|
||||||
const title = content.match(/^# (.+)$/m)?.[1] || file;
|
|
||||||
const preview = content.substring(0, 150).replace(/[#*]/g, '');
|
|
||||||
|
|
||||||
memories.push({
|
memories.push({
|
||||||
id: file.replace('.md', ''),
|
id: file.replace('.md', ''),
|
||||||
title,
|
title: content.match(/^# (.+)$/m)?.[1] || file,
|
||||||
date: file.substring(0, 10),
|
date: file.substring(0, 10),
|
||||||
preview
|
preview: content.substring(0, 150).replace(/[#*]/g, '')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NextResponse.json(memories);
|
return NextResponse.json(memories);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SERVER STATUS - basic data only
|
||||||
|
if (type === 'server') {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'ok',
|
||||||
|
note: 'Full stats coming soon'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// BACKUP STATUS
|
||||||
|
if (type === 'backups') {
|
||||||
|
const backups: any[] = [];
|
||||||
|
|
||||||
|
// Git backup
|
||||||
|
const gitBackupDir = path.join(DATA_DIR, 'workspace-neo/.git');
|
||||||
|
if (fs.existsSync(gitBackupDir)) {
|
||||||
|
const stats = fs.statSync(gitBackupDir);
|
||||||
|
backups.push({ name: 'Git Config', status: 'ok', lastRun: 'Today 10:49' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage backup
|
||||||
|
const storageDir = path.join(DATA_DIR, '../openclaw/jelena/backups/config');
|
||||||
|
if (fs.existsSync(storageDir)) {
|
||||||
|
const files = fs.readdirSync(storageDir);
|
||||||
|
if (files.length > 0) {
|
||||||
|
const latest = files.sort().pop();
|
||||||
|
backups.push({ name: 'Storage Box', status: 'ok', lastRun: latest?.replace('.tar.gz', '') || 'unknown' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(backups);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AGENT STATUS
|
||||||
|
if (type === 'agents') {
|
||||||
|
return NextResponse.json([
|
||||||
|
{ name: 'Jelena', role: 'Chief of Staff', status: 'online' },
|
||||||
|
{ name: 'Neo', role: 'CTO / DevOps', status: 'online' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHATSAPP BACKUP
|
||||||
|
if (type === 'whatsapp') {
|
||||||
|
const waDir = path.join(DATA_DIR, '../openclaw/jelena/whatsapp-chats');
|
||||||
|
if (fs.existsSync(waDir)) {
|
||||||
|
const chats = fs.readdirSync(waDir);
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'ok',
|
||||||
|
chats: chats.length,
|
||||||
|
lastUpdate: 'Today'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NextResponse.json({ status: 'not_configured', chats: 0, lastUpdate: 'Never' });
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: 'Unknown type' }, { status: 400 });
|
return NextResponse.json({ error: 'Unknown type' }, { status: 400 });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||||
|
|||||||
@@ -1,10 +1,53 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
html, body {
|
||||||
background: #0f0f23;
|
background: #0a0a0f;
|
||||||
color: white;
|
color: #e4e4e7;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255,255,255,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection */
|
||||||
|
::selection {
|
||||||
|
background: rgba(255, 0, 80, 0.3);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 20px rgba(255, 0, 80, 0.3); }
|
||||||
|
50% { box-shadow: 0 0 40px rgba(255, 0, 80, 0.5); }
|
||||||
}
|
}
|
||||||
|
|||||||
237
app/page.tsx
237
app/page.tsx
@@ -1,229 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
// Types
|
export default function Page() {
|
||||||
type TaskStatus = 'todo' | 'in-progress' | 'done';
|
const [tab, setTab] = useState('tasks');
|
||||||
type Task = { id: string; title: string; status: TaskStatus; assignee: 'me' | 'jelena' | 'neo'; priority?: string; created?: string };
|
const [data, setData] = useState<any>(null);
|
||||||
|
|
||||||
type ContentStage = 'idea' | 'script' | 'thumbnail' | 'filming' | 'done';
|
|
||||||
type ContentItem = { id: string; title: string; stage: ContentStage };
|
|
||||||
|
|
||||||
type CronJob = { name: string; enabled: boolean; status: string; lastRun?: string };
|
|
||||||
|
|
||||||
type Memory = { id: string; title: string; date: string; preview: string };
|
|
||||||
|
|
||||||
type TeamMember = { id: string; name: string; role: string; status: 'working' | 'idle' };
|
|
||||||
|
|
||||||
export default function MissionControl() {
|
|
||||||
const [activeTab, setActiveTab] = useState('tasks');
|
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
|
||||||
const [crons, setCrons] = useState<CronJob[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
setLoading(true);
|
||||||
try {
|
fetch('/api/data?type=' + tab)
|
||||||
// Fetch tasks from mounted volume
|
|
||||||
const tasksRes = await fetch('/api/data?type=tasks');
|
|
||||||
const tasksData = await tasksRes.json();
|
|
||||||
setTasks(tasksData);
|
|
||||||
|
|
||||||
// Fetch cron jobs
|
|
||||||
const cronsRes = await fetch('/api/data?type=crons');
|
|
||||||
const cronsData = await cronsRes.json();
|
|
||||||
setCrons(cronsData);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch data:', e);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ fontFamily: 'system-ui, sans-serif', minHeight: '100vh', background: '#0f0f23', color: 'white' }}>
|
|
||||||
{/* Header */}
|
|
||||||
<header style={{ padding: '20px 40px', borderBottom: '1px solid #1e1e3f', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<h1 style={{ fontSize: '1.8rem', fontWeight: 700 }}>🎯 Mission Control</h1>
|
|
||||||
<nav style={{ display: 'flex', gap: '10px' }}>
|
|
||||||
{['tasks', 'crons', 'team', 'memory'].map(tab => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
style={{
|
|
||||||
padding: '10px 20px',
|
|
||||||
background: activeTab === tab ? '#e94560' : 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '8px',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer',
|
|
||||||
textTransform: 'capitalize'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<main style={{ padding: '40px' }}>
|
|
||||||
{loading ? (
|
|
||||||
<p style={{ color: '#9ca3af' }}>Loading live data...</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{activeTab === 'tasks' && <TasksBoard tasks={tasks} />}
|
|
||||||
{activeTab === 'crons' && <CronMonitor crons={crons} />}
|
|
||||||
{activeTab === 'team' && <Team />}
|
|
||||||
{activeTab === 'memory' && <Memory />}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ TASKS BOARD ============
|
|
||||||
function TasksBoard({ tasks }: { tasks: Task[] }) {
|
|
||||||
const columns: { status: TaskStatus; label: string; color: string }[] = [
|
|
||||||
{ status: 'todo', label: 'To Do', color: '#6b7280' },
|
|
||||||
{ status: 'in-progress', label: 'In Progress', color: '#e94560' },
|
|
||||||
{ status: 'done', label: 'Done', color: '#10b981' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px' }}>
|
|
||||||
{columns.map(col => (
|
|
||||||
<div key={col.status} style={{ background: '#1a1a2e', borderRadius: '12px', padding: '20px' }}>
|
|
||||||
<h3 style={{ color: col.color, marginBottom: '20px' }}>{col.label}</h3>
|
|
||||||
{tasks.filter(t => t.status === col.status).map(task => (
|
|
||||||
<div key={task.id} style={{ background: '#16213e', padding: '15px', borderRadius: '8px', marginBottom: '10px' }}>
|
|
||||||
<p>{task.title}</p>
|
|
||||||
<span style={{ fontSize: '0.8rem', color: '#9ca3af' }}>@{task.assignee}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{tasks.filter(t => t.status === col.status).length === 0 && (
|
|
||||||
<p style={{ color: '#4b5563', fontSize: '0.9rem' }}>No tasks</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ CRON MONITOR ============
|
|
||||||
function CronMonitor({ crons }: { crons: CronJob[] }) {
|
|
||||||
return (
|
|
||||||
<div style={{ background: '#1a1a2e', borderRadius: '12px', padding: '30px' }}>
|
|
||||||
<h2 style={{ marginBottom: '30px' }}>⏰ Cron Jobs</h2>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr style={{ borderBottom: '1px solid #2a2a4e' }}>
|
|
||||||
<th style={{ textAlign: 'left', padding: '15px', color: '#9ca3af' }}>Job</th>
|
|
||||||
<th style={{ textAlign: 'left', padding: '15px', color: '#9ca3af' }}>Status</th>
|
|
||||||
<th style={{ textAlign: 'left', padding: '15px', color: '#9ca3af' }}>Enabled</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{crons.map((cron, i) => (
|
|
||||||
<tr key={i} style={{ borderBottom: '1px solid #2a2a4e' }}>
|
|
||||||
<td style={{ padding: '15px' }}>{cron.name}</td>
|
|
||||||
<td style={{ padding: '15px' }}>
|
|
||||||
<span style={{
|
|
||||||
background: cron.status === 'ok' ? '#10b981' : '#ef4444',
|
|
||||||
padding: '4px 12px', borderRadius: '20px', fontSize: '0.8rem'
|
|
||||||
}}>
|
|
||||||
{cron.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '15px' }}>
|
|
||||||
{cron.enabled ? '✅' : '❌'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{crons.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3} style={{ padding: '20px', textAlign: 'center', color: '#6b7280' }}>
|
|
||||||
No cron data available
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ TEAM ============
|
|
||||||
function Team() {
|
|
||||||
const members: TeamMember[] = [
|
|
||||||
{ id: '1', name: 'Jelena', role: 'Chief of Staff', status: 'working' },
|
|
||||||
{ id: '2', name: 'Neo', role: 'CTO / DevOps', status: 'working' },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '20px' }}>
|
|
||||||
{members.map(member => (
|
|
||||||
<div key={member.id} style={{ background: '#1a1a2e', borderRadius: '12px', padding: '25px', display: 'flex', alignItems: 'center', gap: '15px' }}>
|
|
||||||
<div style={{
|
|
||||||
width: '50px', height: '50px', borderRadius: '50%',
|
|
||||||
background: member.status === 'working' ? '#10b981' : '#6b7280',
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '1.5rem'
|
|
||||||
}}>
|
|
||||||
{member.name[0]}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>{member.name}</h3>
|
|
||||||
<p style={{ color: '#9ca3af', fontSize: '0.9rem' }}>{member.role}</p>
|
|
||||||
<span style={{ color: member.status === 'working' ? '#10b981' : '#6b7280', fontSize: '0.8rem' }}>
|
|
||||||
{member.status === 'working' ? '● Working' : '○ Idle'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ MEMORY ============
|
|
||||||
function Memory() {
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const [memories, setMemories] = useState<Memory[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/data?type=memory')
|
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(setMemories)
|
.then(d => { setData(d); setLoading(false); })
|
||||||
.catch(() => {});
|
.catch(() => setLoading(false));
|
||||||
}, []);
|
}, [tab]);
|
||||||
|
|
||||||
const filtered = memories.filter(m => m.title.toLowerCase().includes(search.toLowerCase()));
|
const tabs = ['tasks','crons','server','backups','agents','whatsapp','memory'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{background:'#000',minHeight:'100vh',color:'#fff',fontFamily:'monospace',padding:'20px'}}>
|
||||||
<input
|
<h1 style={{fontSize:'20px',marginBottom:'15px'}}>MISSION CONTROL</h1>
|
||||||
type="text"
|
<div style={{display:'flex',gap:'5px',flexWrap:'wrap',marginBottom:'15px'}}>
|
||||||
placeholder="Search memories..."
|
{tabs.map(t => (
|
||||||
value={search}
|
<button key={t} onClick={()=>setTab(t)} style={{
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
padding:'8px 12px',background:tab===t?'#f0f': '#333',
|
||||||
style={{
|
color:'#fff',border:'none',borderRadius:'4px',fontSize:'12px'
|
||||||
width: '100%', padding: '15px', borderRadius: '8px', border: 'none',
|
}}>{t}</button>
|
||||||
background: '#1a1a2e', color: 'white', marginBottom: '30px', fontSize: '1rem'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '20px' }}>
|
|
||||||
{filtered.map(mem => (
|
|
||||||
<div key={mem.id} style={{ background: '#1a1a2e', borderRadius: '12px', padding: '20px' }}>
|
|
||||||
<h3>{mem.title}</h3>
|
|
||||||
<p style={{ color: '#9ca3af', fontSize: '0.9rem', marginTop: '10px' }}>{mem.preview}</p>
|
|
||||||
<span style={{ color: '#6b7280', fontSize: '0.8rem' }}>{mem.date}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
{filtered.length === 0 && (
|
|
||||||
<p style={{ color: '#6b7280' }}>No memories found</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre style={{background:'#111',padding:'15px',borderRadius:'8px',overflow:'auto'}}>
|
||||||
|
{loading ? 'Loading...' : JSON.stringify(data,null,2)}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
next.config.mjs
Normal file
5
next.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
output: 'standalone',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
37
public/index.html
Normal file
37
public/index.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Mission Control</title>
|
||||||
|
<style>
|
||||||
|
body { background: #000; color: #fff; font-family: monospace; padding: 20px; margin: 0; }
|
||||||
|
h1 { font-size: 18px; margin-bottom: 15px; }
|
||||||
|
.nav { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 15px; }
|
||||||
|
.nav button { padding: 8px 12px; background: #222; color: #888; border: none; cursor: pointer; font-size: 11px; }
|
||||||
|
.nav button.active { background: #f0f; color: #fff; }
|
||||||
|
pre { background: #111; padding: 15px; border-radius: 8px; overflow: auto; font-size: 10px; max-height: 70vh; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>MISSION CONTROL</h1>
|
||||||
|
<div class="nav" id="nav"></div>
|
||||||
|
<pre id="out">Loading...</pre>
|
||||||
|
<script>
|
||||||
|
var tabs = ['tasks','crons','server','backups','agents','whatsapp','memory'];
|
||||||
|
var cur = 'tasks';
|
||||||
|
function rn() {
|
||||||
|
document.getElementById('nav').innerHTML = tabs.map(function(t){
|
||||||
|
return '<button class="'+(t===cur?'active':'')+'" onclick="ld(\''+t+'\')">'+t+'</button>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
function ld(t) {
|
||||||
|
cur = t; rn();
|
||||||
|
document.getElementById('out').textContent = 'Loading...';
|
||||||
|
fetch('/api/data?type='+t).then(function(r){return r.json();}).then(function(d){
|
||||||
|
document.getElementById('out').textContent = JSON.stringify(d,null,2);
|
||||||
|
})["catch"](function(e){document.getElementById('out').textContent = 'Error: '+e.message;});
|
||||||
|
}
|
||||||
|
rn(); ld('tasks');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user