Connect to live data from mounted volumes
This commit is contained in:
79
app/api/data/route.ts
Normal file
79
app/api/data/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DATA_DIR = '/app/data';
|
||||
|
||||
// Tasks API
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const type = searchParams.get('type') || 'tasks';
|
||||
|
||||
try {
|
||||
if (type === 'tasks') {
|
||||
const tasksDir = path.join(DATA_DIR, 'shared-context/jelena-neo-tasks/archive');
|
||||
const tasks: any[] = [];
|
||||
|
||||
if (fs.existsSync(tasksDir)) {
|
||||
const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
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({
|
||||
id: file.replace('.md', ''),
|
||||
title,
|
||||
created,
|
||||
priority,
|
||||
status: 'done',
|
||||
assignee: 'neo'
|
||||
});
|
||||
}
|
||||
}
|
||||
return NextResponse.json(tasks);
|
||||
}
|
||||
|
||||
if (type === 'crons') {
|
||||
const cronFile = path.join(DATA_DIR, 'cron/jobs.json');
|
||||
if (fs.existsSync(cronFile)) {
|
||||
const data = JSON.parse(fs.readFileSync(cronFile, 'utf-8'));
|
||||
const jobs = (data.jobs || []).map((job: any) => ({
|
||||
name: job.name,
|
||||
enabled: job.enabled,
|
||||
status: job.state?.lastStatus || 'unknown',
|
||||
lastRun: job.state?.lastRunAtMs ? new Date(job.state.lastRunAtMs).toISOString() : null
|
||||
}));
|
||||
return NextResponse.json(jobs);
|
||||
}
|
||||
return NextResponse.json([]);
|
||||
}
|
||||
|
||||
if (type === 'memory') {
|
||||
const memoryDir = path.join(DATA_DIR, 'memory');
|
||||
const memories: any[] = [];
|
||||
|
||||
if (fs.existsSync(memoryDir)) {
|
||||
const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files.slice(0, 20)) {
|
||||
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({
|
||||
id: file.replace('.md', ''),
|
||||
title,
|
||||
date: file.substring(0, 10),
|
||||
preview
|
||||
});
|
||||
}
|
||||
}
|
||||
return NextResponse.json(memories);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown type' }, { status: 400 });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
230
app/page.tsx
230
app/page.tsx
@@ -1,16 +1,13 @@
|
||||
// Mission Control - Next.js App
|
||||
// Track tasks, content, calendar, memory, team, and office
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Types
|
||||
type TaskStatus = 'todo' | 'in-progress' | 'done';
|
||||
type Task = { id: string; title: string; status: TaskStatus; assignee: 'me' | 'jelena' | 'neo' };
|
||||
type Task = { id: string; title: string; status: TaskStatus; assignee: 'me' | 'jelena' | 'neo'; priority?: string; created?: string };
|
||||
|
||||
type ContentStage = 'idea' | 'script' | 'thumbnail' | 'filming' | 'done';
|
||||
type ContentItem = { id: string; title: string; stage: ContentStage; script?: string };
|
||||
type ContentItem = { id: string; title: string; stage: ContentStage };
|
||||
|
||||
type CalendarEvent = { id: string; title: string; date: string; type: 'cron' | 'scheduled' };
|
||||
type CronJob = { name: string; enabled: boolean; status: string; lastRun?: string };
|
||||
|
||||
type Memory = { id: string; title: string; date: string; preview: string };
|
||||
|
||||
@@ -18,6 +15,30 @@ type TeamMember = { id: string; name: string; role: string; status: 'working' |
|
||||
|
||||
export default function MissionControl() {
|
||||
const [activeTab, setActiveTab] = useState('tasks');
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [crons, setCrons] = useState<CronJob[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
// 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' }}>
|
||||
@@ -25,7 +46,7 @@ export default function MissionControl() {
|
||||
<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', 'content', 'calendar', 'memory', 'team', 'office'].map(tab => (
|
||||
{['tasks', 'crons', 'team', 'memory'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
@@ -47,26 +68,23 @@ export default function MissionControl() {
|
||||
|
||||
{/* Content */}
|
||||
<main style={{ padding: '40px' }}>
|
||||
{activeTab === 'tasks' && <TasksBoard />}
|
||||
{activeTab === 'content' && <ContentPipeline />}
|
||||
{activeTab === 'calendar' && <Calendar />}
|
||||
{activeTab === 'memory' && <Memory />}
|
||||
{activeTab === 'team' && <Team />}
|
||||
{activeTab === 'office' && <Office />}
|
||||
{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() {
|
||||
const [tasks, setTasks] = useState<Task[]>([
|
||||
{ id: '1', title: 'Fix git backup push', status: 'in-progress', assignee: 'neo' },
|
||||
{ id: '2', title: 'Deploy nodecrew landing page', status: 'done', assignee: 'neo' },
|
||||
{ id: '3', title: 'Fix YouTube backup', status: 'todo', assignee: 'neo' },
|
||||
{ id: '4', title: 'Setup Mission Control', status: 'in-progress', assignee: 'jelena' },
|
||||
]);
|
||||
|
||||
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' },
|
||||
@@ -84,129 +102,63 @@ function TasksBoard() {
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ CONTENT PIPELINE ============
|
||||
function ContentPipeline() {
|
||||
const [items, setItems] = useState<ContentItem[]>([
|
||||
{ id: '1', title: 'ManoonOils Ad Copy', stage: 'script' },
|
||||
{ id: '2', title: 'AI Agency Promo', stage: 'idea' },
|
||||
{ id: '3', title: 'Product Launch Video', stage: 'thumbnail' },
|
||||
]);
|
||||
|
||||
const stages: { stage: ContentStage; label: string }[] = [
|
||||
{ stage: 'idea', label: '💡 Ideas' },
|
||||
{ stage: 'script', label: '📝 Script' },
|
||||
{ stage: 'thumbnail', label: '🖼️ Thumbnail' },
|
||||
{ stage: 'filming', label: '🎬 Filming' },
|
||||
{ stage: 'done', label: '✅ Done' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '15px', overflowX: 'auto', paddingBottom: '20px' }}>
|
||||
{stages.map(s => (
|
||||
<div key={s.stage} style={{ minWidth: '250px', background: '#1a1a2e', borderRadius: '12px', padding: '20px' }}>
|
||||
<h3 style={{ marginBottom: '15px' }}>{s.label}</h3>
|
||||
{items.filter(i => i.stage === s.stage).map(item => (
|
||||
<div key={item.id} style={{ background: '#16213e', padding: '15px', borderRadius: '8px', marginBottom: '10px' }}>
|
||||
{item.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ CALENDAR ============
|
||||
function Calendar() {
|
||||
const events: CalendarEvent[] = [
|
||||
{ id: '1', title: 'Twitter Briefing', date: '2026-02-19 06:00', type: 'cron' },
|
||||
{ id: '2', title: 'Infra Status Report', date: '2026-02-19 07:00', type: 'cron' },
|
||||
{ id: '3', title: 'ManoonOils Analytics', date: '2026-02-19 08:00', type: 'cron' },
|
||||
{ id: '4', title: 'YouTube Backup', date: '2026-02-18 21:00', type: 'cron' },
|
||||
];
|
||||
|
||||
// ============ CRON MONITOR ============
|
||||
function CronMonitor({ crons }: { crons: CronJob[] }) {
|
||||
return (
|
||||
<div style={{ background: '#1a1a2e', borderRadius: '12px', padding: '30px' }}>
|
||||
<h2 style={{ marginBottom: '30px' }}>📅 Scheduled Tasks & Cron Jobs</h2>
|
||||
<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' }}>Task</th>
|
||||
<th style={{ textAlign: 'left', padding: '15px', color: '#9ca3af' }}>Date/Time</th>
|
||||
<th style={{ textAlign: 'left', padding: '15px', color: '#9ca3af' }}>Type</th>
|
||||
<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>
|
||||
{events.map(event => (
|
||||
<tr key={event.id} style={{ borderBottom: '1px solid #2a2a4e' }}>
|
||||
<td style={{ padding: '15px' }}>{event.title}</td>
|
||||
<td style={{ padding: '15px', color: '#9ca3af' }}>{event.date}</td>
|
||||
{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: event.type === 'cron' ? '#e94560' : '#10b981',
|
||||
background: cron.status === 'ok' ? '#10b981' : '#ef4444',
|
||||
padding: '4px 12px', borderRadius: '20px', fontSize: '0.8rem'
|
||||
}}>
|
||||
{event.type}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
// ============ MEMORY ============
|
||||
function Memory() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const memories: Memory[] = [
|
||||
{ id: '1', title: 'ManoonOils Product Formula', date: '2026-02-07', preview: 'Professional formulation for anti-aging serum...' },
|
||||
{ id: '2', title: 'Backup System Setup', date: '2026-02-05', preview: 'Storage Box mounted at /mnt/storagebox...' },
|
||||
{ id: '3', title: 'Neo Agent Created', date: '2026-02-03', preview: 'Neo as CTO - infrastructure agent...' },
|
||||
];
|
||||
|
||||
const filtered = memories.filter(m => m.title.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search memories..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{
|
||||
width: '100%', padding: '15px', borderRadius: '8px', border: 'none',
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
</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' },
|
||||
{ id: '3', name: 'Agent 3', role: 'Sales Agent', status: 'idle' },
|
||||
{ id: '4', name: 'Agent 4', role: 'Ad Manager', status: 'idle' },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -233,30 +185,44 @@ function Team() {
|
||||
);
|
||||
}
|
||||
|
||||
// ============ OFFICE ============
|
||||
function Office() {
|
||||
const agents = [
|
||||
{ name: 'Jelena', area: 'Executive Suite', working: true, task: 'Managing operations' },
|
||||
{ name: 'Neo', area: 'Server Room', working: true, task: 'Infrastructure monitoring' },
|
||||
];
|
||||
// ============ MEMORY ============
|
||||
function Memory() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const [memories, setMemories] = useState<Memory[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/data?type=memory')
|
||||
.then(r => r.json())
|
||||
.then(setMemories)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const filtered = memories.filter(m => m.title.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
return (
|
||||
<div style={{ background: '#1a1a2e', borderRadius: '12px', padding: '30px' }}>
|
||||
<h2 style={{ marginBottom: '30px' }}>🏢 Digital Office</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px' }}>
|
||||
{agents.map(agent => (
|
||||
<div key={agent.name} style={{ background: '#16213e', borderRadius: '12px', padding: '25px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h3>{agent.name}</h3>
|
||||
<span style={{
|
||||
width: '12px', height: '12px', borderRadius: '50%',
|
||||
background: agent.working ? '#10b981' : '#6b7280'
|
||||
}} />
|
||||
</div>
|
||||
<p style={{ color: '#9ca3af', marginTop: '10px' }}>📍 {agent.area}</p>
|
||||
<p style={{ color: '#e94560', marginTop: '5px' }}>💻 {agent.task}</p>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search memories..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{
|
||||
width: '100%', padding: '15px', borderRadius: '8px', border: 'none',
|
||||
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user