import React, { useState, useEffect } from 'react';
import './App.css';
const App = () => {
const [tasks, setTasks] = useState([]);
const [selectedTask, setSelectedTask] = useState(null);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({
name: '',
description: '',
prompt: '',
schedule_type: 'manual',
schedule_value: '',
enabled: true
});
const [systemInfo, setSystemInfo] = useState(null);
const [usageStats, setUsageStats] = useState(null);
const [authStatus, setAuthStatus] = useState(null);
const [mcpServers, setMcpServers] = useState(null);
const [showAddMcp, setShowAddMcp] = useState(false);
const [mcpForm, setMcpForm] = useState({ name: '', server_type: 'sse', url: '', command: '' });
const [loading, setLoading] = useState(false);
const [activeView, setActiveView] = useState('tasks');
// Auth token state
const [authToken, setAuthToken] = useState('');
const [tokenType, setTokenType] = useState('oauth_token');
const [tokenSubmitting, setTokenSubmitting] = useState(false);
useEffect(() => {
fetchTasks();
fetchSystemInfo();
fetchUsageStats();
fetchAuthStatus();
fetchMcpServers();
const interval = setInterval(() => {
fetchTasks();
fetchSystemInfo();
fetchUsageStats();
fetchAuthStatus();
fetchMcpServers();
}, 10000);
return () => clearInterval(interval);
}, []);
const fetchTasks = async () => {
try {
const response = await fetch('/api/tasks');
const data = await response.json();
setTasks(data);
} catch (error) {
console.error('Error fetching tasks:', error);
}
};
const fetchSystemInfo = async () => {
try {
const response = await fetch('/api/system/info');
const data = await response.json();
setSystemInfo(data);
} catch (error) {
console.error('Error fetching system info:', error);
}
};
const fetchUsageStats = async () => {
try {
const response = await fetch('/api/system/usage');
const data = await response.json();
setUsageStats(data);
} catch (error) {
console.error('Error fetching usage stats:', error);
}
};
const fetchAuthStatus = async () => {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
setAuthStatus(data);
} catch (error) {
console.error('Error fetching auth status:', error);
}
};
const handleSubmitToken = async (e) => {
e.preventDefault();
if (!authToken.trim()) return;
setTokenSubmitting(true);
try {
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: authToken.trim(), token_type: tokenType })
});
const data = await response.json();
if (data.status === 'logged_in') {
setAuthToken('');
fetchAuthStatus();
} else if (data.status === 'token_saved') {
setAuthToken('');
fetchAuthStatus();
alert(data.message);
} else {
alert(data.message || 'Token submission failed');
}
} catch (error) {
console.error('Error submitting token:', error);
alert('Failed to submit token');
}
setTokenSubmitting(false);
};
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
fetchAuthStatus();
} catch (error) {
console.error('Error logging out:', error);
}
};
const fetchMcpServers = async () => {
try {
const response = await fetch('/api/mcp/servers');
const data = await response.json();
setMcpServers(data);
} catch (error) {
console.error('Error fetching MCP servers:', error);
}
};
const handleAddMcp = async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/mcp/servers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mcpForm)
});
const data = await response.json();
if (data.status === 'ok') {
setShowAddMcp(false);
setMcpForm({ name: '', server_type: 'sse', url: '', command: '' });
fetchMcpServers();
} else {
alert(data.message || 'Failed to add MCP server');
}
} catch (error) {
console.error('Error adding MCP server:', error);
}
};
const handleRemoveMcp = async (name) => {
if (!confirm(`Remove MCP server "${name}"?`)) return;
try {
await fetch(`/api/mcp/servers/${name}`, { method: 'DELETE' });
fetchMcpServers();
} catch (error) {
console.error('Error removing MCP server:', error);
}
};
const handleCreateTask = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
await fetchTasks();
setShowForm(false);
setFormData({ name: '', description: '', prompt: '', schedule_type: 'manual', schedule_value: '', enabled: true });
}
} catch (error) {
console.error('Error creating task:', error);
}
setLoading(false);
};
const handleRunTask = async (taskId) => {
try {
await fetch(`/api/tasks/${taskId}/run`, { method: 'POST' });
await fetchTasks();
} catch (error) {
console.error('Error running task:', error);
}
};
const handleDeleteTask = async (taskId) => {
if (confirm('Delete this task?')) {
try {
await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' });
await fetchTasks();
setSelectedTask(null);
} catch (error) {
console.error('Error deleting task:', error);
}
}
};
const getStatusColor = (status) => {
switch (status) {
case 'running': return '#3498db';
case 'completed': return '#27ae60';
case 'failed': return '#e74c3c';
default: return '#95a5a6';
}
};
return (
Claude Persistent Agent
Scheduled task management & Claude Code runner
{authStatus?.status === 'logged_in' ? (
โ {authStatus.account || `Authenticated (${authStatus.auth_method || 'token'})`}
) : authStatus?.has_saved_token ? (
โ Token saved ({authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth'})
) : (
setActiveView('dashboard')} style={{cursor:'pointer'}}>
โ Not authenticated
)}
{systemInfo && (
{systemInfo.task_count} tasks ยท {systemInfo.total_runs || 0} runs
)}
{activeView === 'dashboard' && (
System Dashboard
{systemInfo && (
<>
๐
{systemInfo.task_count}
Total Tasks
โ
{systemInfo.completed_runs || 0}
Completed Runs
โ
{systemInfo.failed_runs || 0}
Failed Runs
โก
{systemInfo.running_runs || 0}
Currently Running
๐
{systemInfo.total_runs || 0}
Total Runs
{systemInfo.scheduler_running ? '๐ข' : '๐ด'}
{systemInfo.scheduler_running ? 'Active' : 'Stopped'}
Scheduler
>
)}
{/* Auth Section */}
Claude Authentication
{authStatus?.status === 'logged_in' ? (
โ
Connected to Claude
{authStatus.account || 'Authenticated'}{authStatus.auth_method && ` (${authStatus.auth_method})`}
) : (
๐
{authStatus?.has_saved_token ? 'Token Saved โ Update or Replace' : 'Authenticate with Token'}
Two options to authenticate:
Option 1: Setup Token (Claude Max/Pro)
Run this on TrueNAS to generate a long-lived token:
docker exec -it claude-persistent-agent claude setup-token
Then paste the token (starts with sk-ant-oat) below.
Option 2: API Key (Console billing)
{authStatus?.has_saved_token && (
Saved: {authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth Token'}
)}
)}
{/* MCP Section */}
MCP Servers
{mcpServers?.servers?.length > 0 ? (
{mcpServers.servers.map((srv, i) => (
{srv.name || srv}
{srv.details || srv.url || srv.type || ''}
))}
) : (
No MCP servers configured.
{mcpServers?.raw &&
{mcpServers.raw}}
)}
{showAddMcp ? (
) : (
)}
{/* Usage Section */}
Claude API Usage
{usageStats ? (
<>
Claude Runs (all time){usageStats.claude_runs_total ?? 'โ'}
Active Sessions{usageStats.session_count ?? 'โ'}
First Run{usageStats.first_run ? new Date(usageStats.first_run).toLocaleString() : 'โ'}
Last Run{usageStats.last_run ? new Date(usageStats.last_run).toLocaleString() : 'โ'}
๐ Next Monthly Reset{usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : 'โ'}
โณ Days Until Reset{usageStats.days_until_reset ?? 'โ'}
>
) : (
Loading usage stats...
)}
{/* Task Breakdown */}
Task Breakdown
{tasks.length === 0 ? (
No tasks yet. Create one in the Tasks view.
) : (
tasks.map(t => (
{t.name}
{t.schedule_type}
{t.status}
{t.last_run ? new Date(t.last_run).toLocaleString() : 'never'}
))
)}
)}
{activeView === 'tasks' && (<>
{showForm ? (
) : selectedTask ? (
{selectedTask.name}
Status:{selectedTask.status}
Schedule:{selectedTask.schedule_type === 'manual' ? 'Manual' : `${selectedTask.schedule_type}: ${selectedTask.schedule_value}`}
Created:{new Date(selectedTask.created_at).toLocaleString()}
{selectedTask.last_run &&
Last Run:{new Date(selectedTask.last_run).toLocaleString()}
}
{selectedTask.description &&
Description
{selectedTask.description}
}
Prompt
{selectedTask.prompt}
) : (
Select a task to view details
Or create a new one to get started
)}
>)}
);
};
const TaskRuns = ({ taskId }) => {
const [runs, setRuns] = useState([]);
useEffect(() => {
const fetchRuns = async () => {
try {
const response = await fetch(`/api/tasks/${taskId}/runs`);
const data = await response.json();
setRuns(data);
} catch (error) {
console.error('Error fetching runs:', error);
}
};
fetchRuns();
const interval = setInterval(fetchRuns, 3000);
return () => clearInterval(interval);
}, [taskId]);
return (
Run History
{runs.length === 0 ? (
No runs yet
) : (
{runs.map((run) => (
{run.status}
{new Date(run.started_at).toLocaleString()}
{run.output &&
Output
{run.output}}
{run.error &&
Error
{run.error}}
))}
)}
);
};
export default App;