feat: add token-based auth UI in dashboard
This commit is contained in:
parent
0046db1849
commit
84b000de5a
1 changed files with 121 additions and 321 deletions
|
|
@ -20,9 +20,13 @@ const App = () => {
|
|||
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'); // 'tasks' | 'dashboard'
|
||||
const [activeView, setActiveView] = useState('tasks');
|
||||
|
||||
// Auth token state
|
||||
const [authToken, setAuthToken] = useState('');
|
||||
const [tokenType, setTokenType] = useState('oauth_token');
|
||||
const [tokenSubmitting, setTokenSubmitting] = useState(false);
|
||||
|
||||
// Fetch tasks on component mount
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
fetchSystemInfo();
|
||||
|
|
@ -79,50 +83,32 @@ const App = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [authCode, setAuthCode] = useState('');
|
||||
const [codeSubmitting, setCodeSubmitting] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setLoginLoading(true);
|
||||
setAuthCode('');
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
setAuthStatus(prev => ({ ...prev, ...data }));
|
||||
if (data.status === 'error') {
|
||||
alert(data.message || 'Login failed. Check container logs.');
|
||||
}
|
||||
// Don't try window.open — popup blockers kill it.
|
||||
// The auth_url will be shown as a clickable link in the pending state.
|
||||
} catch (error) {
|
||||
console.error('Error initiating login:', error);
|
||||
alert('Failed to connect to auth endpoint');
|
||||
}
|
||||
setLoginLoading(false);
|
||||
};
|
||||
|
||||
const handleSubmitCode = async (e) => {
|
||||
const handleSubmitToken = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!authCode.trim()) return;
|
||||
setCodeSubmitting(true);
|
||||
if (!authToken.trim()) return;
|
||||
setTokenSubmitting(true);
|
||||
try {
|
||||
const response = await fetch('/api/auth/code', {
|
||||
const response = await fetch('/api/auth/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: authCode.trim() })
|
||||
body: JSON.stringify({ token: authToken.trim(), token_type: tokenType })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'logged_in') {
|
||||
setAuthCode('');
|
||||
setAuthToken('');
|
||||
fetchAuthStatus();
|
||||
} else if (data.status === 'token_saved') {
|
||||
setAuthToken('');
|
||||
fetchAuthStatus();
|
||||
alert(data.message);
|
||||
} else {
|
||||
alert(data.message || 'Code submission failed');
|
||||
alert(data.message || 'Token submission failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting auth code:', error);
|
||||
console.error('Error submitting token:', error);
|
||||
alert('Failed to submit token');
|
||||
}
|
||||
setCodeSubmitting(false);
|
||||
setTokenSubmitting(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
|
@ -178,40 +164,26 @@ const App = () => {
|
|||
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
|
||||
});
|
||||
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 fetch(`/api/tasks/${taskId}/run`, { method: 'POST' });
|
||||
await fetchTasks();
|
||||
} catch (error) {
|
||||
console.error('Error running task:', error);
|
||||
|
|
@ -253,24 +225,17 @@ const App = () => {
|
|||
</nav>
|
||||
<div className="auth-badge">
|
||||
{authStatus?.status === 'logged_in' ? (
|
||||
<span className="auth-ok" title={authStatus.account}>● {authStatus.account || 'Logged in'}</span>
|
||||
) : authStatus?.status === 'pending' ? (
|
||||
<div className="header-auth-pending">
|
||||
{authStatus.auth_url && (
|
||||
<a href={authStatus.auth_url} target="_blank" rel="noreferrer" className="header-auth-link">1. Open login →</a>
|
||||
)}
|
||||
<form className="header-code-form" onSubmit={handleSubmitCode}>
|
||||
<input type="text" placeholder="2. Paste code" value={authCode}
|
||||
onChange={e => setAuthCode(e.target.value)} className="header-code-input" />
|
||||
<button type="submit" className="header-code-btn" disabled={codeSubmitting}>
|
||||
{codeSubmitting ? '…' : '→'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<span className="auth-ok" title={authStatus.account || 'Authenticated'}>
|
||||
● {authStatus.account || `Authenticated (${authStatus.auth_method || 'token'})`}
|
||||
</span>
|
||||
) : authStatus?.has_saved_token ? (
|
||||
<span className="auth-ok auth-token-saved" title="Token saved">
|
||||
● Token saved ({authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth'})
|
||||
</span>
|
||||
) : (
|
||||
<button className="auth-login-btn" onClick={handleLogin} disabled={loginLoading}>
|
||||
{loginLoading ? '⏳ Connecting…' : '🔐 Login with Claude Max'}
|
||||
</button>
|
||||
<span className="auth-disconnected-badge" onClick={() => setActiveView('dashboard')} style={{cursor:'pointer'}}>
|
||||
⚠ Not authenticated
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{systemInfo && (
|
||||
|
|
@ -289,40 +254,17 @@ const App = () => {
|
|||
<div className="dashboard-grid">
|
||||
{systemInfo && (
|
||||
<>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">📋</div>
|
||||
<div className="dash-card-value">{systemInfo.task_count}</div>
|
||||
<div className="dash-card-label">Total Tasks</div>
|
||||
</div>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">✅</div>
|
||||
<div className="dash-card-value">{systemInfo.completed_runs || 0}</div>
|
||||
<div className="dash-card-label">Completed Runs</div>
|
||||
</div>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">❌</div>
|
||||
<div className="dash-card-value">{systemInfo.failed_runs || 0}</div>
|
||||
<div className="dash-card-label">Failed Runs</div>
|
||||
</div>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">⚡</div>
|
||||
<div className="dash-card-value">{systemInfo.running_runs || 0}</div>
|
||||
<div className="dash-card-label">Currently Running</div>
|
||||
</div>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">🔄</div>
|
||||
<div className="dash-card-value">{systemInfo.total_runs || 0}</div>
|
||||
<div className="dash-card-label">Total Runs Ever</div>
|
||||
</div>
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-icon">{systemInfo.scheduler_running ? '🟢' : '🔴'}</div>
|
||||
<div className="dash-card-value">{systemInfo.scheduler_running ? 'Active' : 'Stopped'}</div>
|
||||
<div className="dash-card-label">Scheduler</div>
|
||||
</div>
|
||||
<div className="dash-card"><div className="dash-card-icon">📋</div><div className="dash-card-value">{systemInfo.task_count}</div><div className="dash-card-label">Total Tasks</div></div>
|
||||
<div className="dash-card"><div className="dash-card-icon">✅</div><div className="dash-card-value">{systemInfo.completed_runs || 0}</div><div className="dash-card-label">Completed Runs</div></div>
|
||||
<div className="dash-card"><div className="dash-card-icon">❌</div><div className="dash-card-value">{systemInfo.failed_runs || 0}</div><div className="dash-card-label">Failed Runs</div></div>
|
||||
<div className="dash-card"><div className="dash-card-icon">⚡</div><div className="dash-card-value">{systemInfo.running_runs || 0}</div><div className="dash-card-label">Currently Running</div></div>
|
||||
<div className="dash-card"><div className="dash-card-icon">🔄</div><div className="dash-card-value">{systemInfo.total_runs || 0}</div><div className="dash-card-label">Total Runs</div></div>
|
||||
<div className="dash-card"><div className="dash-card-icon">{systemInfo.scheduler_running ? '🟢' : '🔴'}</div><div className="dash-card-value">{systemInfo.scheduler_running ? 'Active' : 'Stopped'}</div><div className="dash-card-label">Scheduler</div></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auth Section */}
|
||||
<div className="dashboard-section">
|
||||
<h3>Claude Authentication</h3>
|
||||
<div className="auth-panel">
|
||||
|
|
@ -330,46 +272,59 @@ const App = () => {
|
|||
<div className="auth-connected">
|
||||
<span className="auth-icon">✅</span>
|
||||
<div>
|
||||
<div className="auth-title">Connected to Claude Max</div>
|
||||
<div className="auth-sub">{authStatus.account}</div>
|
||||
<div className="auth-title">Connected to Claude</div>
|
||||
<div className="auth-sub">{authStatus.account || 'Authenticated'}{authStatus.auth_method && ` (${authStatus.auth_method})`}</div>
|
||||
</div>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleLogout}>Log Out</button>
|
||||
</div>
|
||||
) : authStatus?.status === 'pending' ? (
|
||||
<div className="auth-pending-panel">
|
||||
<span className="auth-icon">⏳</span>
|
||||
<div className="auth-pending-content">
|
||||
<div className="auth-title">Waiting for authorization code…</div>
|
||||
<div className="auth-sub">1. Click the link below to authorize in your browser</div>
|
||||
<div className="auth-sub">2. After authorizing, you'll see a code — paste it below</div>
|
||||
{authStatus.auth_url && (
|
||||
<a className="auth-url-link" href={authStatus.auth_url} target="_blank" rel="noreferrer">
|
||||
Open authorization page →
|
||||
</a>
|
||||
)}
|
||||
<form className="auth-code-form" onSubmit={handleSubmitCode}>
|
||||
<input type="text" placeholder="Paste your authorization code here"
|
||||
value={authCode} onChange={e => setAuthCode(e.target.value)}
|
||||
className="auth-code-input" />
|
||||
<button type="submit" className="btn btn-primary btn-sm" disabled={codeSubmitting}>
|
||||
{codeSubmitting ? 'Verifying…' : 'Submit Code'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="auth-disconnected">
|
||||
<span className="auth-icon">🔐</span>
|
||||
<div>
|
||||
<div className="auth-title">Not logged in</div>
|
||||
<div className="auth-sub">Log in with your Claude Max account to run tasks</div>
|
||||
<div className="auth-token-panel">
|
||||
<div className="auth-token-header">
|
||||
<span className="auth-icon">🔐</span>
|
||||
<div>
|
||||
<div className="auth-title">{authStatus?.has_saved_token ? 'Token Saved — Update or Replace' : 'Authenticate with Token'}</div>
|
||||
<div className="auth-sub">Two options to authenticate:</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleLogin}>Login with Claude Max</button>
|
||||
|
||||
<div className="auth-methods">
|
||||
<div className="auth-method">
|
||||
<div className="auth-method-title">Option 1: Setup Token (Claude Max/Pro)</div>
|
||||
<div className="auth-method-desc">Run this on TrueNAS to generate a long-lived token:</div>
|
||||
<code className="auth-cmd">docker exec -it claude-persistent-agent claude setup-token</code>
|
||||
<div className="auth-method-desc">Then paste the token (starts with <code>sk-ant-oat</code>) below.</div>
|
||||
</div>
|
||||
<div className="auth-method">
|
||||
<div className="auth-method-title">Option 2: API Key (Console billing)</div>
|
||||
<div className="auth-method-desc">
|
||||
Get an API key from <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noreferrer">console.anthropic.com</a> and paste it below.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="auth-token-form" onSubmit={handleSubmitToken}>
|
||||
<div className="auth-token-type-row">
|
||||
<label><input type="radio" name="token_type" value="oauth_token" checked={tokenType === 'oauth_token'} onChange={() => setTokenType('oauth_token')} /> Setup Token (Max/Pro)</label>
|
||||
<label><input type="radio" name="token_type" value="api_key" checked={tokenType === 'api_key'} onChange={() => setTokenType('api_key')} /> API Key</label>
|
||||
</div>
|
||||
<div className="auth-token-input-row">
|
||||
<input type="password" placeholder={tokenType === 'oauth_token' ? 'sk-ant-oat01-...' : 'sk-ant-api03-...'} value={authToken} onChange={e => setAuthToken(e.target.value)} className="auth-token-input" autoComplete="off" />
|
||||
<button type="submit" className="btn btn-primary btn-sm" disabled={tokenSubmitting || !authToken.trim()}>{tokenSubmitting ? 'Saving...' : 'Save Token'}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{authStatus?.has_saved_token && (
|
||||
<div className="auth-saved-info">
|
||||
Saved: {authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth Token'}
|
||||
<button className="btn btn-secondary btn-xs" onClick={handleLogout}>Clear Token</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MCP Section */}
|
||||
<div className="dashboard-section">
|
||||
<h3>MCP Servers</h3>
|
||||
<div className="mcp-panel">
|
||||
|
|
@ -390,24 +345,19 @@ const App = () => {
|
|||
{mcpServers?.raw && <pre className="mcp-raw">{mcpServers.raw}</pre>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddMcp ? (
|
||||
<form className="mcp-add-form" onSubmit={handleAddMcp}>
|
||||
<div className="mcp-form-row">
|
||||
<input type="text" placeholder="Server name" value={mcpForm.name}
|
||||
onChange={e => setMcpForm({...mcpForm, name: e.target.value})} required />
|
||||
<select value={mcpForm.server_type}
|
||||
onChange={e => setMcpForm({...mcpForm, server_type: e.target.value})}>
|
||||
<input type="text" placeholder="Server name" value={mcpForm.name} onChange={e => setMcpForm({...mcpForm, name: e.target.value})} required />
|
||||
<select value={mcpForm.server_type} onChange={e => setMcpForm({...mcpForm, server_type: e.target.value})}>
|
||||
<option value="sse">SSE (URL)</option>
|
||||
<option value="stdio">Stdio (Command)</option>
|
||||
</select>
|
||||
</div>
|
||||
{mcpForm.server_type === 'sse' ? (
|
||||
<input type="url" placeholder="https://mcp-server-url/mcp" value={mcpForm.url}
|
||||
onChange={e => setMcpForm({...mcpForm, url: e.target.value})} required />
|
||||
<input type="url" placeholder="https://mcp-server-url/mcp" value={mcpForm.url} onChange={e => setMcpForm({...mcpForm, url: e.target.value})} required />
|
||||
) : (
|
||||
<input type="text" placeholder="Command (e.g. npx mcp-server)" value={mcpForm.command}
|
||||
onChange={e => setMcpForm({...mcpForm, command: e.target.value})} required />
|
||||
<input type="text" placeholder="Command (e.g. npx mcp-server)" value={mcpForm.command} onChange={e => setMcpForm({...mcpForm, command: e.target.value})} required />
|
||||
)}
|
||||
<div className="mcp-form-actions">
|
||||
<button type="submit" className="btn btn-primary btn-sm">Add Server</button>
|
||||
|
|
@ -420,45 +370,26 @@ const App = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Section */}
|
||||
<div className="dashboard-section">
|
||||
<h3>Claude API Usage</h3>
|
||||
<div className="usage-grid">
|
||||
{usageStats ? (
|
||||
<>
|
||||
<div className="usage-row">
|
||||
<span className="usage-label">Claude Runs (all time)</span>
|
||||
<span className="usage-value">{usageStats.claude_runs_total ?? '—'}</span>
|
||||
</div>
|
||||
<div className="usage-row">
|
||||
<span className="usage-label">Active Sessions</span>
|
||||
<span className="usage-value">{usageStats.session_count ?? '—'}</span>
|
||||
</div>
|
||||
<div className="usage-row">
|
||||
<span className="usage-label">First Run</span>
|
||||
<span className="usage-value">{usageStats.first_run ? new Date(usageStats.first_run).toLocaleString() : '—'}</span>
|
||||
</div>
|
||||
<div className="usage-row">
|
||||
<span className="usage-label">Last Run</span>
|
||||
<span className="usage-value">{usageStats.last_run ? new Date(usageStats.last_run).toLocaleString() : '—'}</span>
|
||||
</div>
|
||||
<div className="usage-row highlight">
|
||||
<span className="usage-label">🔄 Next Monthly Reset</span>
|
||||
<span className="usage-value">{usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : '—'}</span>
|
||||
</div>
|
||||
<div className="usage-row highlight">
|
||||
<span className="usage-label">⏳ Days Until Reset</span>
|
||||
<span className="usage-value">{usageStats.days_until_reset ?? '—'}</span>
|
||||
</div>
|
||||
{usageStats.note && (
|
||||
<div className="usage-note">{usageStats.note}</div>
|
||||
)}
|
||||
<div className="usage-row"><span className="usage-label">Claude Runs (all time)</span><span className="usage-value">{usageStats.claude_runs_total ?? '—'}</span></div>
|
||||
<div className="usage-row"><span className="usage-label">Active Sessions</span><span className="usage-value">{usageStats.session_count ?? '—'}</span></div>
|
||||
<div className="usage-row"><span className="usage-label">First Run</span><span className="usage-value">{usageStats.first_run ? new Date(usageStats.first_run).toLocaleString() : '—'}</span></div>
|
||||
<div className="usage-row"><span className="usage-label">Last Run</span><span className="usage-value">{usageStats.last_run ? new Date(usageStats.last_run).toLocaleString() : '—'}</span></div>
|
||||
<div className="usage-row highlight"><span className="usage-label">🔄 Next Monthly Reset</span><span className="usage-value">{usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : '—'}</span></div>
|
||||
<div className="usage-row highlight"><span className="usage-label">⏳ Days Until Reset</span><span className="usage-value">{usageStats.days_until_reset ?? '—'}</span></div>
|
||||
</>
|
||||
) : (
|
||||
<div className="usage-loading">Loading usage stats…</div>
|
||||
<div className="usage-loading">Loading usage stats...</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Breakdown */}
|
||||
<div className="dashboard-section">
|
||||
<h3>Task Breakdown</h3>
|
||||
<div className="task-breakdown">
|
||||
|
|
@ -481,41 +412,20 @@ const App = () => {
|
|||
|
||||
{activeView === 'tasks' && (<>
|
||||
<aside className="sidebar">
|
||||
<button
|
||||
className="btn btn-primary btn-create"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
+ New Task
|
||||
</button>
|
||||
|
||||
<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 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>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
))
|
||||
)}
|
||||
|
|
@ -529,93 +439,45 @@ const App = () => {
|
|||
<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"
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
<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 })}
|
||||
>
|
||||
<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)"
|
||||
/>
|
||||
<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 })}
|
||||
/>
|
||||
<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 })}
|
||||
/>
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -624,67 +486,18 @@ const App = () => {
|
|||
<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>
|
||||
<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 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>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
|
|
@ -694,7 +507,7 @@ const App = () => {
|
|||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>)} {/* end activeView === 'tasks' */}
|
||||
</>)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -713,7 +526,6 @@ const TaskRuns = ({ taskId }) => {
|
|||
console.error('Error fetching runs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRuns();
|
||||
const interval = setInterval(fetchRuns, 3000);
|
||||
return () => clearInterval(interval);
|
||||
|
|
@ -730,22 +542,10 @@ const TaskRuns = ({ taskId }) => {
|
|||
<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>
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
|
|
|
|||
Reference in a new issue