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/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/AuditPage.tsx b/src/components/AuditPage.tsx new file mode 100644 index 0000000..e86c178 --- /dev/null +++ b/src/components/AuditPage.tsx @@ -0,0 +1,612 @@ +/** + * 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/IntegrationsPage.tsx b/src/components/IntegrationsPage.tsx new file mode 100644 index 0000000..8f23e25 --- /dev/null +++ b/src/components/IntegrationsPage.tsx @@ -0,0 +1,778 @@ +/** + * Integrations Page + * GitHub, Linear, Webhooks management + */ + +import { useState, useEffect } from "react"; +import { + githubApi, + linearApi, + webhooksApi, + type GithubRepo, + type GithubIssue, + type LinearTeam, + type LinearIssue, + type Webhook, +} from "../lib/api"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { Textarea } from "./ui/textarea"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "./ui/table"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; +import { Switch } from "./ui/switch"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "./ui/dialog"; +import { + Github, + Webhook as WebhookIcon, + ExternalLink, + Plus, + Trash2, + RefreshCw, + CheckCircle, + XCircle, + Send, + Link2, +} from "lucide-react"; +import { toast } from "sonner"; + +// Linear icon component +function LinearIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +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 +

+
+ +
+ + + + + + 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}) +

+ +
+ + + + # + 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}) +

+ +
+ + + + 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 + +
+ +
+
+ + {webhooks.length === 0 ? ( +
+ +

+ Keine Webhooks konfiguriert +

+ +
+ ) : ( + + + + 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", + )} + + +
+ + +
+
+
+ ))} +
+
+ )} +
+
+
+
+ + {/* Create Webhook Dialog */} + + + + Neuen Webhook erstellen + + Konfigurieren Sie einen Webhook um bei Events benachrichtigt zu + werden. + + +
+
+ + + setWebhookForm({ ...webhookForm, url: e.target.value }) + } + placeholder="https://example.com/webhook" + /> +
+
+ +
+ {availableEvents.map((event) => ( + { + const events = webhookForm.events.includes(event) + ? webhookForm.events.filter((e) => e !== event) + : [...webhookForm.events, event]; + setWebhookForm({ ...webhookForm, events }); + }} + > + {event} + + ))} +
+
+
+ + + setWebhookForm({ ...webhookForm, secret: e.target.value }) + } + placeholder="Webhook-Signatur-Secret" + /> +
+
+ + + + +
+
+ + {/* Create Issue Dialog */} + + + + + {issueType === "github" + ? "GitHub Issue erstellen" + : "Linear Issue erstellen"} + + +
+
+ + + setIssueForm({ ...issueForm, title: e.target.value }) + } + placeholder="Issue Titel" + /> +
+
+ +