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 + + + + + Aktualisieren + + + + {/* 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" + /> + + + + + + + + Alle Arten + Login + Logout + Passwort Reset + Task erstellt + Task beendet + Task fehlgeschlagen + Fehler + Deploy + + + + + + + + Alle + Info + Warnung + Fehler + + + setLimit(Number(v))}> + + + + + 25 + 50 + 100 + 200 + + + + + {/* 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")} + + + + { + setSelectedTask(task); + setApproveDialogOpen(true); + }} + > + + Genehmigen + + { + setSelectedTask(task); + handleReject(); + }} + > + + Ablehnen + + + + + ))} + + + )} + + + + + {/* 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}. + + + + setApproveDialogOpen(false)}> + Abbrechen + + Genehmigen + + + + + ); +} 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 */} + + + Programmiersprache + + + + + + Python + JavaScript + TypeScript + Go + Rust + + + + + Spricht (Sprache) + setSpokenLanguage(v as 'de' | 'en' | 'bs' | 'sr')}> + + + + + {SPOKEN_LANGUAGE_LABELS.de} + {SPOKEN_LANGUAGE_LABELS.en} + {SPOKEN_LANGUAGE_LABELS.bs} + {SPOKEN_LANGUAGE_LABELS.sr} + + + + + + {/* Agent-Typ Auswahl */} - Language - - + Agent-Typ + 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' && ( + + + Content-Autonomie + + Agent erstellt und veröffentlicht Content eigenständig + + + + + )} + Code + + + ); +} + +export function IntegrationsPage() { + // GitHub state + const [githubConnected, setGithubConnected] = useState(false); + const [githubRepos, setGithubRepos] = useState([]); + const [githubIssues, setGithubIssues] = useState([]); + + // Linear state + const [linearConnected, setLinearConnected] = useState(false); + const [linearTeams, setLinearTeams] = useState([]); + const [linearIssues, setLinearIssues] = useState([]); + + // Webhooks state + const [webhooks, setWebhooks] = useState([]); + + // UI state + const [loading, setLoading] = useState(true); + const [createWebhookOpen, setCreateWebhookOpen] = useState(false); + const [createIssueOpen, setCreateIssueOpen] = useState(false); + const [issueType, setIssueType] = useState<"github" | "linear">("github"); + + // Form state + const [webhookForm, setWebhookForm] = useState({ + url: "", + events: [] as string[], + secret: "", + }); + const [issueForm, setIssueForm] = useState({ + title: "", + body: "", + repo: "", + teamId: "", + }); + + useEffect(() => { + loadData(); + }, []); + + async function loadData() { + setLoading(true); + try { + // Load GitHub data + const ghStatus = await githubApi.status().catch(() => ({ connected: false })); + setGithubConnected(ghStatus.connected); + if (ghStatus.connected) { + const [reposRes, issuesRes] = await Promise.all([ + githubApi.repos().catch(() => ({ repos: [] })), + githubApi.issues().catch(() => ({ issues: [] })), + ]); + setGithubRepos(reposRes.repos); + setGithubIssues(issuesRes.issues); + } + + // Load Linear data + const linearStatus = await linearApi.status().catch(() => ({ connected: false })); + setLinearConnected(linearStatus.connected); + if (linearStatus.connected) { + const [teamsRes, issuesRes] = await Promise.all([ + linearApi.teams().catch(() => ({ teams: [] })), + linearApi.issues().catch(() => ({ issues: [] })), + ]); + setLinearTeams(teamsRes.teams); + setLinearIssues(issuesRes.issues); + } + + // Load webhooks + const webhooksRes = await webhooksApi.list().catch(() => ({ webhooks: [] })); + setWebhooks(webhooksRes.webhooks); + } catch (error) { + console.error("Failed to load integrations:", error); + } finally { + setLoading(false); + } + } + + async function handleCreateWebhook() { + try { + await webhooksApi.create({ + url: webhookForm.url, + events: webhookForm.events, + secret: webhookForm.secret || undefined, + }); + toast.success("Webhook erstellt"); + setCreateWebhookOpen(false); + setWebhookForm({ url: "", events: [], secret: "" }); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + async function handleDeleteWebhook(id: string) { + try { + await webhooksApi.delete(id); + toast.success("Webhook gelöscht"); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + async function handleTestWebhook(id: string) { + try { + await webhooksApi.test(id); + toast.success("Test-Webhook gesendet"); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + async function handleCreateIssue() { + try { + if (issueType === "github") { + await githubApi.createIssue({ + title: issueForm.title, + body: issueForm.body, + repo: issueForm.repo, + }); + } else { + await linearApi.createIssue({ + title: issueForm.title, + description: issueForm.body, + teamId: issueForm.teamId, + }); + } + toast.success("Issue erstellt"); + setCreateIssueOpen(false); + setIssueForm({ title: "", body: "", repo: "", teamId: "" }); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + const availableEvents = [ + "task.created", + "task.completed", + "task.failed", + "agent.started", + "agent.stopped", + "user.login", + "deploy.success", + "deploy.failed", + ]; + + return ( + + {/* Header */} + + + + + Integrationen + + + GitHub, Linear und Webhook-Verbindungen verwalten + + + + + Aktualisieren + + + + + + + + GitHub + + + + Linear + + + + Webhooks ({webhooks.length}) + + + + {/* GitHub Tab */} + + + + + + + + GitHub Integration + + + Repositories und Issues verwalten + + + + {githubConnected ? ( + <> + Verbunden + > + ) : ( + <> + Nicht verbunden + > + )} + + + + + {!githubConnected ? ( + + + + GitHub ist nicht verbunden. Konfigurieren Sie GITHUB_TOKEN in den Umgebungsvariablen. + + + ) : ( + + {/* Repos */} + + Repositories ({githubRepos.length}) + + {githubRepos.slice(0, 6).map((repo) => ( + + + + {repo.name} + + {repo.description || "Keine Beschreibung"} + + + + + + + + ))} + + + + {/* Issues */} + + + Offene Issues ({githubIssues.length}) + { + setIssueType("github"); + setCreateIssueOpen(true); + }} + > + + Neues Issue + + + + + + # + Titel + Status + Erstellt + + + + {githubIssues.slice(0, 5).map((issue) => ( + + #{issue.number} + {issue.title} + + + {issue.state} + + + + {new Date(issue.createdAt).toLocaleDateString("de-DE")} + + + ))} + + + + + )} + + + + + {/* Linear Tab */} + + + + + + + + Linear Integration + + + Teams und Issues verwalten + + + + {linearConnected ? ( + <> + Verbunden + > + ) : ( + <> + Nicht verbunden + > + )} + + + + + {!linearConnected ? ( + + + + Linear ist nicht verbunden. Konfigurieren Sie LINEAR_API_KEY in den Umgebungsvariablen. + + + ) : ( + + {/* Teams */} + + Teams ({linearTeams.length}) + + {linearTeams.map((team) => ( + + {team.name} ({team.key}) + + ))} + + + + {/* Issues */} + + + Issues ({linearIssues.length}) + { + setIssueType("linear"); + setCreateIssueOpen(true); + }} + > + + Neues Issue + + + + + + Titel + Status + Priorität + + + + {linearIssues.slice(0, 5).map((issue) => ( + + {issue.title} + + {issue.state} + + + + P{issue.priority} + + + + ))} + + + + + )} + + + + + {/* Webhooks Tab */} + + + + + + + + Webhooks + + + Externe Dienste bei Events benachrichtigen + + + setCreateWebhookOpen(true)}> + + Neuer Webhook + + + + + {webhooks.length === 0 ? ( + + + + Keine Webhooks konfiguriert + + setCreateWebhookOpen(true)}> + + Ersten Webhook erstellen + + + ) : ( + + + + URL + Events + Status + Erstellt + Aktionen + + + + {webhooks.map((webhook) => ( + + + {webhook.url} + + + + {webhook.events.slice(0, 2).map((event) => ( + + {event} + + ))} + {webhook.events.length > 2 && ( + + +{webhook.events.length - 2} + + )} + + + + + + + {new Date(webhook.createdAt).toLocaleDateString("de-DE")} + + + + handleTestWebhook(webhook.id)} + title="Test senden" + > + + + handleDeleteWebhook(webhook.id)} + title="Löschen" + > + + + + + + ))} + + + )} + + + + + + {/* Create Webhook Dialog */} + + + + Neuen Webhook erstellen + + Konfigurieren Sie einen Webhook um bei Events benachrichtigt zu werden. + + + + + URL * + setWebhookForm({ ...webhookForm, url: e.target.value })} + placeholder="https://example.com/webhook" + /> + + + Events * + + {availableEvents.map((event) => ( + { + const events = webhookForm.events.includes(event) + ? webhookForm.events.filter((e) => e !== event) + : [...webhookForm.events, event]; + setWebhookForm({ ...webhookForm, events }); + }} + > + {event} + + ))} + + + + Secret (optional) + setWebhookForm({ ...webhookForm, secret: e.target.value })} + placeholder="Webhook-Signatur-Secret" + /> + + + + setCreateWebhookOpen(false)}> + Abbrechen + + + Erstellen + + + + + + {/* Create Issue Dialog */} + + + + + {issueType === "github" ? "GitHub Issue erstellen" : "Linear Issue erstellen"} + + + + + Titel * + setIssueForm({ ...issueForm, title: e.target.value })} + placeholder="Issue Titel" + /> + + + Beschreibung + setIssueForm({ ...issueForm, body: e.target.value })} + placeholder="Issue Beschreibung..." + rows={4} + /> + + {issueType === "github" && githubRepos.length > 0 && ( + + Repository * + + {githubRepos.slice(0, 6).map((repo) => ( + setIssueForm({ ...issueForm, repo: repo.fullName })} + > + {repo.name} + + ))} + + + )} + {issueType === "linear" && linearTeams.length > 0 && ( + + Team * + + {linearTeams.map((team) => ( + setIssueForm({ ...issueForm, teamId: team.id })} + > + {team.name} + + ))} + + + )} + + + setCreateIssueOpen(false)}> + Abbrechen + + + Erstellen + + + + + + ); +} diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx new file mode 100644 index 0000000..0f92d44 --- /dev/null +++ b/src/components/ThemeProvider.tsx @@ -0,0 +1,104 @@ +/** + * Theme Provider Component + * Provides dark/light theme switching functionality + */ + +import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface ThemeContextValue { + theme: Theme; + setTheme: (theme: Theme) => void; + resolvedTheme: 'light' | 'dark'; +} + +const ThemeContext = createContext(undefined); + +/** + * Gets the system's preferred color scheme + */ +function getSystemTheme(): 'light' | 'dark' { + if (typeof window === 'undefined') return 'light'; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +interface ThemeProviderProps { + children: ReactNode; + defaultTheme?: Theme; + storageKey?: string; +} + +/** + * Theme Provider Component + * Handles theme persistence and application + */ +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'cca-theme', +}: ThemeProviderProps) { + const [theme, setThemeState] = useState(() => { + if (typeof window === 'undefined') return defaultTheme; + return (localStorage.getItem(storageKey) as Theme) || defaultTheme; + }); + + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => { + if (theme === 'system') return getSystemTheme(); + return theme; + }); + + useEffect(() => { + const root = window.document.documentElement; + + // Remove existing theme classes + root.classList.remove('light', 'dark'); + + // Determine which theme to apply + const resolved = theme === 'system' ? getSystemTheme() : theme; + setResolvedTheme(resolved); + + // Apply theme class + root.classList.add(resolved); + }, [theme]); + + // Listen for system theme changes + useEffect(() => { + if (theme !== 'system') return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => { + const systemTheme = getSystemTheme(); + setResolvedTheme(systemTheme); + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(systemTheme); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); + + const setTheme = (newTheme: Theme) => { + localStorage.setItem(storageKey, newTheme); + setThemeState(newTheme); + }; + + return ( + + + {children} + + + ); +} + +/** + * Hook to access theme context + */ +export function useTheme(): ThemeContextValue { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..12cfce2 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,70 @@ +/** + * Theme Toggle Component + * Button to switch between light/dark/system themes + */ + +import { Moon, Sun, Monitor } from 'lucide-react'; +import { Button } from './ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; +import { useTheme } from './ThemeProvider'; + +/** + * Theme toggle dropdown button + */ +export function ThemeToggle() { + const { theme, setTheme, resolvedTheme } = useTheme(); + + return ( + + + + {resolvedTheme === 'dark' ? ( + + ) : ( + + )} + Toggle theme + + + + setTheme('light')} + className={theme === 'light' ? 'bg-accent' : ''} + data-testid="cloudagents.theme.toggle.option.light" + > + + Light + + setTheme('dark')} + className={theme === 'dark' ? 'bg-accent' : ''} + data-testid="cloudagents.theme.toggle.option.dark" + > + + Dark + + setTheme('system')} + className={theme === 'system' ? 'bg-accent' : ''} + data-testid="cloudagents.theme.toggle.option.system" + > + + System + + + + ); +} diff --git a/src/components/UsersPage.tsx b/src/components/UsersPage.tsx new file mode 100644 index 0000000..6ddc759 --- /dev/null +++ b/src/components/UsersPage.tsx @@ -0,0 +1,594 @@ +/** + * Users Management Page + * Admin-only page for user CRUD operations + */ + +import { useState, useEffect } from "react"; +import { usersApi, authApi, type User } from "../lib/api"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "./ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; +import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Switch } from "./ui/switch"; +import { + Users, + UserPlus, + MoreHorizontal, + Pencil, + Trash2, + Key, + Shield, + UserCheck, + AlertCircle, + Search, + RefreshCw, +} from "lucide-react"; +import { toast } from "sonner"; + +export function UsersPage() { + const [users, setUsers] = useState([]); + const [stats, setStats] = useState<{ + total: number; + active: number; + admins: number; + users: number; + demos: number; + } | null>(null); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + + // Dialog states + const [createDialogOpen, setCreateDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + + // Form states + const [formData, setFormData] = useState({ + email: "", + password: "", + displayName: "", + role: "user" as "admin" | "user" | "demo", + }); + const [newPassword, setNewPassword] = useState(""); + + // Load users and stats + useEffect(() => { + loadData(); + }, []); + + async function loadData() { + setLoading(true); + try { + const [usersRes, statsRes] = await Promise.all([ + usersApi.list(), + usersApi.stats(), + ]); + setUsers(usersRes.users || []); + setStats(statsRes); + } catch (error) { + console.error("Failed to load users:", error); + toast.error("Fehler beim Laden der Benutzer"); + } finally { + setLoading(false); + } + } + + // Create user + async function handleCreate() { + try { + await usersApi.create({ + email: formData.email, + password: formData.password, + role: formData.role, + displayName: formData.displayName || undefined, + }); + toast.success("Benutzer erstellt"); + setCreateDialogOpen(false); + setFormData({ email: "", password: "", displayName: "", role: "user" }); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Update user + async function handleUpdate() { + if (!selectedUser) return; + try { + await usersApi.update(selectedUser.id, { + email: formData.email, + displayName: formData.displayName, + role: formData.role, + }); + toast.success("Benutzer aktualisiert"); + setEditDialogOpen(false); + setSelectedUser(null); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Delete user + async function handleDelete() { + if (!selectedUser) return; + try { + await usersApi.delete(selectedUser.id); + toast.success("Benutzer gelöscht"); + setDeleteDialogOpen(false); + setSelectedUser(null); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Reset password + async function handleResetPassword() { + if (!selectedUser || !newPassword) return; + try { + await authApi.resetPassword(selectedUser.id, newPassword); + toast.success("Passwort zurückgesetzt"); + setResetPasswordDialogOpen(false); + setSelectedUser(null); + setNewPassword(""); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Toggle user active status + async function handleToggleActive(user: User) { + try { + await usersApi.update(user.id, { isActive: !user.isActive }); + toast.success(user.isActive ? "Benutzer deaktiviert" : "Benutzer aktiviert"); + loadData(); + } catch (error: unknown) { + toast.error(`Fehler: ${error instanceof Error ? error.message : "Unbekannt"}`); + } + } + + // Open edit dialog + function openEditDialog(user: User) { + setSelectedUser(user); + setFormData({ + email: user.email, + password: "", + displayName: user.displayName || "", + role: user.role, + }); + setEditDialogOpen(true); + } + + // Filter users + const filteredUsers = users.filter( + (user) => + user.email.toLowerCase().includes(searchQuery.toLowerCase()) || + (user.displayName?.toLowerCase() || "").includes(searchQuery.toLowerCase()) + ); + + // Role badge color + function getRoleBadge(role: string) { + switch (role) { + case "admin": + return {role}; + case "demo": + return {role}; + default: + return {role}; + } + } + + return ( + + {/* Header */} + + + + + Benutzerverwaltung + + + Benutzer erstellen, bearbeiten und verwalten + + + setCreateDialogOpen(true)} data-testid="users_button_create"> + + Neuer Benutzer + + + + {/* Stats Cards */} + {stats && ( + + + + + Gesamt + + + + {stats.total} + + + + + + Aktiv + + + + {stats.active} + + + + + + Admins + + + + {stats.admins} + + + + + + Users + + + + {stats.users} + + + + + + Demos + + + + {stats.demos} + + + + )} + + {/* Search and Refresh */} + + + + setSearchQuery(e.target.value)} + className="pl-10" + data-testid="users_input_search" + /> + + + + + + + {/* Users Table */} + + + + + + Email + Name + Rolle + Status + Erstellt + Letzter Login + + + + + {loading ? ( + + + Laden... + + + ) : filteredUsers.length === 0 ? ( + + + Keine Benutzer gefunden + + + ) : ( + filteredUsers.map((user) => ( + + {user.email} + {user.displayName || "-"} + {getRoleBadge(user.role)} + + handleToggleActive(user)} + data-testid={`users_switch_active_${user.id}`} + /> + + + {new Date(user.createdAt).toLocaleDateString("de-DE")} + + + {user.lastLoginAt + ? new Date(user.lastLoginAt).toLocaleDateString("de-DE") + : "-"} + + + + + + + + + + openEditDialog(user)}> + + Bearbeiten + + { + setSelectedUser(user); + setResetPasswordDialogOpen(true); + }} + > + + Passwort zurücksetzen + + { + setSelectedUser(user); + setDeleteDialogOpen(true); + }} + > + + Löschen + + + + + + )) + )} + + + + + + {/* Create User Dialog */} + + + + Neuen Benutzer erstellen + + Füllen Sie die Felder aus um einen neuen Benutzer zu erstellen. + + + + + Email * + setFormData({ ...formData, email: e.target.value })} + placeholder="user@example.com" + data-testid="users_input_email" + /> + + + Passwort * + setFormData({ ...formData, password: e.target.value })} + placeholder="Mindestens 8 Zeichen" + data-testid="users_input_password" + /> + + + Anzeigename + setFormData({ ...formData, displayName: e.target.value })} + placeholder="Max Mustermann" + data-testid="users_input_displayName" + /> + + + Rolle * + + setFormData({ ...formData, role: value }) + } + > + + + + + User + Admin + Demo + + + + + + setCreateDialogOpen(false)}> + Abbrechen + + + Erstellen + + + + + + {/* Edit User Dialog */} + + + + Benutzer bearbeiten + + Bearbeiten Sie die Benutzerinformationen. + + + + + Email + setFormData({ ...formData, email: e.target.value })} + /> + + + Anzeigename + setFormData({ ...formData, displayName: e.target.value })} + /> + + + Rolle + + setFormData({ ...formData, role: value }) + } + > + + + + + User + Admin + Demo + + + + + + setEditDialogOpen(false)}> + Abbrechen + + Speichern + + + + + {/* Delete User Dialog */} + + + + + + Benutzer löschen + + + Sind Sie sicher, dass Sie den Benutzer{" "} + {selectedUser?.email} löschen möchten? Diese Aktion + kann nicht rückgängig gemacht werden. + + + + setDeleteDialogOpen(false)}> + Abbrechen + + + Löschen + + + + + + {/* Reset Password Dialog */} + + + + + + Passwort zurücksetzen + + + Setzen Sie ein neues Passwort für {selectedUser?.email}. + + + + + Neues Passwort + setNewPassword(e.target.value)} + placeholder="Mindestens 8 Zeichen" + data-testid="users_input_newPassword" + /> + + + + setResetPasswordDialogOpen(false)}> + Abbrechen + + + Passwort setzen + + + + + + ); +} diff --git a/src/db/audit-events.ts b/src/db/audit-events.ts index c6fc1ff..440905a 100644 --- a/src/db/audit-events.ts +++ b/src/db/audit-events.ts @@ -20,6 +20,9 @@ export type AuditEventKind = | "brain_search" | "user_login" | "user_logout" + | "user_created" + | "user_deleted" + | "password_reset" | "api_call"; export type AuditSeverity = "info" | "warn" | "error"; diff --git a/src/integrations/linear/client.ts b/src/integrations/linear/client.ts index d30f989..e394ff7 100644 --- a/src/integrations/linear/client.ts +++ b/src/integrations/linear/client.ts @@ -54,9 +54,7 @@ export interface LinearClient { teams?: LinearTeam[]; error?: string; }>; - listWorkflowStates( - teamId: string, - ): Promise<{ + listWorkflowStates(teamId: string): Promise<{ success: boolean; states?: LinearWorkflowState[]; error?: string; @@ -100,9 +98,7 @@ export function createLinearClient(config?: LinearConfig): LinearClient { * @param issue - Issue details * @returns Promise with created issue details */ - async createIssue( - issue: LinearIssue, - ): Promise<{ + async createIssue(issue: LinearIssue): Promise<{ success: boolean; issue?: LinearIssueResult; error?: string; @@ -205,9 +201,7 @@ export function createLinearClient(config?: LinearConfig): LinearClient { * @param teamId - Team ID * @returns Promise with workflow states */ - async listWorkflowStates( - teamId: string, - ): Promise<{ + async listWorkflowStates(teamId: string): Promise<{ success: boolean; states?: LinearWorkflowState[]; error?: string; diff --git a/src/integrations/slack/client.ts b/src/integrations/slack/client.ts index 532c62e..2d16345 100644 --- a/src/integrations/slack/client.ts +++ b/src/integrations/slack/client.ts @@ -32,9 +32,7 @@ export interface SlackChannel { export interface SlackClient { isEnabled(): boolean; - sendMessage( - message: SlackMessage, - ): Promise<{ + sendMessage(message: SlackMessage): Promise<{ success: boolean; message?: SlackMessageResult; error?: string; @@ -82,9 +80,7 @@ export function createSlackClient(config?: SlackConfig): SlackClient { * @param message - Message details including channel and text * @returns Promise with message result */ - async sendMessage( - message: SlackMessage, - ): Promise<{ + async sendMessage(message: SlackMessage): Promise<{ success: boolean; message?: SlackMessageResult; error?: string; diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..60cd262 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,948 @@ +/** + * Comprehensive API Client + * All backend endpoints organized by category + */ + +const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:3001"; + +/** + * Get auth headers from localStorage + */ +function getAuthHeaders(): HeadersInit { + const token = localStorage.getItem("token"); + return { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; +} + +/** + * Generic fetch wrapper with error handling + */ +async function fetchApi( + endpoint: string, + options: RequestInit = {} +): Promise { + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers: { + ...getAuthHeaders(), + ...options.headers, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: "Unknown error" })); + throw new Error(error.error || error.message || `HTTP ${response.status}`); + } + + return response.json(); +} + +// ============================================================================ +// AUTH API +// ============================================================================ +export const authApi = { + login: (email: string, password: string) => + fetchApi<{ + success: boolean; + user: { id: string; email: string; role: string; displayName: string }; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; + }>("/api/auth/login", { + method: "POST", + body: JSON.stringify({ email, password }), + }), + + logout: (refreshToken?: string) => + fetchApi<{ success: boolean }>("/api/auth/logout", { + method: "POST", + body: JSON.stringify({ refreshToken }), + }), + + refresh: (refreshToken: string) => + fetchApi<{ + success: boolean; + tokens: { accessToken: string; refreshToken: string; expiresIn: number }; + }>("/api/auth/refresh", { + method: "POST", + body: JSON.stringify({ refreshToken }), + }), + + verify: () => + fetchApi<{ success: boolean; valid: boolean; user: { userId: string; role: string; email: string } }>( + "/api/auth/verify" + ), + + me: () => + fetchApi<{ + success: boolean; + user: { + id: string; + email: string; + role: string; + displayName: string; + createdAt: string; + lastLoginAt: string; + isActive: boolean; + }; + }>("/api/auth/me"), + + resetPassword: (userId: string, newPassword: string) => + fetchApi<{ success: boolean; message: string }>("/api/auth/reset-password", { + method: "POST", + body: JSON.stringify({ userId, newPassword }), + }), +}; + +// ============================================================================ +// USERS API +// ============================================================================ +export const usersApi = { + list: (params?: { role?: string; isActive?: boolean; limit?: number; offset?: number }) => + fetchApi<{ success: boolean; users: User[]; total: number }>( + `/api/users${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + getById: (id: string) => + fetchApi<{ success: boolean; user: User }>(`/api/users/${id}`), + + create: (data: { email: string; password: string; role: string; displayName?: string }) => + fetchApi<{ success: boolean; user: User }>("/api/users", { + method: "POST", + body: JSON.stringify(data), + }), + + update: (id: string, data: Partial<{ email: string; role: string; displayName: string; isActive: boolean }>) => + fetchApi<{ success: boolean; user: User }>(`/api/users/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }), + + delete: (id: string) => + fetchApi<{ success: boolean }>(`/api/users/${id}`, { method: "DELETE" }), + + stats: () => + fetchApi<{ total: number; active: number; admins: number; users: number; demos: number }>("/api/users/stats"), +}; + +// ============================================================================ +// AGENTS API +// ============================================================================ +export const agentsApi = { + list: () => + fetchApi<{ agents: Agent[] }>("/api/agents"), + + getById: (id: string) => + fetchApi<{ agent: Agent }>(`/api/agents/${id}`), + + start: (id: string) => + fetchApi<{ success: boolean }>(`/api/agents/${id}/start`, { method: "POST" }), + + stop: (id: string) => + fetchApi<{ success: boolean }>(`/api/agents/${id}/stop`, { method: "POST" }), + + restart: (id: string) => + fetchApi<{ success: boolean }>(`/api/agents/${id}/restart`, { method: "POST" }), + + status: (id: string) => + fetchApi<{ status: string }>(`/api/agents/${id}/status`), + + logs: (id: string) => + fetchApi<{ logs: string[] }>(`/api/agents/${id}/logs`), + + createTask: (id: string, task: { description: string; priority?: string }) => + fetchApi<{ success: boolean; taskId: string }>(`/api/agents/${id}/tasks`, { + method: "POST", + body: JSON.stringify(task), + }), +}; + +// ============================================================================ +// TASKS API +// ============================================================================ +export const tasksApi = { + list: (params?: { status?: string; limit?: number }) => + fetchApi<{ tasks: Task[] }>( + `/api/tasks${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + getById: (id: string) => + fetchApi<{ task: Task }>(`/api/tasks/${id}`), + + create: (data: { description: string; priority?: string; agentId?: string }) => + fetchApi<{ success: boolean; task: Task }>("/api/tasks", { + method: "POST", + body: JSON.stringify(data), + }), + + submit: (id: string, result: { output: string; status: string }) => + fetchApi<{ success: boolean }>(`/api/tasks/${id}/submit`, { + method: "POST", + body: JSON.stringify(result), + }), + + statusAll: () => + fetchApi<{ statuses: Record }>("/api/agent-tasks/status/all"), + + workerStart: () => + fetchApi<{ success: boolean }>("/api/agent-tasks/worker/start", { method: "POST" }), + + workerStop: () => + fetchApi<{ success: boolean }>("/api/agent-tasks/worker/stop", { method: "POST" }), +}; + +// ============================================================================ +// CHAT API +// ============================================================================ +export const chatApi = { + send: (message: string, agentName?: string) => + fetchApi<{ response: string; chatId: string }>("/api/chat/send", { + method: "POST", + body: JSON.stringify({ message, agentName }), + }), + + agents: () => + fetchApi<{ agents: ChatAgent[] }>("/api/chat/agents"), + + list: (userId: string) => + fetchApi<{ chats: Chat[] }>(`/api/chat/list/${userId}`), + + delete: (chatId: string) => + fetchApi<{ success: boolean }>(`/api/chat/${chatId}`, { method: "DELETE" }), + + updateTitle: (chatId: string, title: string) => + fetchApi<{ success: boolean }>(`/api/chat/${chatId}/title`, { + method: "PUT", + body: JSON.stringify({ title }), + }), + + threads: () => + fetchApi<{ threads: ChatThread[] }>("/api/chat/threads"), + + createThread: (title: string) => + fetchApi<{ success: boolean; thread: ChatThread }>("/api/chat/threads", { + method: "POST", + body: JSON.stringify({ title }), + }), +}; + +// ============================================================================ +// AUDIT API +// ============================================================================ +export const auditApi = { + list: (params?: { limit?: number; kind?: string; severity?: string }) => + fetchApi<{ events: AuditEvent[] }>( + `/api/audit${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + getById: (id: string) => + fetchApi<{ event: AuditEvent }>(`/api/audit/${id}`), + + stopScoreStats: () => + fetchApi<{ stats: StopScoreStats }>("/api/audit/stats/stop-scores"), + + events: (params?: { limit?: number; kind?: string }) => + fetchApi<{ events: AuditEvent[] }>( + `/api/audit/events${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + eventStats: () => + fetchApi<{ stats: EventStats }>("/api/audit/events/stats"), + + cleanup: (olderThanDays: number) => + fetchApi<{ deleted: number }>("/api/audit/events/cleanup", { + method: "POST", + body: JSON.stringify({ olderThanDays }), + }), +}; + +// ============================================================================ +// OPS API +// ============================================================================ +export const opsApi = { + events: (params?: { limit?: number; kind?: string }) => + fetchApi<{ events: OpsEvent[] }>( + `/api/ops/events${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + tasksHistory: (params?: { limit?: number; status?: string }) => + fetchApi<{ tasks: Task[] }>( + `/api/ops/tasks/history${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + stats: () => + fetchApi("/api/ops/stats"), +}; + +// ============================================================================ +// BRAIN API +// ============================================================================ +export const brainApi = { + ingestText: (data: { title: string; content: string; tags?: string[] }) => + fetchApi<{ success: boolean; docId: string }>("/api/brain/ingest/text", { + method: "POST", + body: JSON.stringify(data), + }), + + ingestUrl: (url: string) => + fetchApi<{ success: boolean; docId: string }>("/api/brain/ingest/url", { + method: "POST", + body: JSON.stringify({ url }), + }), + + search: (query: string, limit?: number) => + fetchApi<{ results: BrainSearchResult[] }>("/api/brain/search", { + method: "POST", + body: JSON.stringify({ query, limit }), + }), + + docs: () => + fetchApi<{ docs: BrainDoc[] }>("/api/brain/docs"), + + getDoc: (docId: string) => + fetchApi<{ doc: BrainDoc }>(`/api/brain/docs/${docId}`), + + updateDoc: (docId: string, data: Partial) => + fetchApi<{ success: boolean }>(`/api/brain/docs/${docId}`, { + method: "PATCH", + body: JSON.stringify(data), + }), + + deleteDoc: (docId: string) => + fetchApi<{ success: boolean }>(`/api/brain/docs/${docId}`, { method: "DELETE" }), + + similarDocs: (docId: string) => + fetchApi<{ docs: BrainDoc[] }>(`/api/brain/docs/${docId}/similar`), + + stats: () => + fetchApi("/api/brain/stats"), + + proxyHealth: () => + fetchApi<{ status: string }>("/api/brain/proxy/health"), +}; + +// ============================================================================ +// MEMORY API +// ============================================================================ +export const memoryApi = { + chats: (userId: string) => + fetchApi<{ chats: MemoryChat[] }>(`/api/memory/chats/${userId}`), + + createChat: (data: { userId: string; title: string }) => + fetchApi<{ success: boolean; chat: MemoryChat }>("/api/memory/chats", { + method: "POST", + body: JSON.stringify(data), + }), + + chatDetails: (chatId: string) => + fetchApi<{ chat: MemoryChat }>(`/api/memory/chats/${chatId}/details`), + + chatMessages: (chatId: string) => + fetchApi<{ messages: MemoryMessage[] }>(`/api/memory/chats/${chatId}/messages`), + + addMessage: (chatId: string, message: { role: string; content: string }) => + fetchApi<{ success: boolean }>(`/api/memory/chats/${chatId}/messages`, { + method: "POST", + body: JSON.stringify(message), + }), + + search: (query: string, userId: string) => + fetchApi<{ results: MemorySearchResult[] }>("/api/memory/search", { + method: "POST", + body: JSON.stringify({ query, userId }), + }), + + semanticSearch: (query: string) => + fetchApi<{ results: MemorySearchResult[] }>("/api/memory/semantic/search", { + method: "POST", + body: JSON.stringify({ query }), + }), + + trending: (userId: string) => + fetchApi<{ topics: string[] }>(`/api/memory/trending/${userId}`), + + stats: (userId: string) => + fetchApi(`/api/memory/stats/${userId}`), + + exportChat: (chatId: string) => + fetchApi<{ export: string }>(`/api/memory/chats/${chatId}/export`), +}; + +// ============================================================================ +// SETTINGS API +// ============================================================================ +export const settingsApi = { + getUser: (userId: string) => + fetchApi<{ settings: UserSettings }>(`/api/settings/user/${userId}`), + + updateUser: (userId: string, settings: Partial) => + fetchApi<{ success: boolean }>(`/api/settings/user/${userId}`, { + method: "PUT", + body: JSON.stringify(settings), + }), + + getSystem: () => + fetchApi<{ settings: SystemSettings }>("/api/settings/system"), + + updateSystem: (settings: Partial) => + fetchApi<{ success: boolean }>("/api/settings/system", { + method: "PUT", + body: JSON.stringify(settings), + }), + + getSystemKey: (key: string) => + fetchApi<{ value: unknown }>(`/api/settings/system/${key}`), + + history: (userId: string) => + fetchApi<{ history: SettingsHistory[] }>(`/api/settings/history/user/${userId}`), +}; + +// ============================================================================ +// ENFORCEMENT API +// ============================================================================ +export const enforcementApi = { + blocked: () => + fetchApi<{ tasks: BlockedTask[] }>("/api/enforcement/blocked"), + + getBlocked: (taskId: string) => + fetchApi<{ task: BlockedTask }>(`/api/enforcement/blocked/${taskId}`), + + approve: (taskId: string, reason?: string) => + fetchApi<{ success: boolean }>(`/api/enforcement/approve/${taskId}`, { + method: "POST", + body: JSON.stringify({ reason }), + }), + + reject: (taskId: string, reason: string) => + fetchApi<{ success: boolean }>(`/api/enforcement/reject/${taskId}`, { + method: "POST", + body: JSON.stringify({ reason }), + }), +}; + +// ============================================================================ +// GITHUB INTEGRATION API +// ============================================================================ +export const githubApi = { + status: () => + fetchApi<{ connected: boolean; user?: string }>("/api/github/status"), + + repos: () => + fetchApi<{ repos: GithubRepo[] }>("/api/github/repos"), + + getRepo: (owner: string, repo: string) => + fetchApi<{ repo: GithubRepo }>(`/api/github/repos/${owner}/${repo}`), + + issues: (params?: { state?: string; labels?: string }) => + fetchApi<{ issues: GithubIssue[] }>( + `/api/github/issues${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + createIssue: (data: { title: string; body: string; repo: string }) => + fetchApi<{ success: boolean; issue: GithubIssue }>("/api/github/issues", { + method: "POST", + body: JSON.stringify(data), + }), + + pulls: () => + fetchApi<{ pulls: GithubPR[] }>("/api/github/pulls"), + + createPull: (data: { title: string; body: string; head: string; base: string; repo: string }) => + fetchApi<{ success: boolean; pull: GithubPR }>("/api/github/pulls", { + method: "POST", + body: JSON.stringify(data), + }), + + comments: (issueId: string) => + fetchApi<{ comments: GithubComment[] }>(`/api/github/comments?issueId=${issueId}`), + + addComment: (issueId: string, body: string) => + fetchApi<{ success: boolean }>("/api/github/comments", { + method: "POST", + body: JSON.stringify({ issueId, body }), + }), +}; + +// ============================================================================ +// LINEAR INTEGRATION API +// ============================================================================ +export const linearApi = { + status: () => + fetchApi<{ connected: boolean }>("/api/linear/status"), + + teams: () => + fetchApi<{ teams: LinearTeam[] }>("/api/linear/teams"), + + issues: (params?: { teamId?: string; state?: string }) => + fetchApi<{ issues: LinearIssue[] }>( + `/api/linear/issues${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + createIssue: (data: { title: string; description: string; teamId: string }) => + fetchApi<{ success: boolean; issue: LinearIssue }>("/api/linear/issues", { + method: "POST", + body: JSON.stringify(data), + }), + + updateIssue: (issueId: string, data: Partial) => + fetchApi<{ success: boolean }>(`/api/linear/issues/${issueId}`, { + method: "PATCH", + body: JSON.stringify(data), + }), + + projects: () => + fetchApi<{ projects: LinearProject[] }>("/api/linear/projects"), + + states: () => + fetchApi<{ states: LinearState[] }>("/api/linear/states"), + + labels: () => + fetchApi<{ labels: LinearLabel[] }>("/api/linear/labels"), + + users: () => + fetchApi<{ users: LinearUser[] }>("/api/linear/users"), +}; + +// ============================================================================ +// WEBHOOKS API +// ============================================================================ +export const webhooksApi = { + list: () => + fetchApi<{ webhooks: Webhook[] }>("/api/webhooks"), + + getById: (id: string) => + fetchApi<{ webhook: Webhook }>(`/api/webhooks/${id}`), + + create: (data: { url: string; events: string[]; secret?: string }) => + fetchApi<{ success: boolean; webhook: Webhook }>("/api/webhooks", { + method: "POST", + body: JSON.stringify(data), + }), + + update: (id: string, data: Partial) => + fetchApi<{ success: boolean }>(`/api/webhooks/${id}`, { + method: "PATCH", + body: JSON.stringify(data), + }), + + delete: (id: string) => + fetchApi<{ success: boolean }>(`/api/webhooks/${id}`, { method: "DELETE" }), + + deliveries: (id: string) => + fetchApi<{ deliveries: WebhookDelivery[] }>(`/api/webhooks/${id}/deliveries`), + + allDeliveries: () => + fetchApi<{ deliveries: WebhookDelivery[] }>("/api/webhooks/deliveries/all"), + + test: (id: string) => + fetchApi<{ success: boolean }>("/api/webhooks/test", { + method: "POST", + body: JSON.stringify({ webhookId: id }), + }), +}; + +// ============================================================================ +// MODULES API +// ============================================================================ +export const modulesApi = { + list: () => + fetchApi<{ modules: Module[] }>("/api/modules"), + + getById: (id: string) => + fetchApi<{ module: Module }>(`/api/modules/${id}`), + + byCategory: (category: string) => + fetchApi<{ modules: Module[] }>(`/api/modules/category/${category}`), + + byStatus: (status: string) => + fetchApi<{ modules: Module[] }>(`/api/modules/status/${status}`), + + report: () => + fetchApi<{ report: ModuleReport }>("/api/modules/report"), + + categories: () => + fetchApi<{ categories: string[] }>("/api/modules/categories"), +}; + +// ============================================================================ +// BILLING API +// ============================================================================ +export const billingApi = { + usage: (userId: string) => + fetchApi<{ usage: BillingUsage }>(`/api/billing/usage/${userId}`), + + costs: (params?: { from?: string; to?: string }) => + fetchApi<{ costs: BillingCost[] }>( + `/api/billing/costs${params ? "?" + new URLSearchParams(params as Record).toString() : ""}` + ), + + pricing: () => + fetchApi<{ pricing: PricingTier[] }>("/api/billing/pricing"), +}; + +// ============================================================================ +// DEMO API +// ============================================================================ +export const demoApi = { + createInvite: (data: { email: string; expiresInDays?: number }) => + fetchApi<{ success: boolean; code: string }>("/api/demo/invites", { + method: "POST", + body: JSON.stringify(data), + }), + + getInvite: (code: string) => + fetchApi<{ invite: DemoInvite }>(`/api/demo/invites/${code}`), + + redeem: (code: string, userData: { email: string; name: string }) => + fetchApi<{ success: boolean; user: User }>("/api/demo/redeem", { + method: "POST", + body: JSON.stringify({ code, ...userData }), + }), + + getUser: (userId: string) => + fetchApi<{ user: User }>(`/api/demo/users/${userId}`), + + stats: () => + fetchApi<{ stats: DemoStats }>("/api/demo/stats"), +}; + +// ============================================================================ +// HEALTH API +// ============================================================================ +export const healthApi = { + check: () => + fetchApi<{ status: string; time: string }>("/api/health"), + + ready: () => + fetchApi<{ ready: boolean }>("/api/health/ready"), + + live: () => + fetchApi<{ live: boolean }>("/api/health/live"), +}; + +// ============================================================================ +// TYPE DEFINITIONS +// ============================================================================ +export interface User { + id: string; + email: string; + role: "admin" | "user" | "demo"; + displayName?: string; + createdAt: string; + updatedAt?: string; + lastLoginAt?: string; + isActive: boolean; +} + +export interface Agent { + id: string; + name: string; + description: string; + status: "active" | "paused" | "stopped"; + language: string; + lastRun: string; + executionCount: number; +} + +export interface Task { + id: string; + description: string; + status: "pending" | "running" | "completed" | "failed"; + priority: string; + agentId?: string; + createdAt: string; + completedAt?: string; + output?: string; +} + +export interface ChatAgent { + name: string; + description: string; + model: string; +} + +export interface Chat { + id: string; + title: string; + createdAt: string; + messageCount: number; +} + +export interface ChatThread { + id: string; + title: string; + createdAt: string; +} + +export interface AuditEvent { + id: string; + ts: string; + kind: string; + agentId?: string; + taskId?: string; + userId?: string; + severity: "info" | "warn" | "error"; + message: string; + meta?: Record; +} + +export interface StopScoreStats { + total: number; + avgScore: number; + distribution: Record; +} + +export interface EventStats { + total: number; + byKind: Record; + bySeverity: Record; +} + +export interface OpsEvent { + id: string; + kind: string; + message: string; + timestamp: string; + severity: string; +} + +export interface OpsStats { + uptime: number; + activeAgents: number; + tasksToday: number; + errorsToday: number; +} + +export interface BrainDoc { + id: string; + title: string; + content: string; + tags?: string[]; + createdAt: string; +} + +export interface BrainSearchResult { + id: string; + title: string; + content: string; + score: number; +} + +export interface BrainStats { + totalDocs: number; + totalChunks: number; + totalEmbeddings: number; +} + +export interface MemoryChat { + id: string; + userId: string; + title: string; + createdAt: string; + messageCount: number; +} + +export interface MemoryMessage { + id: string; + role: "user" | "assistant"; + content: string; + createdAt: string; +} + +export interface MemorySearchResult { + id: string; + content: string; + score: number; + chatId: string; +} + +export interface MemoryStats { + totalChats: number; + totalMessages: number; + avgMessagesPerChat: number; +} + +export interface UserSettings { + theme: "light" | "dark" | "system"; + notifications: boolean; + language: string; +} + +export interface SystemSettings { + maintenanceMode: boolean; + maxAgents: number; + defaultModel: string; +} + +export interface SettingsHistory { + id: string; + key: string; + oldValue: unknown; + newValue: unknown; + changedAt: string; + changedBy: string; +} + +export interface BlockedTask { + id: string; + taskId: string; + reason: string; + stopScore: number; + createdAt: string; +} + +export interface GithubRepo { + id: number; + name: string; + fullName: string; + description: string; + url: string; + stars: number; +} + +export interface GithubIssue { + id: number; + number: number; + title: string; + body: string; + state: string; + createdAt: string; +} + +export interface GithubPR { + id: number; + number: number; + title: string; + state: string; + head: string; + base: string; +} + +export interface GithubComment { + id: number; + body: string; + createdAt: string; + user: string; +} + +export interface LinearTeam { + id: string; + name: string; + key: string; +} + +export interface LinearIssue { + id: string; + title: string; + description: string; + state: string; + priority: number; +} + +export interface LinearProject { + id: string; + name: string; + state: string; +} + +export interface LinearState { + id: string; + name: string; + color: string; +} + +export interface LinearLabel { + id: string; + name: string; + color: string; +} + +export interface LinearUser { + id: string; + name: string; + email: string; +} + +export interface Webhook { + id: string; + url: string; + events: string[]; + secret?: string; + active: boolean; + createdAt: string; +} + +export interface WebhookDelivery { + id: string; + webhookId: string; + event: string; + status: number; + deliveredAt: string; +} + +export interface Module { + id: string; + name: string; + category: string; + status: "active" | "inactive" | "error"; + version: string; +} + +export interface ModuleReport { + total: number; + active: number; + inactive: number; + errors: number; +} + +export interface BillingUsage { + tokens: number; + requests: number; + cost: number; +} + +export interface BillingCost { + date: string; + amount: number; + breakdown: Record; +} + +export interface PricingTier { + name: string; + price: number; + features: string[]; +} + +export interface DemoInvite { + code: string; + email: string; + expiresAt: string; + redeemed: boolean; +} + +export interface DemoStats { + totalInvites: number; + redeemed: number; + active: number; +} + +export default { + auth: authApi, + users: usersApi, + agents: agentsApi, + tasks: tasksApi, + chat: chatApi, + audit: auditApi, + ops: opsApi, + brain: brainApi, + memory: memoryApi, + settings: settingsApi, + enforcement: enforcementApi, + github: githubApi, + linear: linearApi, + webhooks: webhooksApi, + modules: modulesApi, + billing: billingApi, + demo: demoApi, + health: healthApi, +};
+ System-Events, Audit-Logs und Operations-Statistiken +
Keine blockierten Tasks
Keine Daten
ID
{selectedEvent.id}
Zeit
{new Date(selectedEvent.ts).toLocaleString("de-DE")}
Art
Severity
Nachricht
{selectedEvent.message}
Metadaten
+ {JSON.stringify(selectedEvent.meta, null, 2)} +
+ Influencer-Agenten können eigenständig Content erstellen und veröffentlichen. +
+ Agent erstellt und veröffentlicht Content eigenständig +
+ GitHub, Linear und Webhook-Verbindungen verwalten +
+ GitHub ist nicht verbunden. Konfigurieren Sie GITHUB_TOKEN in den Umgebungsvariablen. +
{repo.name}
+ {repo.description || "Keine Beschreibung"} +
+ Linear ist nicht verbunden. Konfigurieren Sie LINEAR_API_KEY in den Umgebungsvariablen. +
+ Keine Webhooks konfiguriert +
+ Benutzer erstellen, bearbeiten und verwalten +