diff --git a/AGENT_0_DEPENDENCY_FIX_REPORT_2025-12-26.md b/AGENT_0_DEPENDENCY_FIX_REPORT_2025-12-26.md index 6871995..580b550 100644 --- a/AGENT_0_DEPENDENCY_FIX_REPORT_2025-12-26.md +++ b/AGENT_0_DEPENDENCY_FIX_REPORT_2025-12-26.md @@ -907,6 +907,7 @@ b5d5aca - docs(agent-0): Add comprehensive code review report - Zeit: ~8-10h 6. **Production Deployment Verification** + ```bash ssh root@178.156.178.70 cd /root/cloud-agents diff --git a/docs/AGENT_CONTROL.md b/docs/AGENT_CONTROL.md new file mode 100644 index 0000000..36efb72 --- /dev/null +++ b/docs/AGENT_CONTROL.md @@ -0,0 +1,590 @@ +++ b/docs/AGENT_CONTROL.md +# 🤖 Agent Control API + +## Overview + +The Agent Control API provides endpoints to manage, monitor, and control agents in the Code Cloud Agents system. + +## Agents in the System + +The system includes three primary agents: + +1. **ENGINEERING_LEAD_SUPERVISOR** + - Plans and delegates tasks + - Reviews work and evidence + - Makes STOP decisions based on risk assessment + +2. **CLOUD_ASSISTANT** + - Executes tasks delegated by supervisor + - Reports progress and evidence + - Implements code changes + +3. **META_SUPERVISOR** + - Routes requests between agents + - Monitors overall system health + - Coordinates multi-agent workflows + +--- + +## Base URL + +``` +http://localhost:3000/api/agents +``` + +--- + +## Endpoints + +### 1. List All Agents + +**GET** `/api/agents` + +Returns a list of all agents in the system. + +**Response:** +```json +{ + "success": true, + "agents": [ + { + "id": "agent_engineering_lead_supervisor", + "name": "ENGINEERING_LEAD_SUPERVISOR", + "state": "idle", + "currentTask": null, + "progress": null, + "startedAt": "2025-12-26T14:00:00.000Z", + "lastActivity": "2025-12-26T14:05:00.000Z", + "tasksCompleted": 15, + "tasksInProgress": 0, + "errorCount": 0 + }, + { + "id": "agent_cloud_assistant", + "name": "CLOUD_ASSISTANT", + "state": "working", + "currentTask": "Implementing feature X", + "progress": 45, + "startedAt": "2025-12-26T14:00:00.000Z", + "lastActivity": "2025-12-26T14:10:00.000Z", + "tasksCompleted": 32, + "tasksInProgress": 1, + "errorCount": 2 + } + ], + "count": 3 +} +``` + +--- + +### 2. Get Agent Details + +**GET** `/api/agents/:agentId` + +Get detailed information about a specific agent. + +**Parameters:** +- `agentId` (path) - Agent ID (e.g., `agent_cloud_assistant`) + +**Response:** +```json +{ + "success": true, + "agent": { + "id": "agent_cloud_assistant", + "name": "CLOUD_ASSISTANT", + "state": "working", + "currentTask": "Implementing feature X", + "progress": 45, + "startedAt": "2025-12-26T14:00:00.000Z", + "lastActivity": "2025-12-26T14:10:00.000Z", + "tasksCompleted": 32, + "tasksInProgress": 1, + "errorCount": 2 + } +} +``` + +**Error Response (404):** +```json +{ + "success": false, + "error": "Agent not found" +} +``` + +--- + +### 3. Start Agent + +**POST** `/api/agents/:agentId/start` + +Start a stopped agent. + +**Parameters:** +- `agentId` (path) - Agent ID + +**Response:** +```json +{ + "success": true, + "message": "Agent started successfully", + "agent": { + "id": "agent_cloud_assistant", + "name": "CLOUD_ASSISTANT", + "state": "idle", + ... + } +} +``` + +**Error Response (400):** +```json +{ + "success": false, + "error": "Agent is already idle" +} +``` + +--- + +### 4. Stop Agent + +**POST** `/api/agents/:agentId/stop` + +Stop a running agent. + +**Request Body:** +```json +{ + "reason": "Maintenance required" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Agent stopped successfully", + "agent": { + "id": "agent_cloud_assistant", + "name": "CLOUD_ASSISTANT", + "state": "stopped", + ... + } +} +``` + +--- + +### 5. Update Agent State + +**PATCH** `/api/agents/:agentId/state` + +Manually update agent state. + +**Request Body:** +```json +{ + "state": "idle", + "reason": "Task completed" +} +``` + +**Valid States:** +- `idle` - Agent is ready for work +- `working` - Agent is processing a task +- `stopped` - Agent is stopped + +**Response:** +```json +{ + "success": true, + "agent": { + "id": "agent_cloud_assistant", + "state": "idle", + ... + } +} +``` + +--- + +### 6. Get Agent Logs + +**GET** `/api/agents/:agentId/logs` + +Retrieve agent activity logs. + +**Query Parameters:** +- `limit` (optional) - Number of logs to return (default: 100, max: 1000) +- `level` (optional) - Filter by log level (`info`, `warn`, `error`, `debug`) + +**Example:** +``` +GET /api/agents/agent_cloud_assistant/logs?limit=50&level=error +``` + +**Response:** +```json +{ + "success": true, + "logs": [ + { + "timestamp": "2025-12-26T14:10:00.000Z", + "level": "info", + "message": "Task started", + "context": { + "taskId": "task_123" + } + }, + { + "timestamp": "2025-12-26T14:15:00.000Z", + "level": "error", + "message": "API call failed", + "context": { + "error": "Connection timeout" + } + } + ], + "count": 2 +} +``` + +--- + +### 7. Get Agent Metrics + +**GET** `/api/agents/:agentId/metrics` + +Get performance metrics for an agent. + +**Response:** +```json +{ + "success": true, + "metrics": { + "uptime": 3600, + "totalTasks": 34, + "successfulTasks": 32, + "failedTasks": 2, + "averageTaskDuration": 0, + "memoryUsage": { + "rss": 52428800, + "heapTotal": 20971520, + "heapUsed": 15728640, + "external": 1048576 + }, + "cpuUsage": { + "user": 1000000, + "system": 500000 + } + } +} +``` + +**Metric Descriptions:** +- `uptime` - Agent uptime in seconds +- `totalTasks` - Total tasks processed (success + failed) +- `successfulTasks` - Number of completed tasks +- `failedTasks` - Number of failed tasks +- `averageTaskDuration` - Average task completion time (seconds) +- `memoryUsage` - Memory usage in bytes + - `rss` - Resident Set Size + - `heapTotal` - Total heap size + - `heapUsed` - Used heap size + - `external` - External memory +- `cpuUsage` - CPU usage in microseconds + - `user` - User CPU time + - `system` - System CPU time + +--- + +### 8. Get System Health + +**GET** `/api/agents/health/status` + +Get overall system health status. + +**Response:** +```json +{ + "success": true, + "health": "healthy", + "summary": { + "total": 3, + "idle": 2, + "working": 1, + "stopped": 0, + "error": 0 + }, + "agents": [ + { + "id": "agent_engineering_lead_supervisor", + "name": "ENGINEERING_LEAD_SUPERVISOR", + "state": "idle" + }, + { + "id": "agent_cloud_assistant", + "name": "CLOUD_ASSISTANT", + "state": "working" + }, + { + "id": "agent_meta_supervisor", + "name": "META_SUPERVISOR", + "state": "idle" + } + ] +} +``` + +**Health Status:** +- `healthy` - All agents running normally +- `degraded` - Some agents stopped but no errors +- `unhealthy` - One or more agents in error state + +--- + +## Agent States + +| State | Description | +|-------|-------------| +| `idle` | Agent is ready and waiting for tasks | +| `working` | Agent is actively processing a task | +| `stopped` | Agent has been manually stopped | +| `error` | Agent encountered an error | + +--- + +## State Transitions + +``` + idle ←→ working + ↑ ↓ + └─ stopped + ↓ + error +``` + +**Valid Transitions:** +- `idle` → `working` (task assigned) +- `working` → `idle` (task completed) +- `working` → `error` (task failed) +- `idle/working` → `stopped` (manual stop) +- `stopped` → `idle` (manual start) + +--- + +## WebSocket Integration + +Agent state changes are automatically broadcast via WebSocket: + +```javascript +// Connect to WebSocket +const ws = new WebSocket('ws://localhost:3000/ws?token=YOUR_TOKEN'); + +// Listen for agent status updates +ws.onmessage = (event) => { + const message = JSON.parse(event.data); + + if (message.type === 'agent_status') { + console.log('Agent update:', message.data); + // { + // agentName: 'CLOUD_ASSISTANT', + // state: 'working', + // currentTask: 'Processing webhook', + // progress: 50 + // } + } +}; +``` + +--- + +## Examples + +### cURL Examples + +**List all agents:** +```bash +curl http://localhost:3000/api/agents +``` + +**Get agent details:** +```bash +curl http://localhost:3000/api/agents/agent_cloud_assistant +``` + +**Start agent:** +```bash +curl -X POST http://localhost:3000/api/agents/agent_cloud_assistant/start +``` + +**Stop agent:** +```bash +curl -X POST http://localhost:3000/api/agents/agent_cloud_assistant/stop \ + -H "Content-Type: application/json" \ + -d '{"reason":"Maintenance"}' +``` + +**Update agent state:** +```bash +curl -X PATCH http://localhost:3000/api/agents/agent_cloud_assistant/state \ + -H "Content-Type: application/json" \ + -d '{"state":"idle","reason":"Task completed"}' +``` + +**Get agent logs:** +```bash +curl "http://localhost:3000/api/agents/agent_cloud_assistant/logs?limit=50&level=error" +``` + +**Get agent metrics:** +```bash +curl http://localhost:3000/api/agents/agent_cloud_assistant/metrics +``` + +**Get system health:** +```bash +curl http://localhost:3000/api/agents/health/status +``` + +--- + +### JavaScript/TypeScript Example + +```typescript +class AgentControlClient { + private baseUrl = 'http://localhost:3000/api/agents'; + + async listAgents() { + const response = await fetch(this.baseUrl); + return await response.json(); + } + + async getAgent(agentId: string) { + const response = await fetch(`${this.baseUrl}/${agentId}`); + return await response.json(); + } + + async startAgent(agentId: string) { + const response = await fetch(`${this.baseUrl}/${agentId}/start`, { + method: 'POST' + }); + return await response.json(); + } + + async stopAgent(agentId: string, reason?: string) { + const response = await fetch(`${this.baseUrl}/${agentId}/stop`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reason }) + }); + return await response.json(); + } + + async updateState(agentId: string, state: 'idle' | 'working' | 'stopped', reason?: string) { + const response = await fetch(`${this.baseUrl}/${agentId}/state`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ state, reason }) + }); + return await response.json(); + } + + async getLogs(agentId: string, limit = 100, level?: string) { + const params = new URLSearchParams({ limit: limit.toString() }); + if (level) params.append('level', level); + + const response = await fetch(`${this.baseUrl}/${agentId}/logs?${params}`); + return await response.json(); + } + + async getMetrics(agentId: string) { + const response = await fetch(`${this.baseUrl}/${agentId}/metrics`); + return await response.json(); + } + + async getSystemHealth() { + const response = await fetch(`${this.baseUrl}/health/status`); + return await response.json(); + } +} + +// Usage +const client = new AgentControlClient(); + +// List all agents +const agents = await client.listAgents(); +console.log('Agents:', agents); + +// Stop an agent for maintenance +await client.stopAgent('agent_cloud_assistant', 'System maintenance'); + +// Start agent again +await client.startAgent('agent_cloud_assistant'); + +// Monitor agent health +const health = await client.getSystemHealth(); +console.log('System health:', health.health); +``` + +--- + +## Error Handling + +All endpoints return errors in the following format: + +```json +{ + "success": false, + "error": "Error message description" +} +``` + +**HTTP Status Codes:** +- `200` - Success +- `400` - Bad request (invalid input) +- `404` - Agent not found +- `500` - Internal server error + +--- + +## Monitoring & Observability + +### Health Checks + +Monitor system health with: + +```bash +# Check overall health +curl http://localhost:3000/api/agents/health/status + +# Check individual agent metrics +curl http://localhost:3000/api/agents/agent_cloud_assistant/metrics +``` + +### Log Aggregation + +Retrieve agent logs for debugging: + +```bash +# Get recent errors +curl "http://localhost:3000/api/agents/agent_cloud_assistant/logs?level=error&limit=100" + +# Get all recent activity +curl "http://localhost:3000/api/agents/agent_cloud_assistant/logs?limit=1000" +``` + +--- + +**Last Updated:** 2025-12-26 +**Version:** 1.0.0 + +🤖 Generated with Claude Code +++ b/docs/WEBSOCKET.md +# 🔌 WebSocket Real-time Communication + +## Overview + diff --git a/src/App.tsx b/src/App.tsx index 3ebd283..b261b6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,9 @@ import { StatsCard } from "./components/StatsCard"; import { ActivityLog } from "./components/ActivityLog"; import { SettingsPanel } from "./components/SettingsPanel"; import { BrainMemoryPage } from "./components/BrainMemoryPage"; +import { UsersPage } from "./components/UsersPage"; +import { AuditPage } from "./components/AuditPage"; +import { IntegrationsPage } from "./components/IntegrationsPage"; import { Button } from "./components/ui/button"; import { Input } from "./components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; @@ -35,6 +38,9 @@ import { MessageSquare, CheckSquare, Brain, + Users, + FileText, + Link2, } from "lucide-react"; import { toast } from "sonner"; import { @@ -485,6 +491,30 @@ export default function App() { Brain + + + Users + + + + Audit + + + + Integrations + @@ -644,6 +674,18 @@ export default function App() { + + + + + + + + + + + + diff --git a/src/api/auth.ts b/src/api/auth.ts index 4c97124..b45973d 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -11,8 +11,13 @@ import { revokeToken, refreshAccessToken, } from "../auth/jwt.js"; -import { verifyUserPassword, getUserById } from "../db/users.js"; +import { + verifyUserPassword, + getUserById, + changeUserPassword, +} from "../db/users.js"; import { loginRateLimiter } from "../auth/rate-limiter.js"; +import { requireAdmin, type AuthenticatedRequest } from "../auth/middleware.js"; const db = initDatabase(); @@ -282,5 +287,84 @@ export function createAuthRouter(): Router { } }); + /** + * POST /api/auth/reset-password + * Reset user password (Admin only) + * Body: { userId: string, newPassword: string } + */ + router.post( + "/reset-password", + requireAdmin, + async (req: AuthenticatedRequest, res: Response) => { + try { + const { userId, newPassword } = req.body; + + // Validation + if (!userId || !newPassword) { + return res.status(400).json({ + error: "Missing required fields", + message: "userId and newPassword are required", + }); + } + + // Password strength validation + if (newPassword.length < 8) { + return res.status(400).json({ + error: "Password too weak", + message: "Password must be at least 8 characters", + }); + } + + // Check if target user exists + const rawDb = db.getRawDb(); + const targetUser = getUserById(rawDb, userId); + + if (!targetUser) { + return res.status(404).json({ + error: "User not found", + message: `No user found with ID: ${userId}`, + }); + } + + // Reset password + const success = await changeUserPassword(rawDb, userId, newPassword); + + if (!success) { + return res.status(500).json({ + error: "Password reset failed", + message: "Could not update password", + }); + } + + // Log password reset event + db.audit.log({ + kind: "password_reset", + message: `Admin ${req.userId} reset password for user ${targetUser.email}`, + userId: req.userId!, + severity: "warn", + meta: { + targetUserId: userId, + targetEmail: targetUser.email, + adminId: req.userId, + }, + }); + + res.json({ + success: true, + message: "Password reset successfully", + user: { + id: targetUser.id, + email: targetUser.email, + }, + }); + } catch (error) { + console.error("Reset password error:", error); + res.status(500).json({ + error: "Internal server error", + }); + } + }, + ); + return router; } diff --git a/src/brain/core-brain.ts b/src/brain/core-brain.ts new file mode 100644 index 0000000..209d058 --- /dev/null +++ b/src/brain/core-brain.ts @@ -0,0 +1,171 @@ +/** + * Core Brain Client - Connects to central brain-core API + * + * Provides read + append-only write access to the central knowledge base. + * No overwrites - only new entries are added. + * + * brain-core API uses headers for context: + * - x-org-id: Organization ID + * - x-user-id: User ID + * - x-project-id: Project ID + */ + +const CORE_BRAIN_ORIGIN = + process.env.CORE_BRAIN_ORIGIN || "http://49.13.158.176:5001"; +const DEFAULT_ORG_ID = "activi-dev"; +const DEFAULT_PROJECT_ID = "cloud-agents"; + +export interface CoreBrainMemory { + id: string; + type: string; + content: string; + tags?: string[]; + createdAt: string; +} + +export interface CoreBrainSearchResult { + id: string; + type: string; + content: string; + tags?: string[]; + createdAt?: string; +} + +export interface CoreBrainStoreParams { + userId: string; + content: string; + type?: string; + tags?: string[]; +} + +export interface CoreBrainSearchParams { + userId: string; + query: string; + limit?: number; +} + +/** + * Build headers for brain-core API + * User is auto-created in brain-core if not exists (via user-sync middleware) + */ +function buildHeaders(userId: string): Record { + return { + "Content-Type": "application/json", + "x-org-id": DEFAULT_ORG_ID, + "x-user-id": userId, + "x-project-id": DEFAULT_PROJECT_ID, + }; +} + +/** + * Search the central brain for relevant memories + */ +export async function coreBrainSearch( + params: CoreBrainSearchParams, +): Promise { + try { + const response = await fetch(`${CORE_BRAIN_ORIGIN}/api/memory/search`, { + method: "POST", + headers: buildHeaders(params.userId), + body: JSON.stringify({ + query: params.query, + limit: params.limit || 5, + }), + }); + + if (!response.ok) { + console.error(`[core-brain] Search failed: ${response.status}`); + return []; + } + + const data = await response.json(); + return data.results || []; + } catch (error) { + console.error("[core-brain] Search error:", error); + return []; + } +} + +/** + * Store new memory in central brain (append-only, no overwrites) + */ +export async function coreBrainStore( + params: CoreBrainStoreParams, +): Promise<{ success: boolean; id?: string }> { + try { + const response = await fetch(`${CORE_BRAIN_ORIGIN}/api/memory/store`, { + method: "POST", + headers: buildHeaders(params.userId), + body: JSON.stringify({ + type: params.type || "chat", + content: params.content, + tags: params.tags || [], + }), + }); + + if (!response.ok) { + console.error(`[core-brain] Store failed: ${response.status}`); + return { success: false }; + } + + const data = await response.json(); + return { success: true, id: data.id }; + } catch (error) { + console.error("[core-brain] Store error:", error); + return { success: false }; + } +} + +/** + * Get recent memories from central brain + */ +export async function coreBrainRecent( + userId: string, + limit: number = 10, +): Promise { + try { + const response = await fetch(`${CORE_BRAIN_ORIGIN}/api/memory/recent`, { + method: "POST", + headers: buildHeaders(userId), + body: JSON.stringify({ limit }), + }); + + if (!response.ok) { + console.error(`[core-brain] Recent failed: ${response.status}`); + return []; + } + + const data = await response.json(); + return data.results || []; + } catch (error) { + console.error("[core-brain] Recent error:", error); + return []; + } +} + +/** + * Build context string from core brain results for prompt injection + */ +export function buildCoreBrainContext( + results: CoreBrainSearchResult[], +): string { + if (results.length === 0) return ""; + + const lines = results.map((r, i) => `[${i + 1}] ${r.content}`); + return `\n--- Central Knowledge Base ---\n${lines.join("\n")}\n---\n`; +} + +/** + * Health check for core brain connection + */ +export async function coreBrainHealthCheck(): Promise { + try { + const response = await fetch(`${CORE_BRAIN_ORIGIN}/health`, { + method: "GET", + signal: AbortSignal.timeout(3000), + }); + return response.ok; + } catch { + return false; + } +} diff --git a/src/brain/index.ts b/src/brain/index.ts index 269e8df..d7c9aca 100644 --- a/src/brain/index.ts +++ b/src/brain/index.ts @@ -15,6 +15,21 @@ export type { export { BrainSearch } from "./search.js"; export type { BrainSearchResult, KeywordSearchResult } from "./search.js"; +// Core Brain Client (central knowledge base) +export { + coreBrainSearch, + coreBrainStore, + coreBrainRecent, + buildCoreBrainContext, + coreBrainHealthCheck, +} from "./core-brain.js"; +export type { + CoreBrainMemory, + CoreBrainSearchResult, + CoreBrainStoreParams, + CoreBrainSearchParams, +} from "./core-brain.js"; + // Re-export types from DB schema export type { BrainDoc, diff --git a/src/chat/manager.ts b/src/chat/manager.ts index 38c7059..981afc1 100644 --- a/src/chat/manager.ts +++ b/src/chat/manager.ts @@ -10,6 +10,11 @@ import { selectModel } from "../billing/modelSelector.ts"; import { agentTools, executeTool } from "./tools.ts"; import { trackAICall, log, metrics } from "../monitoring/sentry.js"; import { events } from "../lib/events.js"; +import { + coreBrainSearch, + coreBrainStore, + buildCoreBrainContext, +} from "../brain/core-brain.js"; import Anthropic from "@anthropic-ai/sdk"; import OpenAI from "openai"; import { GoogleGenerativeAI } from "@google/generative-ai"; @@ -100,11 +105,33 @@ export class ChatManager { ? this.storage.getRecentMessages(chat.id, request.maxHistory || 10) : []; - // Build prompt with history + // Pre-Recall: Search core brain for relevant context + let coreBrainContext = ""; + try { + const coreBrainResults = await coreBrainSearch({ + userId: request.userId, + query: request.message, + limit: 5, + }); + if (coreBrainResults.length > 0) { + coreBrainContext = buildCoreBrainContext(coreBrainResults); + log.info("Core brain pre-recall", { + userId: request.userId, + resultsCount: coreBrainResults.length, + }); + } + } catch (error) { + log.warn("Core brain pre-recall failed", { + error: error instanceof Error ? error.message : "Unknown", + }); + } + + // Build prompt with history and core brain context const prompt = this.buildPrompt( request.message, history, request.agentName, + coreBrainContext, ); // Select optimal model @@ -174,6 +201,27 @@ export class ChatManager { // Log chat event for audit trail events.chatSent(request.userId, request.agentName, request.message); + // Writeback: Store conversation in core brain (append-only, no overwrites) + try { + // Store the Q&A pair as a single memory entry + const memoryContent = `Q: ${request.message.substring(0, 500)}\nA: ${aiResponse.content.substring(0, 1000)}`; + await coreBrainStore({ + userId: request.userId, + content: memoryContent, + type: "chat", + tags: ["chat", request.agentName], + }); + log.info("Core brain writeback", { + userId: request.userId, + chatId: chat.id, + agentName: request.agentName, + }); + } catch (error) { + log.warn("Core brain writeback failed", { + error: error instanceof Error ? error.message : "Unknown", + }); + } + return { chatId: chat.id, messageId: assistantMessage.id, @@ -256,18 +304,25 @@ export class ChatManager { } /** - * Build prompt with chat history + * Build prompt with chat history and core brain context */ private buildPrompt( message: string, history: ChatMessage[], agentName: string, + coreBrainContext: string = "", ): string { let prompt = ""; // Add system message for agent prompt += `You are ${agentName}, a helpful AI assistant.\n\n`; + // Add core brain context if available + if (coreBrainContext) { + prompt += coreBrainContext; + prompt += "\n"; + } + // Add chat history if (history.length > 0) { prompt += "Previous conversation:\n"; diff --git a/src/components/AgentCard.tsx b/src/components/AgentCard.tsx index 076dd57..0aa8606 100644 --- a/src/components/AgentCard.tsx +++ b/src/components/AgentCard.tsx @@ -1,3 +1,8 @@ +/** + * AgentCard Component - MERGED VERSION + * Kombiniert: code-cloud-agents (Test-IDs) + PR#4 (Influencer, spokenLanguage, contentAutonomy) + */ + import { Card, CardContent, @@ -7,7 +12,17 @@ import { } from "./ui/card"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; -import { Play, Pause, Settings, Trash2, Activity } from "lucide-react"; +import { Play, Pause, Settings, Trash2, Activity, Sparkles, Globe } from "lucide-react"; + +/** + * Spoken language labels for display + */ +const SPOKEN_LANGUAGE_LABELS: Record = { + de: 'Deutsch', + en: 'English', + bs: 'Bosanski', + sr: 'Српски', +}; interface AgentCardProps { id: string; @@ -15,11 +30,14 @@ interface AgentCardProps { description: string; status: "active" | "paused" | "stopped"; language: string; + spokenLanguage?: 'de' | 'en' | 'bs' | 'sr'; // NEU aus PR#4 + agentType?: 'standard' | 'influencer'; // NEU aus PR#4 + contentAutonomy?: boolean; // NEU aus PR#4 lastRun: string; executionCount: number; onStart: (id: string) => void; onPause: (id: string) => void; - onConfigure: (id: string) => void; + onConfigure: (id: string) => void; // BEHALTEN aus code-cloud-agents onDelete: (id: string) => void; } @@ -29,6 +47,9 @@ export function AgentCard({ description, status, language, + spokenLanguage, + agentType, + contentAutonomy, lastRun, executionCount, onStart, @@ -42,14 +63,46 @@ export function AgentCard({ stopped: "bg-gray-500", }; + const isInfluencer = agentType === 'influencer'; + return ( - +
-
- {name} -
+
+ + {isInfluencer && } + {name} + +
+ {isInfluencer && ( + + Influencer + + )} + {contentAutonomy && ( + + Autonom + + )}
{description}
@@ -58,19 +111,42 @@ export function AgentCard({
- Language: - {language} + Code: + + {language} +
+ {spokenLanguage && ( +
+ + + Spricht: + + + {SPOKEN_LANGUAGE_LABELS[spokenLanguage] || spokenLanguage} + +
+ )}
- Executions: - + Ausführungen: + {executionCount}
- Last Run: - {lastRun} + Letzte Ausführung: + {lastRun}
{status === "active" ? ( diff --git a/src/components/AuditPage.tsx b/src/components/AuditPage.tsx new file mode 100644 index 0000000..8a75704 --- /dev/null +++ b/src/components/AuditPage.tsx @@ -0,0 +1,564 @@ +/** + * Audit & Ops Dashboard + * System events, audit logs, and operational stats + */ + +import { useState, useEffect } from "react"; +import { auditApi, opsApi, enforcementApi, type AuditEvent, type BlockedTask } from "../lib/api"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./ui/table"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "./ui/dialog"; +import { + FileText, + Activity, + AlertTriangle, + CheckCircle, + XCircle, + RefreshCw, + Search, + Shield, + Zap, + Server, + Filter, +} from "lucide-react"; +import { toast } from "sonner"; + +export function AuditPage() { + const [events, setEvents] = useState([]); + const [blockedTasks, setBlockedTasks] = useState([]); + const [opsStats, setOpsStats] = useState<{ + uptime: number; + activeAgents: number; + tasksToday: number; + errorsToday: number; + } | null>(null); + const [eventStats, setEventStats] = useState<{ + total: number; + byKind: Record; + bySeverity: Record; + } | null>(null); + const [loading, setLoading] = useState(true); + + // Filters + const [kindFilter, setKindFilter] = useState("all"); + const [severityFilter, setSeverityFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [limit, setLimit] = useState(50); + + // Dialog states + const [selectedEvent, setSelectedEvent] = useState(null); + const [selectedTask, setSelectedTask] = useState(null); + const [approveDialogOpen, setApproveDialogOpen] = useState(false); + + useEffect(() => { + loadData(); + }, [kindFilter, severityFilter, limit]); + + async function loadData() { + setLoading(true); + try { + const params: { limit?: number; kind?: string; severity?: string } = { limit }; + if (kindFilter !== "all") params.kind = kindFilter; + if (severityFilter !== "all") params.severity = severityFilter; + + const [eventsRes, statsRes, opsRes, blockedRes] = await Promise.all([ + auditApi.list(params), + auditApi.eventStats().catch(() => null), + opsApi.stats().catch(() => null), + enforcementApi.blocked().catch(() => ({ tasks: [] })), + ]); + + setEvents(eventsRes.events || []); + setEventStats(statsRes?.stats || null); + setOpsStats(opsRes); + setBlockedTasks(blockedRes.tasks || []); + } catch (error) { + console.error("Failed to load audit data:", error); + toast.error("Fehler beim Laden der Audit-Daten"); + } finally { + setLoading(false); + } + } + + async function handleApprove() { + if (!selectedTask) return; + try { + await enforcementApi.approve(selectedTask.taskId, "Approved by admin"); + toast.success("Task genehmigt"); + setApproveDialogOpen(false); + setSelectedTask(null); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + async function handleReject() { + if (!selectedTask) return; + try { + await enforcementApi.reject(selectedTask.taskId, "Rejected by admin"); + toast.success("Task abgelehnt"); + setApproveDialogOpen(false); + setSelectedTask(null); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Filter events by search + const filteredEvents = events.filter( + (event) => + event.message.toLowerCase().includes(searchQuery.toLowerCase()) || + event.kind.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Severity badge + function getSeverityBadge(severity: string) { + switch (severity) { + case "error": + return {severity}; + case "warn": + return {severity}; + default: + return {severity}; + } + } + + // Kind badge + function getKindBadge(kind: string) { + const colors: Record = { + user_login: "bg-green-500", + user_logout: "bg-gray-500", + password_reset: "bg-orange-500", + task_created: "bg-blue-500", + task_finished: "bg-green-600", + task_failed: "bg-red-500", + error: "bg-red-600", + deploy: "bg-purple-500", + }; + return ( + + {kind.replace(/_/g, " ")} + + ); + } + + // Format uptime + function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const mins = Math.floor((seconds % 3600) / 60); + if (days > 0) return `${days}d ${hours}h`; + if (hours > 0) return `${hours}h ${mins}m`; + return `${mins}m`; + } + + return ( +
+ {/* Header */} +
+
+

+ + Audit & Operations +

+

+ System-Events, Audit-Logs und Operations-Statistiken +

+
+ +
+ + {/* Stats Cards */} +
+ + + + Uptime + + + +
+ {opsStats ? formatUptime(opsStats.uptime) : "-"} +
+
+
+ + + + Tasks Heute + + + +
+ {opsStats?.tasksToday ?? "-"} +
+
+
+ + + + Fehler Heute + + + +
+ {opsStats?.errorsToday ?? "-"} +
+
+
+ + + + Blockiert + + + +
+ {blockedTasks.length} +
+
+
+
+ + {/* Tabs */} + + + + + Events ({events.length}) + + + + Blockiert ({blockedTasks.length}) + + + + Statistiken + + + + {/* Events Tab */} + + {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + data-testid="audit_input_search" + /> +
+ + + +
+ + {/* Events Table */} + + + + + + Zeit + Art + Severity + Nachricht + User + + + + {loading ? ( + + + Laden... + + + ) : filteredEvents.length === 0 ? ( + + + Keine Events gefunden + + + ) : ( + filteredEvents.map((event) => ( + setSelectedEvent(event)} + data-testid={`audit_row_${event.id}`} + > + + {new Date(event.ts).toLocaleString("de-DE")} + + {getKindBadge(event.kind)} + {getSeverityBadge(event.severity)} + + {event.message} + + + {event.userId || "-"} + + + )) + )} + +
+
+
+
+ + {/* Blocked Tasks Tab */} + + + + Blockierte Tasks + + Tasks die auf Genehmigung warten aufgrund hoher STOP-Scores + + + + {blockedTasks.length === 0 ? ( +
+ +

Keine blockierten Tasks

+
+ ) : ( + + + + Task ID + Grund + STOP Score + Erstellt + Aktionen + + + + {blockedTasks.map((task) => ( + + {task.taskId.slice(0, 8)}... + {task.reason} + + = 70 ? "destructive" : "secondary"} + > + {task.stopScore} + + + + {new Date(task.createdAt).toLocaleString("de-DE")} + + +
+ + +
+
+
+ ))} +
+
+ )} +
+
+
+ + {/* Stats Tab */} + +
+ + + Events nach Art + + + {eventStats?.byKind ? ( +
+ {Object.entries(eventStats.byKind).map(([kind, count]) => ( +
+ {kind.replace(/_/g, " ")} + {count} +
+ ))} +
+ ) : ( +

Keine Daten

+ )} +
+
+ + + Events nach Severity + + + {eventStats?.bySeverity ? ( +
+ {Object.entries(eventStats.bySeverity).map(([severity, count]) => ( +
+ {severity} + {getSeverityBadge(severity)} + {count} +
+ ))} +
+ ) : ( +

Keine Daten

+ )} +
+
+
+
+
+ + {/* Event Detail Dialog */} + setSelectedEvent(null)}> + + + Event Details + + {selectedEvent && ( +
+
+
+

ID

+

{selectedEvent.id}

+
+
+

Zeit

+

{new Date(selectedEvent.ts).toLocaleString("de-DE")}

+
+
+

Art

+ {getKindBadge(selectedEvent.kind)} +
+
+

Severity

+ {getSeverityBadge(selectedEvent.severity)} +
+
+
+

Nachricht

+

{selectedEvent.message}

+
+ {selectedEvent.meta && ( +
+

Metadaten

+
+                    {JSON.stringify(selectedEvent.meta, null, 2)}
+                  
+
+ )} +
+ )} +
+
+ + {/* Approve Dialog */} + + + + Task genehmigen + + Möchten Sie diesen Task wirklich genehmigen? Der STOP-Score beträgt{" "} + {selectedTask?.stopScore}. + + + + + + + + +
+ ); +} diff --git a/src/components/CreateAgentDialog.tsx b/src/components/CreateAgentDialog.tsx index 8caa2c1..449d543 100644 --- a/src/components/CreateAgentDialog.tsx +++ b/src/components/CreateAgentDialog.tsx @@ -1,3 +1,9 @@ +/** + * CreateAgentDialog Component - MERGED VERSION + * Kombiniert: code-cloud-agents (Test-IDs, DialogDescription, DialogFooter) + * + PR#4 (Influencer, spokenLanguage, contentAutonomy, Switch) + */ + import { Dialog, DialogContent, @@ -17,6 +23,7 @@ import { SelectValue, } from "./ui/select"; import { Button } from "./ui/button"; +import { Switch } from "./ui/switch"; import { useState } from "react"; interface CreateAgentDialogProps { @@ -27,9 +34,22 @@ interface CreateAgentDialogProps { description: string; language: string; code: string; + spokenLanguage?: 'de' | 'en' | 'bs' | 'sr'; // NEU aus PR#4 + agentType?: 'standard' | 'influencer'; // NEU aus PR#4 + contentAutonomy?: boolean; // NEU aus PR#4 }) => void; } +/** + * Spoken language labels for display + */ +const SPOKEN_LANGUAGE_LABELS: Record = { + de: 'Deutsch', + en: 'English', + bs: 'Bosanski', + sr: 'Српски (Serbian)', +}; + export function CreateAgentDialog({ open, onOpenChange, @@ -39,21 +59,49 @@ export function CreateAgentDialog({ const [description, setDescription] = useState(""); const [language, setLanguage] = useState("python"); const [code, setCode] = useState(""); + const [spokenLanguage, setSpokenLanguage] = useState<'de' | 'en' | 'bs' | 'sr'>('de'); + const [agentType, setAgentType] = useState<'standard' | 'influencer'>('standard'); + const [contentAutonomy, setContentAutonomy] = useState(false); const handleCreate = () => { if (name && description && code) { - onCreate({ name, description, language, code }); + onCreate({ + name, + description, + language, + code, + spokenLanguage, + agentType, + contentAutonomy: agentType === 'influencer' ? contentAutonomy : false, + }); + // Reset form setName(""); setDescription(""); setLanguage("python"); setCode(""); + setSpokenLanguage('de'); + setAgentType('standard'); + setContentAutonomy(false); onOpenChange(false); } }; + /** + * Handle agent type change - auto-enable content autonomy for influencers + */ + const handleAgentTypeChange = (value: 'standard' | 'influencer') => { + setAgentType(value); + if (value === 'influencer') { + setContentAutonomy(true); + } + }; + return ( - + Create New Agent @@ -81,21 +129,82 @@ export function CreateAgentDialog({ data-testid="cloudagents.agent.create.description.input" />
+ + {/* Grid für Programmiersprache und gesprochene Sprache */} +
+
+ + +
+
+ + +
+
+ + {/* Agent-Typ Auswahl */}
- - handleAgentTypeChange(v as 'standard' | 'influencer')}> + - Python - JavaScript - TypeScript - Go - Rust + Standard Agent + Influencer Agent + {agentType === 'influencer' && ( +

+ Influencer-Agenten können eigenständig Content erstellen und veröffentlichen. +

+ )}
+ + {/* Content-Autonomie (nur für Influencer) */} + {agentType === 'influencer' && ( +
+
+ +

+ Agent erstellt und veröffentlicht Content eigenständig +

+
+ +
+ )} +