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)
Get an API key from console.anthropic.com and paste it below.
setAuthToken(e.target.value)} className="auth-token-input" autoComplete="off" />
{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 ? (
setMcpForm({...mcpForm, name: e.target.value})} required />
{mcpForm.server_type === 'sse' ? ( setMcpForm({...mcpForm, url: e.target.value})} required /> ) : ( setMcpForm({...mcpForm, command: e.target.value})} required /> )}
) : ( )}
{/* 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 ? (

Create New Task

setFormData({ ...formData, name: e.target.value })} placeholder="e.g., Daily Backup" />