232 lines
8.4 KiB
TypeScript
232 lines
8.4 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
// Types
|
|
type TaskStatus = 'todo' | 'in-progress' | 'done';
|
|
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 };
|
|
|
|
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);
|
|
|
|
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' }}>
|
|
{/* 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(setMemories)
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
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>
|
|
))}
|
|
{filtered.length === 0 && (
|
|
<p style={{ color: '#6b7280' }}>No memories found</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|