diff options
| author | soryu <soryu@soryu.co> | 2026-01-27 11:04:20 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-27 11:04:20 +0000 |
| commit | c618174e60e4632d36d7352d83399508c72b2f42 (patch) | |
| tree | fbca74b921a57165aea046b959a44ab00589532f /frontend/src/components/ContractDetail.tsx | |
| parent | b6f239c19f0d3130515f3745f842e17a69212295 (diff) | |
| download | soryu-c618174e60e4632d36d7352d83399508c72b2f42.tar.gz soryu-c618174e60e4632d36d7352d83399508c72b2f42.zip | |
Add Red Team CLI command and frontend UI (#39)
* Add Red Team CLI command and frontend UI
Backend additions:
- Add `makima red-team notify` CLI command for red team tasks
- Add RedTeamCommand enum with Notify subcommand
- Add red_team API client module for notify endpoint
- Add RedTeamNotifyArgs with severity, task, file, context options
Frontend additions:
- Add ContractCreateModal with red team toggle and prompt input
- Update ContractDetail with red-team tab for notifications
- Update ContractList with red team enabled badge
- Add TypeScript types for RedTeamNotification and related interfaces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add CSS styles for Red Team frontend components
Add comprehensive styling for:
- Contract list and detail containers
- Red team badge styling with gradient backgrounds
- Tab navigation with red team specific styling
- Red team notifications panel with severity indicators
- Contract creation modal form elements
- Task badges for supervisor and red team roles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing local_only field in TUI CreateContractRequest
Add the missing local_only field to the CreateContractRequest struct
initialization in the TUI contract creation handler.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* [WIP] Heartbeat checkpoint - 2026-01-27 03:07:28 UTC
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'frontend/src/components/ContractDetail.tsx')
| -rw-r--r-- | frontend/src/components/ContractDetail.tsx | 139 |
1 files changed, 135 insertions, 4 deletions
diff --git a/frontend/src/components/ContractDetail.tsx b/frontend/src/components/ContractDetail.tsx index 72527ce..a9dd550 100644 --- a/frontend/src/components/ContractDetail.tsx +++ b/frontend/src/components/ContractDetail.tsx @@ -12,6 +12,8 @@ interface TaskSummary { id: string name: string status: string + is_supervisor?: boolean + is_red_team?: boolean } interface ContractRepository { @@ -30,6 +32,24 @@ interface Contract { status: string version: number created_at: string + red_team_enabled?: boolean + red_team_prompt?: string +} + +interface RedTeamNotification { + id: string + contract_id: string + red_team_task_id: string + related_task_id?: string + message: string + severity: 'info' | 'warning' | 'critical' + file_path?: string + context?: string + delivered: boolean + delivered_at?: string + acknowledged: boolean + acknowledged_at?: string + created_at: string } interface ContractWithRelations { @@ -39,7 +59,7 @@ interface ContractWithRelations { tasks: TaskSummary[] } -type Tab = 'overview' | 'files' | 'tasks' | 'repositories' +type Tab = 'overview' | 'files' | 'tasks' | 'repositories' | 'red-team' export function ContractDetail() { const { id } = useParams<{ id: string }>() @@ -47,6 +67,8 @@ export function ContractDetail() { const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) const [activeTab, setActiveTab] = useState<Tab>('overview') + const [notifications, setNotifications] = useState<RedTeamNotification[]>([]) + const [notificationsLoading, setNotificationsLoading] = useState(false) useEffect(() => { async function fetchContract() { @@ -70,6 +92,28 @@ export function ContractDetail() { fetchContract() }, [id]) + // Fetch red team notifications when viewing red-team tab + useEffect(() => { + async function fetchNotifications() { + if (!id || activeTab !== 'red-team' || !data?.contract.red_team_enabled) return + + try { + setNotificationsLoading(true) + const response = await fetch(`/api/v1/contracts/${id}/red-team/notifications`) + if (response.ok) { + const notificationData = await response.json() + setNotifications(notificationData.notifications || []) + } + } catch (err) { + console.error('Failed to fetch red team notifications:', err) + } finally { + setNotificationsLoading(false) + } + } + + fetchNotifications() + }, [id, activeTab, data?.contract.red_team_enabled]) + if (loading) { return ( <div className="contract-detail-container"> @@ -108,7 +152,14 @@ export function ContractDetail() { <Link to="/contracts" className="back-link"> Back to Contracts </Link> - <h1 className="contract-title">{contract.name}</h1> + <h1 className="contract-title"> + {contract.name} + {contract.red_team_enabled && ( + <span className="red-team-badge" title="Red Team monitoring enabled"> + đĄī¸ Red Team + </span> + )} + </h1> {contract.description && ( <p className="contract-description">{contract.description}</p> )} @@ -144,6 +195,14 @@ export function ContractDetail() { > Repositories ({repositories.length}) </button> + {contract.red_team_enabled && ( + <button + className={`tab-button red-team-tab ${activeTab === 'red-team' ? 'active' : ''}`} + onClick={() => setActiveTab('red-team')} + > + đĄī¸ Red Team + </button> + )} </div> <div className="contract-tab-content"> @@ -194,8 +253,12 @@ export function ContractDetail() { ) : ( <ul className="task-list"> {tasks.map((task) => ( - <li key={task.id} className="task-item"> - <h3>{task.name}</h3> + <li key={task.id} className={`task-item ${task.is_red_team ? 'red-team-task' : ''}`}> + <h3> + {task.is_red_team && <span className="task-badge red-team">đĄī¸</span>} + {task.is_supervisor && <span className="task-badge supervisor">đ</span>} + {task.name} + </h3> <span className={`task-status status-${task.status}`}> {task.status} </span> @@ -226,6 +289,74 @@ export function ContractDetail() { )} </div> )} + + {activeTab === 'red-team' && contract.red_team_enabled && ( + <div className="tab-panel red-team-panel"> + <h2>đĄī¸ Red Team Monitoring</h2> + + {contract.red_team_prompt && ( + <div className="red-team-prompt"> + <h3>Review Criteria</h3> + <p>{contract.red_team_prompt}</p> + </div> + )} + + <div className="red-team-status"> + <h3>Red Team Task</h3> + {(() => { + const redTeamTask = tasks.find(t => t.is_red_team) + if (redTeamTask) { + return ( + <div className="red-team-task-info"> + <span className="task-name">{redTeamTask.name}</span> + <span className={`task-status status-${redTeamTask.status}`}> + {redTeamTask.status} + </span> + </div> + ) + } + return <p className="no-task">Red team task not yet spawned</p> + })()} + </div> + + <div className="red-team-notifications"> + <h3>Alerts ({notifications.length})</h3> + {notificationsLoading ? ( + <p>Loading notifications...</p> + ) : notifications.length === 0 ? ( + <p className="no-alerts">No alerts from red team</p> + ) : ( + <ul className="notification-list"> + {notifications.map((notification) => ( + <li + key={notification.id} + className={`notification-item severity-${notification.severity}`} + > + <div className="notification-header"> + <span className={`severity-badge ${notification.severity}`}> + {notification.severity === 'critical' && 'đ¨'} + {notification.severity === 'warning' && 'â ī¸'} + {notification.severity === 'info' && 'âšī¸'} + {notification.severity.toUpperCase()} + </span> + <span className="notification-time"> + {new Date(notification.created_at).toLocaleString()} + </span> + </div> + <p className="notification-message">{notification.message}</p> + {notification.file_path && ( + <span className="notification-file">File: {notification.file_path}</span> + )} + {notification.context && ( + <pre className="notification-context">{notification.context}</pre> + )} + </li> + ))} + </ul> + )} + </div> + </div> + )} </div> </div> ) |
