Add frontend/src/App.jsx

This commit is contained in:
Zac Gaetano 2026-04-04 22:40:51 -04:00
parent 30abd56a6a
commit 1275a94692

403
frontend/src/App.jsx Normal file
View file

@ -0,0 +1,403 @@
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 [loading, setLoading] = useState(false);
// Fetch tasks on component mount
useEffect(() => {
fetchTasks();
fetchSystemInfo();
const interval = setInterval(() => {
fetchTasks();
fetchSystemInfo();
}, 5000); // Refresh every 5 seconds
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 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 {
const response = await fetch(`/api/tasks/${taskId}/run`, {
method: 'POST'
});
const data = await response.json();
console.log('Task started:', data);
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 (
<div className="app-container">
<header className="app-header">
<div className="header-content">
<h1>Claude Persistent Agent</h1>
<p>Scheduled task management & Claude Code runner</p>
</div>
{systemInfo && (
<div className="system-status">
<span className={`status-indicator ${systemInfo.scheduler_running ? 'running' : 'stopped'}`}></span>
<span>{systemInfo.task_count} tasks</span>
</div>
)}
</header>
<main className="app-main">
<aside className="sidebar">
<button
className="btn btn-primary btn-create"
onClick={() => setShowForm(true)}
>
+ New Task
</button>
<div className="tasks-list">
<h2>Tasks</h2>
{tasks.length === 0 ? (
<p className="empty-state">No tasks yet. Create one to get started.</p>
) : (
tasks.map((task) => (
<div
key={task.id}
className={`task-item ${selectedTask?.id === task.id ? 'active' : ''}`}
onClick={() => setSelectedTask(task)}
>
<div className="task-item-header">
<h3>{task.name}</h3>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(task.status) }}
>
{task.status}
</span>
</div>
<p className="task-schedule">
{task.schedule_type === 'manual' ? 'Manual' : `${task.schedule_type}: ${task.schedule_value}`}
</p>
{task.last_run && (
<p className="task-last-run">
Last run: {new Date(task.last_run).toLocaleString()}
</p>
)}
</div>
))
)}
</div>
</aside>
<section className="content">
{showForm ? (
<div className="form-container">
<h2>Create New Task</h2>
<form onSubmit={handleCreateTask} className="task-form">
<div className="form-group">
<label>Task Name</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g., Daily Backup"
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Optional description"
rows={2}
/>
</div>
<div className="form-group">
<label>Claude Prompt</label>
<textarea
required
value={formData.prompt}
onChange={(e) => setFormData({ ...formData, prompt: e.target.value })}
placeholder="What should Claude do? Be specific..."
rows={6}
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Schedule Type</label>
<select
value={formData.schedule_type}
onChange={(e) => setFormData({ ...formData, schedule_type: e.target.value })}
>
<option value="manual">Manual Only</option>
<option value="recurring">Recurring (Cron)</option>
<option value="once">Once (Datetime)</option>
</select>
</div>
{formData.schedule_type === 'recurring' && (
<div className="form-group">
<label>Cron Expression</label>
<input
type="text"
value={formData.schedule_value}
onChange={(e) => setFormData({ ...formData, schedule_value: e.target.value })}
placeholder="0 9 * * * (daily at 9am)"
/>
</div>
)}
{formData.schedule_type === 'once' && (
<div className="form-group">
<label>Run At</label>
<input
type="datetime-local"
value={formData.schedule_value}
onChange={(e) => setFormData({ ...formData, schedule_value: e.target.value })}
/>
</div>
)}
</div>
<div className="form-group checkbox">
<input
type="checkbox"
checked={formData.enabled}
onChange={(e) => setFormData({ ...formData, enabled: e.target.checked })}
/>
<label>Enabled</label>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? 'Creating...' : 'Create Task'}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => setShowForm(false)}
>
Cancel
</button>
</div>
</form>
</div>
) : selectedTask ? (
<div className="task-detail">
<div className="task-detail-header">
<h2>{selectedTask.name}</h2>
<div className="task-actions">
<button
className="btn btn-success"
onClick={() => handleRunTask(selectedTask.id)}
>
Run Now
</button>
<button
className="btn btn-danger"
onClick={() => handleDeleteTask(selectedTask.id)}
>
🗑 Delete
</button>
</div>
</div>
<div className="task-meta">
<div className="meta-item">
<span className="label">Status:</span>
<span
className="value"
style={{ color: getStatusColor(selectedTask.status) }}
>
{selectedTask.status}
</span>
</div>
<div className="meta-item">
<span className="label">Schedule:</span>
<span className="value">
{selectedTask.schedule_type === 'manual'
? 'Manual'
: `${selectedTask.schedule_type}: ${selectedTask.schedule_value}`}
</span>
</div>
<div className="meta-item">
<span className="label">Created:</span>
<span className="value">
{new Date(selectedTask.created_at).toLocaleString()}
</span>
</div>
{selectedTask.last_run && (
<div className="meta-item">
<span className="label">Last Run:</span>
<span className="value">
{new Date(selectedTask.last_run).toLocaleString()}
</span>
</div>
)}
</div>
{selectedTask.description && (
<div className="task-section">
<h3>Description</h3>
<p>{selectedTask.description}</p>
</div>
)}
<div className="task-section">
<h3>Prompt</h3>
<pre className="prompt-display">{selectedTask.prompt}</pre>
</div>
<TaskRuns taskId={selectedTask.id} />
</div>
) : (
<div className="empty-state-main">
<h2>Select a task to view details</h2>
<p>Or create a new one to get started</p>
</div>
)}
</section>
</main>
</div>
);
};
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 (
<div className="task-section">
<h3>Run History</h3>
{runs.length === 0 ? (
<p className="empty">No runs yet</p>
) : (
<div className="runs-list">
{runs.map((run) => (
<div key={run.run_id} className="run-item">
<div className="run-header">
<span className={`run-status ${run.status}`}>{run.status}</span>
<span className="run-time">
{new Date(run.started_at).toLocaleString()}
</span>
</div>
{run.output && (
<details>
<summary>Output</summary>
<pre>{run.output}</pre>
</details>
)}
{run.error && (
<details>
<summary>Error</summary>
<pre className="error">{run.error}</pre>
</details>
)}
</div>
))}
</div>
)}
</div>
);
};
export default App;