2026-04-04 22:40:51 -04:00
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 ) ;
2026-04-04 22:46:16 -04:00
const [ usageStats , setUsageStats ] = useState ( null ) ;
2026-04-04 22:51:39 -04:00
const [ authStatus , setAuthStatus ] = useState ( null ) ;
2026-04-04 23:50:16 -04:00
const [ mcpServers , setMcpServers ] = useState ( null ) ;
const [ showAddMcp , setShowAddMcp ] = useState ( false ) ;
const [ mcpForm , setMcpForm ] = useState ( { name : '' , server _type : 'sse' , url : '' , command : '' } ) ;
2026-04-04 22:40:51 -04:00
const [ loading , setLoading ] = useState ( false ) ;
2026-04-05 00:13:37 -04:00
const [ activeView , setActiveView ] = useState ( 'tasks' ) ;
// Auth token state
const [ authToken , setAuthToken ] = useState ( '' ) ;
const [ tokenType , setTokenType ] = useState ( 'oauth_token' ) ;
const [ tokenSubmitting , setTokenSubmitting ] = useState ( false ) ;
2026-04-04 22:40:51 -04:00
useEffect ( ( ) => {
fetchTasks ( ) ;
fetchSystemInfo ( ) ;
2026-04-04 22:46:16 -04:00
fetchUsageStats ( ) ;
2026-04-04 22:51:39 -04:00
fetchAuthStatus ( ) ;
2026-04-04 23:50:16 -04:00
fetchMcpServers ( ) ;
2026-04-04 22:40:51 -04:00
const interval = setInterval ( ( ) => {
fetchTasks ( ) ;
fetchSystemInfo ( ) ;
2026-04-04 22:46:16 -04:00
fetchUsageStats ( ) ;
2026-04-04 22:51:39 -04:00
fetchAuthStatus ( ) ;
2026-04-04 23:50:16 -04:00
fetchMcpServers ( ) ;
2026-04-04 22:46:16 -04:00
} , 10000 ) ;
2026-04-04 22:40:51 -04:00
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 ) ;
}
} ;
2026-04-04 22:46:16 -04:00
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 ) ;
}
} ;
2026-04-04 22:51:39 -04:00
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 ) ;
}
} ;
2026-04-05 00:13:37 -04:00
const handleSubmitToken = async ( e ) => {
2026-04-04 23:54:24 -04:00
e . preventDefault ( ) ;
2026-04-05 00:13:37 -04:00
if ( ! authToken . trim ( ) ) return ;
setTokenSubmitting ( true ) ;
2026-04-04 23:54:24 -04:00
try {
2026-04-05 00:13:37 -04:00
const response = await fetch ( '/api/auth/token' , {
2026-04-04 23:54:24 -04:00
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-04-05 00:13:37 -04:00
body : JSON . stringify ( { token : authToken . trim ( ) , token _type : tokenType } )
2026-04-04 23:54:24 -04:00
} ) ;
const data = await response . json ( ) ;
if ( data . status === 'logged_in' ) {
2026-04-05 00:13:37 -04:00
setAuthToken ( '' ) ;
2026-04-04 23:54:24 -04:00
fetchAuthStatus ( ) ;
2026-04-05 00:13:37 -04:00
} else if ( data . status === 'token_saved' ) {
setAuthToken ( '' ) ;
fetchAuthStatus ( ) ;
alert ( data . message ) ;
2026-04-04 23:54:24 -04:00
} else {
2026-04-05 00:13:37 -04:00
alert ( data . message || 'Token submission failed' ) ;
2026-04-04 23:54:24 -04:00
}
} catch ( error ) {
2026-04-05 00:13:37 -04:00
console . error ( 'Error submitting token:' , error ) ;
alert ( 'Failed to submit token' ) ;
2026-04-04 23:54:24 -04:00
}
2026-04-05 00:13:37 -04:00
setTokenSubmitting ( false ) ;
2026-04-04 23:54:24 -04:00
} ;
2026-04-04 22:51:39 -04:00
const handleLogout = async ( ) => {
try {
await fetch ( '/api/auth/logout' , { method : 'POST' } ) ;
fetchAuthStatus ( ) ;
} catch ( error ) {
console . error ( 'Error logging out:' , error ) ;
}
} ;
2026-04-04 23:50:16 -04:00
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 ) ;
}
} ;
2026-04-04 22:40:51 -04:00
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 ) ;
2026-04-05 00:13:37 -04:00
setFormData ( { name : '' , description : '' , prompt : '' , schedule _type : 'manual' , schedule _value : '' , enabled : true } ) ;
2026-04-04 22:40:51 -04:00
}
} catch ( error ) {
console . error ( 'Error creating task:' , error ) ;
}
setLoading ( false ) ;
} ;
const handleRunTask = async ( taskId ) => {
try {
2026-04-05 00:13:37 -04:00
await fetch ( ` /api/tasks/ ${ taskId } /run ` , { method : 'POST' } ) ;
2026-04-04 22:40:51 -04:00
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 >
2026-04-04 22:46:16 -04:00
< div className = "header-right" >
< nav className = "header-nav" >
< button className = { ` nav-btn ${ activeView === 'tasks' ? 'active' : '' } ` } onClick = { ( ) => setActiveView ( 'tasks' ) } > Tasks < / button >
< button className = { ` nav-btn ${ activeView === 'dashboard' ? 'active' : '' } ` } onClick = { ( ) => setActiveView ( 'dashboard' ) } > Dashboard < / button >
< / nav >
2026-04-04 22:51:39 -04:00
< div className = "auth-badge" >
{ authStatus ? . status === 'logged_in' ? (
2026-04-05 00:13:37 -04:00
< 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 >
2026-04-04 22:51:39 -04:00
) : (
2026-04-05 00:13:37 -04:00
< span className = "auth-disconnected-badge" onClick = { ( ) => setActiveView ( 'dashboard' ) } style = { { cursor : 'pointer' } } >
⚠ Not authenticated
< / span >
2026-04-04 22:51:39 -04:00
) }
< / div >
2026-04-04 22:46:16 -04:00
{ systemInfo && (
< div className = "system-status" >
< span className = { ` status-indicator ${ systemInfo . scheduler _running ? 'running' : 'stopped' } ` } > < / span >
< span > { systemInfo . task _count } tasks · { systemInfo . total _runs || 0 } runs < / span >
< / div >
) }
< / div >
2026-04-04 22:40:51 -04:00
< / header >
< main className = "app-main" >
2026-04-04 22:46:16 -04:00
{ activeView === 'dashboard' && (
< div className = "dashboard-view" >
< h2 className = "dashboard-title" > System Dashboard < / h2 >
< div className = "dashboard-grid" >
{ systemInfo && (
< >
2026-04-05 00:13:37 -04:00
< 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 >
2026-04-04 22:46:16 -04:00
< / >
) }
< / div >
2026-04-05 00:13:37 -04:00
{ /* Auth Section */ }
2026-04-04 22:51:39 -04:00
< div className = "dashboard-section" >
< h3 > Claude Authentication < / h3 >
< div className = "auth-panel" >
{ authStatus ? . status === 'logged_in' ? (
< div className = "auth-connected" >
< span className = "auth-icon" > ✅ < / span >
< div >
2026-04-05 00:13:37 -04:00
< div className = "auth-title" > Connected to Claude < / div >
< div className = "auth-sub" > { authStatus . account || 'Authenticated' } { authStatus . auth _method && ` ( ${ authStatus . auth _method } ) ` } < / div >
2026-04-04 22:51:39 -04:00
< / div >
< button className = "btn btn-secondary btn-sm" onClick = { handleLogout } > Log Out < / button >
< / div >
) : (
2026-04-05 00:13:37 -04:00
< 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 >
2026-04-04 22:51:39 -04:00
< / div >
2026-04-05 00:13:37 -04:00
< 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 >
) }
2026-04-04 22:51:39 -04:00
< / div >
) }
< / div >
< / div >
2026-04-05 00:13:37 -04:00
{ /* MCP Section */ }
2026-04-04 23:50:16 -04:00
< div className = "dashboard-section" >
< h3 > MCP Servers < / h3 >
< div className = "mcp-panel" >
{ mcpServers ? . servers ? . length > 0 ? (
< div className = "mcp-list" >
{ mcpServers . servers . map ( ( srv , i ) => (
< div key = { i } className = "mcp-row" >
< span className = "mcp-status-dot connected" > < / span >
< span className = "mcp-name" > { srv . name || srv } < / span >
< span className = "mcp-details" > { srv . details || srv . url || srv . type || '' } < / span >
< button className = "btn-icon" onClick = { ( ) => handleRemoveMcp ( srv . name || srv ) } title = "Remove" > ✕ < / button >
< / div >
) ) }
< / div >
) : (
< div className = "mcp-empty" >
< p > No MCP servers configured . < / p >
{ mcpServers ? . raw && < pre className = "mcp-raw" > { mcpServers . raw } < / pre > }
< / div >
) }
{ showAddMcp ? (
< form className = "mcp-add-form" onSubmit = { handleAddMcp } >
< div className = "mcp-form-row" >
2026-04-05 00:13:37 -04:00
< 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 } ) } >
2026-04-04 23:50:16 -04:00
< option value = "sse" > SSE ( URL ) < / option >
< option value = "stdio" > Stdio ( Command ) < / option >
< / select >
< / div >
{ mcpForm . server _type === 'sse' ? (
2026-04-05 00:13:37 -04:00
< input type = "url" placeholder = "https://mcp-server-url/mcp" value = { mcpForm . url } onChange = { e => setMcpForm ( { ... mcpForm , url : e . target . value } ) } required / >
2026-04-04 23:50:16 -04:00
) : (
2026-04-05 00:13:37 -04:00
< input type = "text" placeholder = "Command (e.g. npx mcp-server)" value = { mcpForm . command } onChange = { e => setMcpForm ( { ... mcpForm , command : e . target . value } ) } required / >
2026-04-04 23:50:16 -04:00
) }
< div className = "mcp-form-actions" >
< button type = "submit" className = "btn btn-primary btn-sm" > Add Server < / button >
< button type = "button" className = "btn btn-secondary btn-sm" onClick = { ( ) => setShowAddMcp ( false ) } > Cancel < / button >
< / div >
< / form >
) : (
< button className = "btn btn-secondary btn-sm mcp-add-btn" onClick = { ( ) => setShowAddMcp ( true ) } > + Add MCP Server < / button >
) }
< / div >
< / div >
2026-04-05 00:13:37 -04:00
{ /* Usage Section */ }
2026-04-04 22:46:16 -04:00
< div className = "dashboard-section" >
< h3 > Claude API Usage < / h3 >
< div className = "usage-grid" >
{ usageStats ? (
< >
2026-04-05 00:13:37 -04:00
< 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 >
2026-04-04 22:46:16 -04:00
< / >
) : (
2026-04-05 00:13:37 -04:00
< div className = "usage-loading" > Loading usage stats ... < / div >
2026-04-04 22:46:16 -04:00
) }
< / div >
< / div >
2026-04-05 00:13:37 -04:00
{ /* Task Breakdown */ }
2026-04-04 22:46:16 -04:00
< div className = "dashboard-section" >
< h3 > Task Breakdown < / h3 >
< div className = "task-breakdown" >
{ tasks . length === 0 ? (
< p className = "empty-msg" > No tasks yet . Create one in the Tasks view . < / p >
) : (
tasks . map ( t => (
< div key = { t . id } className = "breakdown-row" >
< span className = "breakdown-name" > { t . name } < / span >
< span className = "breakdown-type" > { t . schedule _type } < / span >
< span className = { ` breakdown-status status- ${ t . status } ` } > { t . status } < / span >
< span className = "breakdown-last" > { t . last _run ? new Date ( t . last _run ) . toLocaleString ( ) : 'never' } < / span >
< / div >
) )
) }
< / div >
< / div >
< / div >
) }
2026-04-04 22:47:31 -04:00
{ activeView === 'tasks' && ( < >
2026-04-04 22:40:51 -04:00
< aside className = "sidebar" >
2026-04-05 00:13:37 -04:00
< button className = "btn btn-primary btn-create" onClick = { ( ) => setShowForm ( true ) } > + New Task < / button >
2026-04-04 22:40:51 -04:00
< 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 ) => (
2026-04-05 00:13:37 -04:00
< div key = { task . id } className = { ` task-item ${ selectedTask ? . id === task . id ? 'active' : '' } ` } onClick = { ( ) => setSelectedTask ( task ) } >
2026-04-04 22:40:51 -04:00
< div className = "task-item-header" >
< h3 > { task . name } < / h3 >
2026-04-05 00:13:37 -04:00
< span className = "status-badge" style = { { backgroundColor : getStatusColor ( task . status ) } } > { task . status } < / span >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:13:37 -04:00
< 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 > }
2026-04-04 22:40:51 -04:00
< / 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 >
2026-04-05 00:13:37 -04:00
< input type = "text" required value = { formData . name } onChange = { ( e ) => setFormData ( { ... formData , name : e . target . value } ) } placeholder = "e.g., Daily Backup" / >
2026-04-04 22:40:51 -04:00
< / div >
< div className = "form-group" >
< label > Description < / label >
2026-04-05 00:13:37 -04:00
< textarea value = { formData . description } onChange = { ( e ) => setFormData ( { ... formData , description : e . target . value } ) } placeholder = "Optional description" rows = { 2 } / >
2026-04-04 22:40:51 -04:00
< / div >
< div className = "form-group" >
< label > Claude Prompt < / label >
2026-04-05 00:13:37 -04:00
< textarea required value = { formData . prompt } onChange = { ( e ) => setFormData ( { ... formData , prompt : e . target . value } ) } placeholder = "What should Claude do? Be specific..." rows = { 6 } / >
2026-04-04 22:40:51 -04:00
< / div >
< div className = "form-row" >
< div className = "form-group" >
< label > Schedule Type < / label >
2026-04-05 00:13:37 -04:00
< select value = { formData . schedule _type } onChange = { ( e ) => setFormData ( { ... formData , schedule _type : e . target . value } ) } >
2026-04-04 22:40:51 -04:00
< 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 >
2026-04-05 00:13:37 -04:00
< input type = "text" value = { formData . schedule _value } onChange = { ( e ) => setFormData ( { ... formData , schedule _value : e . target . value } ) } placeholder = "0 9 * * * (daily at 9am)" / >
2026-04-04 22:40:51 -04:00
< / div >
) }
{ formData . schedule _type === 'once' && (
< div className = "form-group" >
< label > Run At < / label >
2026-04-05 00:13:37 -04:00
< input type = "datetime-local" value = { formData . schedule _value } onChange = { ( e ) => setFormData ( { ... formData , schedule _value : e . target . value } ) } / >
2026-04-04 22:40:51 -04:00
< / div >
) }
< / div >
< div className = "form-group checkbox" >
2026-04-05 00:13:37 -04:00
< input type = "checkbox" checked = { formData . enabled } onChange = { ( e ) => setFormData ( { ... formData , enabled : e . target . checked } ) } / >
2026-04-04 22:40:51 -04:00
< label > Enabled < / label >
< / div >
< div className = "form-actions" >
2026-04-05 00:13:37 -04:00
< 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 >
2026-04-04 22:40:51 -04:00
< / div >
< / form >
< / div >
) : selectedTask ? (
< div className = "task-detail" >
< div className = "task-detail-header" >
< h2 > { selectedTask . name } < / h2 >
< div className = "task-actions" >
2026-04-05 00:13:37 -04:00
< button className = "btn btn-success" onClick = { ( ) => handleRunTask ( selectedTask . id ) } > ▶ Run Now < / button >
< button className = "btn btn-danger" onClick = { ( ) => handleDeleteTask ( selectedTask . id ) } > 🗑 Delete < / button >
2026-04-04 22:40:51 -04:00
< / div >
< / div >
< div className = "task-meta" >
2026-04-05 00:13:37 -04:00
< 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 > }
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:13:37 -04:00
{ 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 >
2026-04-04 22:40:51 -04:00
< 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 >
2026-04-05 00:13:37 -04:00
< / > ) }
2026-04-04 22:40:51 -04:00
< / 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 >
2026-04-05 00:13:37 -04:00
< span className = "run-time" > { new Date ( run . started _at ) . toLocaleString ( ) } < / span >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:13:37 -04:00
{ run . output && < details > < summary > Output < / summary > < pre > { run . output } < / pre > < / details > }
{ run . error && < details > < summary > Error < / summary > < pre className = "error" > { run . error } < / pre > < / details > }
2026-04-04 22:40:51 -04:00
< / div >
) ) }
< / div >
) }
< / div >
) ;
} ;
export default App ;