Compare commits

...

17 Commits

Author SHA1 Message Date
Neo
ca1a3f88f6 Add Dockerfile with HOST env var 2026-02-19 13:12:56 +00:00
Neo
9f963a7914 Add HOST env var for Next.js 2026-02-19 13:12:11 +00:00
Neo
634adcc005 Fix: add use client directive for React hooks 2026-02-19 13:01:24 +00:00
Neo
05b05bc8a5 Static HTML fallback 2026-02-19 12:28:27 +00:00
Neo
498e38697c Simple React page 2026-02-19 12:26:04 +00:00
Neo
457e652310 Simple debug view 2026-02-19 12:06:20 +00:00
Neo
e92e576d22 Simplify server stats for now 2026-02-19 12:01:33 +00:00
Neo
f52cdd926c Force rebuild 2026-02-19 11:59:30 +00:00
Neo
429e803ea6 Fix uptime command for BusyBox 2026-02-19 11:48:18 +00:00
Neo
457ad7234b Real server stats via docker socket 2026-02-19 11:44:56 +00:00
Neo
25c749b6ee Fix cron path: read from shared-context 2026-02-19 11:31:34 +00:00
Neo
a6a627da17 Add server, backup, agent, whatsapp status tabs 2026-02-19 11:25:43 +00:00
Neo
d194771a3e Mobile: smaller fonts, single column, less padding 2026-02-19 11:11:02 +00:00
Neo
f752b7a11c Responsive: grid and padding adjustments 2026-02-19 11:10:09 +00:00
Neo
7ea4e022be Fix: plain JS in next.config.mjs 2026-02-19 10:29:14 +00:00
Neo
555d2a001d Redesign: Rich cyberpunk aesthetic with animations 2026-02-19 10:09:32 +00:00
Neo
37acbd2945 Fix: next.config.mjs + use client directive 2026-02-19 09:50:21 +00:00
8 changed files with 212 additions and 254 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
HOST=0.0.0.0

8
Dockerfile Normal file
View 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"]

View File

@@ -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 });

View File

@@ -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); }
} }

View File

@@ -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
View File

@@ -0,0 +1,5 @@
const nextConfig = {
output: 'standalone',
};
export default nextConfig;

View File

@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: 'standalone',
};
export default nextConfig;

37
public/index.html Normal file
View 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>