Add frontend/src/App.jsx
This commit is contained in:
parent
30abd56a6a
commit
1275a94692
1 changed files with 403 additions and 0 deletions
403
frontend/src/App.jsx
Normal file
403
frontend/src/App.jsx
Normal 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;
|
||||
Reference in a new issue