Friizy commited on
Commit
46d38bf
·
2 Parent(s): 88890c9380048c

Merge upstream main, preferring our changes

Browse files
feature-flag-agent-env/agents/rl_agent.py CHANGED
@@ -7,32 +7,41 @@ import numpy as np
7
  from feature_flag_env.models import FeatureFlagAction, FeatureFlagObservation
8
  from agents.replay_buffer import ReplayBuffer
9
 
 
10
  try:
11
  import torch
12
  import torch.nn as nn
13
  import torch.optim as optim
14
  from torch.nn.utils import clip_grad_norm_
15
- except ImportError as exc:
16
- raise ImportError(
17
- "PyTorch is required for RLAgent. Install with: pip install torch"
18
- ) from exc
19
-
20
-
21
- class DQN(nn.Module):
22
- def __init__(self, state_dim: int, action_dim: int):
23
- super().__init__()
24
- self.net = nn.Sequential(
25
- nn.Linear(state_dim, 128),
26
- nn.ReLU(),
27
- nn.Linear(128, 64),
28
- nn.ReLU(),
29
- nn.Linear(64, 32),
30
- nn.ReLU(),
31
- nn.Linear(32, action_dim),
32
- )
 
 
 
33
 
34
- def forward(self, x):
35
- return self.net(x)
 
 
 
 
 
36
 
37
 
38
  class RLAgent:
@@ -139,14 +148,22 @@ class RLAgent:
139
  self.action_mask_events = 0
140
  self.action_safety_overrides = 0
141
 
142
- self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
143
- self.policy_net = DQN(self.state_dim, self.action_dim).to(self.device)
144
- self.target_net = DQN(self.state_dim, self.action_dim).to(self.device)
145
- self.target_net.load_state_dict(self.policy_net.state_dict())
146
- self.target_net.eval()
 
 
 
 
 
 
 
 
 
 
147
 
148
- self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
149
- self.loss_fn = nn.MSELoss()
150
  self.replay_buffer = ReplayBuffer(capacity=buffer_capacity)
151
  self.external_transition_mode = False
152
 
@@ -368,6 +385,9 @@ class RLAgent:
368
  if self.training and random.random() < self.epsilon:
369
  return int(random.choice(allowed_actions))
370
 
 
 
 
371
  with torch.no_grad():
372
  state_tensor = torch.from_numpy(state).float().unsqueeze(0).to(self.device)
373
  q_values = self.policy_net(state_tensor)
@@ -413,6 +433,8 @@ class RLAgent:
413
  )
414
 
415
  def _optimize_model(self):
 
 
416
  if len(self.replay_buffer) < self.batch_size:
417
  return None
418
 
@@ -557,6 +579,8 @@ class RLAgent:
557
  self.last_action = None
558
 
559
  def save_model(self, model_path: Optional[str] = None):
 
 
560
  path = model_path or self.model_path
561
  os.makedirs(os.path.dirname(path), exist_ok=True)
562
  torch.save(
@@ -570,6 +594,8 @@ class RLAgent:
570
  )
571
 
572
  def load_model(self, model_path: Optional[str] = None):
 
 
573
  path = model_path or self.model_path
574
  checkpoint = torch.load(path, map_location=self.device)
575
 
 
7
  from feature_flag_env.models import FeatureFlagAction, FeatureFlagObservation
8
  from agents.replay_buffer import ReplayBuffer
9
 
10
+ TORCH_AVAILABLE = True
11
  try:
12
  import torch
13
  import torch.nn as nn
14
  import torch.optim as optim
15
  from torch.nn.utils import clip_grad_norm_
16
+ except ImportError:
17
+ TORCH_AVAILABLE = False
18
+ torch = None
19
+ nn = None
20
+ optim = None
21
+ clip_grad_norm_ = None
22
+
23
+
24
+ if TORCH_AVAILABLE:
25
+ class DQN(nn.Module):
26
+ def __init__(self, state_dim: int, action_dim: int):
27
+ super().__init__()
28
+ self.net = nn.Sequential(
29
+ nn.Linear(state_dim, 128),
30
+ nn.ReLU(),
31
+ nn.Linear(128, 64),
32
+ nn.ReLU(),
33
+ nn.Linear(64, 32),
34
+ nn.ReLU(),
35
+ nn.Linear(32, action_dim),
36
+ )
37
 
38
+ def forward(self, x):
39
+ return self.net(x)
40
+ else:
41
+ class DQN:
42
+ def __init__(self, state_dim: int, action_dim: int):
43
+ self.state_dim = state_dim
44
+ self.action_dim = action_dim
45
 
46
 
47
  class RLAgent:
 
148
  self.action_mask_events = 0
149
  self.action_safety_overrides = 0
150
 
151
+ self.torch_available = TORCH_AVAILABLE
152
+ if self.torch_available:
153
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
154
+ self.policy_net = DQN(self.state_dim, self.action_dim).to(self.device)
155
+ self.target_net = DQN(self.state_dim, self.action_dim).to(self.device)
156
+ self.target_net.load_state_dict(self.policy_net.state_dict())
157
+ self.target_net.eval()
158
+ self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
159
+ self.loss_fn = nn.MSELoss()
160
+ else:
161
+ self.device = "cpu"
162
+ self.policy_net = None
163
+ self.target_net = None
164
+ self.optimizer = None
165
+ self.loss_fn = None
166
 
 
 
167
  self.replay_buffer = ReplayBuffer(capacity=buffer_capacity)
168
  self.external_transition_mode = False
169
 
 
385
  if self.training and random.random() < self.epsilon:
386
  return int(random.choice(allowed_actions))
387
 
388
+ if not self.torch_available:
389
+ return int(random.choice(allowed_actions))
390
+
391
  with torch.no_grad():
392
  state_tensor = torch.from_numpy(state).float().unsqueeze(0).to(self.device)
393
  q_values = self.policy_net(state_tensor)
 
433
  )
434
 
435
  def _optimize_model(self):
436
+ if not self.torch_available:
437
+ return None
438
  if len(self.replay_buffer) < self.batch_size:
439
  return None
440
 
 
579
  self.last_action = None
580
 
581
  def save_model(self, model_path: Optional[str] = None):
582
+ if not self.torch_available:
583
+ return
584
  path = model_path or self.model_path
585
  os.makedirs(os.path.dirname(path), exist_ok=True)
586
  torch.save(
 
594
  )
595
 
596
  def load_model(self, model_path: Optional[str] = None):
597
+ if not self.torch_available:
598
+ return
599
  path = model_path or self.model_path
600
  checkpoint = torch.load(path, map_location=self.device)
601
 
feature-flag-agent-env/feature_flag_env/tools/connectors.py CHANGED
@@ -8,7 +8,6 @@ by higher-level lifecycle handlers (Tools) to enforce backward compatibility and
8
 
9
  from abc import ABC, abstractmethod
10
  from typing import Dict, Any, Optional
11
- import time
12
 
13
 
14
  class Connector(ABC):
 
8
 
9
  from abc import ABC, abstractmethod
10
  from typing import Dict, Any, Optional
 
11
 
12
 
13
  class Connector(ABC):
feature-flag-agent-env/pyproject.toml CHANGED
@@ -18,7 +18,6 @@ dependencies = [
18
 
19
  # 🧮 Math & Simulation
20
  "numpy>=1.24.0",
21
- "torch>=2.0.0",
22
 
23
  # 🤖 LLM Integration (OpenAI-compatible client)
24
  "groq>=0.4.0",
 
18
 
19
  # 🧮 Math & Simulation
20
  "numpy>=1.24.0",
 
21
 
22
  # 🤖 LLM Integration (OpenAI-compatible client)
23
  "groq>=0.4.0",
feature-flag-agent-env/requirements.txt CHANGED
@@ -21,7 +21,6 @@ prometheus-client>=0.19.0
21
 
22
  # Math & Simulation
23
  numpy>=1.24.0
24
- torch>=2.0.0
25
 
26
  # LLM Integration
27
  groq>=0.4.0
 
21
 
22
  # Math & Simulation
23
  numpy>=1.24.0
 
24
 
25
  # LLM Integration
26
  groq>=0.4.0
frontend/app/ai-decisions/page.tsx CHANGED
@@ -193,6 +193,11 @@ export default function AIDecisionsPage() {
193
  <div className="space-y-0">
194
  {recentDecisions.length > 0 ? (
195
  recentDecisions.map((step, i) => (
 
 
 
 
 
196
  <div key={i} className="flex gap-4 border-b border-border/30 last:border-0 py-4 group">
197
  <div className="flex flex-col items-center">
198
  <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
@@ -240,6 +245,8 @@ export default function AIDecisionsPage() {
240
  </div>
241
  </div>
242
  </div>
 
 
243
  ))
244
  ) : (
245
  <div className="py-20 text-center text-muted-foreground flex flex-col items-center gap-4">
 
193
  <div className="space-y-0">
194
  {recentDecisions.length > 0 ? (
195
  recentDecisions.map((step, i) => (
196
+ (() => {
197
+ const actionType = step.action?.action_type ?? "MAINTAIN";
198
+ const targetPercentage = step.action?.target_percentage ?? 0;
199
+ const reason = step.action?.reason ?? "No reasoning provided.";
200
+ return (
201
  <div key={i} className="flex gap-4 border-b border-border/30 last:border-0 py-4 group">
202
  <div className="flex flex-col items-center">
203
  <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
 
245
  </div>
246
  </div>
247
  </div>
248
+ );
249
+ })()
250
  ))
251
  ) : (
252
  <div className="py-20 text-center text-muted-foreground flex flex-col items-center gap-4">
frontend/app/integrations/page.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ MessageSquare,
8
+ GitGraph,
9
+ ExternalLink,
10
+ RefreshCw,
11
+ Bell
12
+ } from "lucide-react";
13
+ import { useEffect, useState } from "react";
14
+ import { api } from "@/lib/api";
15
+
16
+ const IntegrationsPage = () => {
17
+ const [loading, setLoading] = useState(false);
18
+ const [lastSync, setLastSync] = useState("Never");
19
+ const [isHealthy, setIsHealthy] = useState(false);
20
+
21
+ const syncIntegrations = async () => {
22
+ setLoading(true);
23
+ try {
24
+ const [health] = await Promise.all([
25
+ api.getHealth(),
26
+ api.getDashboard(),
27
+ ]);
28
+ setIsHealthy(Boolean(health?.environment_ready));
29
+ setLastSync(new Date().toLocaleTimeString());
30
+ } catch (error) {
31
+ console.error("Failed to sync integration status:", error);
32
+ setIsHealthy(false);
33
+ setLastSync("Sync failed");
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ useEffect(() => {
40
+ const initialSync = setTimeout(() => {
41
+ void syncIntegrations();
42
+ }, 0);
43
+ return () => clearTimeout(initialSync);
44
+ }, []);
45
+
46
+ const integrations = [
47
+ {
48
+ id: "slack",
49
+ name: "Slack",
50
+ description: "Automated rollout notifications and approval workflows.",
51
+ icon: MessageSquare,
52
+ status: isHealthy ? "connected" : "disconnected",
53
+ details: isHealthy ? "Alerts can be routed through backend automation" : "Backend unavailable",
54
+ lastSync
55
+ },
56
+ {
57
+ id: "github",
58
+ name: "GitHub",
59
+ description: "Trigger rollouts from PRs and sync deployment status.",
60
+ icon: GitGraph,
61
+ status: "disconnected",
62
+ details: "Requires OAuth",
63
+ lastSync
64
+ }
65
+ ];
66
+
67
+ return (
68
+ <div className="flex-1 space-y-6 p-8">
69
+ <div className="flex items-center justify-between">
70
+ <div>
71
+ <h1 className="text-3xl font-bold tracking-tight">Integrations</h1>
72
+ <p className="text-muted-foreground mt-1">Manage external connections and observability sync.</p>
73
+ </div>
74
+ <Button variant="outline" size="sm" className="rounded-full" onClick={syncIntegrations} disabled={loading}>
75
+ <RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
76
+ {loading ? "Syncing..." : "Sync All"}
77
+ </Button>
78
+ </div>
79
+
80
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
81
+ {integrations.map((item) => (
82
+ <Card key={item.id} className="border-border/50 bg-card/50">
83
+ <CardHeader className="pb-3">
84
+ <div className="flex items-center justify-between">
85
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted/50">
86
+ <item.icon className="h-6 w-6 text-primary" />
87
+ </div>
88
+ <Badge
89
+ variant={item.status === "connected" ? "default" : "secondary"}
90
+ className={item.status === "connected" ? "bg-green-500/10 text-green-600 hover:bg-green-500/10" : ""}
91
+ >
92
+ {item.status === "connected" ? "Connected" : "Disconnected"}
93
+ </Badge>
94
+ </div>
95
+ <CardTitle className="mt-4">{item.name}</CardTitle>
96
+ <CardDescription>{item.description}</CardDescription>
97
+ </CardHeader>
98
+ <CardContent>
99
+ <div className="space-y-4">
100
+ <div className="flex items-center justify-between text-sm">
101
+ <span className="text-muted-foreground">Status</span>
102
+ <span className="font-medium">{item.details}</span>
103
+ </div>
104
+ <div className="flex items-center justify-between text-sm">
105
+ <span className="text-muted-foreground">Last Sync</span>
106
+ <span className="text-xs">{item.lastSync}</span>
107
+ </div>
108
+ <div className="pt-4 flex gap-2">
109
+ <Button variant="outline" className="w-full text-xs h-8 rounded-full">
110
+ Configure
111
+ </Button>
112
+ <Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
113
+ <ExternalLink className="h-4 w-4" />
114
+ </Button>
115
+ </div>
116
+ </div>
117
+ </CardContent>
118
+ </Card>
119
+ ))}
120
+ </div>
121
+
122
+ <Card className="border-primary/20 bg-primary/5">
123
+ <CardHeader>
124
+ <CardTitle className="flex items-center gap-2 text-primary">
125
+ <Bell className="h-5 w-5" />
126
+ Webhook Endpoints
127
+ </CardTitle>
128
+ <CardDescription>Receive automated alerts from your own infrastructure.</CardDescription>
129
+ </CardHeader>
130
+ <CardContent>
131
+ <div className="rounded-lg border bg-background/50 p-4 font-mono text-xs">
132
+ GET {(api.getApiBaseUrl() || "http://localhost:8000")}/monitoring/alerts
133
+ </div>
134
+ <p className="text-[10px] text-muted-foreground mt-4">
135
+ Uses live backend monitoring routes for alert ingestion and observability polling.
136
+ </p>
137
+ </CardContent>
138
+ </Card>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default IntegrationsPage;
frontend/app/layout.tsx CHANGED
@@ -30,8 +30,7 @@ export default function RootLayout({
30
  <body className="min-h-full flex flex-col font-sans">
31
  <ThemeProvider
32
  attribute="class"
33
- defaultTheme="system"
34
- enableSystem
35
  disableTransitionOnChange
36
  >
37
  <EnvProvider>
 
30
  <body className="min-h-full flex flex-col font-sans">
31
  <ThemeProvider
32
  attribute="class"
33
+ defaultTheme="dark"
 
34
  disableTransitionOnChange
35
  >
36
  <EnvProvider>
frontend/app/page.tsx CHANGED
@@ -16,7 +16,6 @@ import {
16
  import {
17
  Area,
18
  AreaChart,
19
- ResponsiveContainer,
20
  Tooltip,
21
  XAxis,
22
  YAxis,
@@ -27,6 +26,7 @@ import {
27
  } from "recharts";
28
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
29
  import { Badge } from "@/components/ui/badge";
 
30
  import { Progress } from "@/components/ui/progress";
31
  import { useMemo } from "react";
32
  import { Observation } from "@/lib/api";
@@ -38,6 +38,14 @@ const Dashboard = () => {
38
  const { dashboard: data, state, connectionState, connectionText } = useEnv();
39
  const loading = useMemo(() => connectionState === "checking" && !data && !state, [connectionState, data, state]);
40
 
 
 
 
 
 
 
 
 
41
  const lastObs: Observation | undefined = state?.history?.[state.history.length - 1]?.observation;
42
 
43
  const healthScore = lastObs?.system_health_score ?? data?.summary?.health_score ?? 0;
@@ -79,23 +87,42 @@ const Dashboard = () => {
79
  <h1 className="text-3xl font-bold tracking-tight">Enterprise Overview</h1>
80
  <p className="text-muted-foreground mt-1">Real-time autonomous deployment monitoring.</p>
81
  </div>
82
- <Badge
83
- variant="outline"
84
- className={cn(
85
- "px-4 py-1.5 rounded-full border-2",
86
- connectionState === "connected"
87
- ? "border-green-500/30 text-green-600 bg-green-50"
88
- : connectionState === "disconnected"
89
- ? "border-destructive/30 text-destructive bg-destructive/5"
90
- : ""
91
- )}
92
- >
93
- <div className={cn(
94
- "w-2 h-2 rounded-full mr-2 animate-pulse",
95
- connectionState === "connected" ? "bg-green-500" : "bg-destructive"
96
- )} />
97
- {loading ? "Syncing..." : connectionText}
98
- </Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
 
101
  {/* Primary Metrics */}
@@ -204,11 +231,11 @@ const Dashboard = () => {
204
 
205
  <div className="grid gap-6 md:grid-cols-7">
206
  {/* Main Traffic Chart */}
207
- <Card className="md:col-span-4 border-border/50 bg-card/50 shadow-sm">
208
  <CardHeader>
209
  <CardTitle className="flex items-center justify-between">
210
  Evaluation Traffic
211
- <Badge variant="outline" className="font-normal text-xs">
212
  Live Stream
213
  </Badge>
214
  </CardTitle>
@@ -219,21 +246,37 @@ const Dashboard = () => {
219
  <AreaChart data={state?.history?.map(h => ({
220
  time: h.observation?.time_step,
221
  rollout: h.observation?.current_rollout_percentage,
222
- error: (h.observation?.error_rate ?? 0) * 10000 // scaling for visibility
223
- })) || []}>
224
- <defs>
225
- <linearGradient id="colorRollout" x1="0" y1="0" x2="0" y2="1">
226
- <stop offset="5%" stopColor="var(--primary)" stopOpacity={0.2}/>
227
- <stop offset="95%" stopColor="var(--primary)" stopOpacity={0}/>
228
- </linearGradient>
229
- </defs>
230
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--border)" opacity={0.5} />
231
- <XAxis dataKey="time" stroke="var(--muted-foreground)" fontSize={10} tickLine={false} axisLine={false} />
232
- <YAxis stroke="var(--muted-foreground)" fontSize={10} tickLine={false} axisLine={false} />
233
- <Tooltip />
234
- <Area type="monotone" dataKey="rollout" stroke="var(--primary)" strokeWidth={2} fill="url(#colorRollout)" />
235
- </AreaChart>
236
- </ResponsiveContainer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </CardContent>
239
  </Card>
@@ -296,9 +339,16 @@ const Dashboard = () => {
296
 
297
  <Card className="border-border/50 bg-card/50 col-span-2">
298
  <CardHeader className="pb-2">
299
- <CardTitle className="text-sm font-medium flex items-center gap-2">
300
- <ShieldAlert className="h-4 w-4 text-red-500" />
301
- Active Alerts & Incidents
 
 
 
 
 
 
 
302
  </CardTitle>
303
  </CardHeader>
304
  <CardContent>
 
16
  import {
17
  Area,
18
  AreaChart,
 
19
  Tooltip,
20
  XAxis,
21
  YAxis,
 
26
  } from "recharts";
27
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
28
  import { Badge } from "@/components/ui/badge";
29
+ import { Button } from "@/components/ui/button";
30
  import { Progress } from "@/components/ui/progress";
31
  import { useMemo } from "react";
32
  import { Observation } from "@/lib/api";
 
38
  const { dashboard: data, state, connectionState, connectionText } = useEnv();
39
  const loading = useMemo(() => connectionState === "checking" && !data && !state, [connectionState, data, state]);
40
 
41
+ useEffect(() => {
42
+ let simInterval: NodeJS.Timeout;
43
+ if (isSimulating) {
44
+ simInterval = setInterval(runSimulationStep, 2000);
45
+ }
46
+ return () => clearInterval(simInterval);
47
+ }, [isSimulating, state]);
48
+
49
  const lastObs: Observation | undefined = state?.history?.[state.history.length - 1]?.observation;
50
 
51
  const healthScore = lastObs?.system_health_score ?? data?.summary?.health_score ?? 0;
 
87
  <h1 className="text-3xl font-bold tracking-tight">Enterprise Overview</h1>
88
  <p className="text-muted-foreground mt-1">Real-time autonomous deployment monitoring.</p>
89
  </div>
90
+ <div className="flex items-center gap-3">
91
+ <Button
92
+ variant={isSimulating ? "destructive" : "default"}
93
+ size="sm"
94
+ onClick={() => setIsSimulating(!isSimulating)}
95
+ className="rounded-full gap-2 px-6"
96
+ >
97
+ {isSimulating ? <Zap className="h-4 w-4 animate-pulse" /> : <Zap className="h-4 w-4" />}
98
+ {isSimulating ? "Stop Autonomous Mode" : "Start Autonomous Mode"}
99
+ </Button>
100
+ <Button
101
+ variant="outline"
102
+ size="sm"
103
+ onClick={() => { api.reset().then(() => fetchData()); }}
104
+ className="rounded-full"
105
+ >
106
+ Reset Session
107
+ </Button>
108
+ <Badge
109
+ variant="outline"
110
+ className={cn(
111
+ "px-4 py-1.5 rounded-full border-2",
112
+ connectionState === "connected"
113
+ ? "border-green-500/30 text-green-600 bg-green-50"
114
+ : connectionState === "disconnected"
115
+ ? "border-destructive/30 text-destructive bg-destructive/5"
116
+ : ""
117
+ )}
118
+ >
119
+ <div className={cn(
120
+ "w-2 h-2 rounded-full mr-2 animate-pulse",
121
+ connectionState === "connected" ? "bg-green-500" : "bg-destructive"
122
+ )} />
123
+ {loading ? "Syncing..." : connectionText}
124
+ </Badge>
125
+ </div>
126
  </div>
127
 
128
  {/* Primary Metrics */}
 
231
 
232
  <div className="grid gap-6 md:grid-cols-7">
233
  {/* Main Traffic Chart */}
234
+ <Card className="md:col-span-4 border-border/50 bg-card/50 shadow-sm overflow-hidden">
235
  <CardHeader>
236
  <CardTitle className="flex items-center justify-between">
237
  Evaluation Traffic
238
+ <Badge variant="outline" className="font-normal text-xs bg-background/50">
239
  Live Stream
240
  </Badge>
241
  </CardTitle>
 
246
  <AreaChart data={state?.history?.map(h => ({
247
  time: h.observation?.time_step,
248
  rollout: h.observation?.current_rollout_percentage,
249
+ error: (h.observation?.error_rate ?? 0) * 100
250
+ })) : [
251
+ { time: -2, rollout: 5, error: 0.1 },
252
+ { time: -1, rollout: 5, error: 0.15 },
253
+ { time: 0, rollout: 5, error: 0.1 }
254
+ ]}
255
+ margin={{ top: 20, right: 30, left: 0, bottom: 0 }}
256
+ >
257
+ <defs>
258
+ <linearGradient id="colorRollout" x1="0" y1="0" x2="0" y2="1">
259
+ <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.4}/>
260
+ <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
261
+ </linearGradient>
262
+ </defs>
263
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#222" />
264
+ <XAxis dataKey="time" stroke="#444" fontSize={10} tickLine={false} axisLine={false} />
265
+ <YAxis stroke="#444" fontSize={10} tickLine={false} axisLine={false} domain={[0, 100]} />
266
+ <Tooltip
267
+ contentStyle={{ backgroundColor: '#111', border: '1px solid #333', borderRadius: '8px' }}
268
+ itemStyle={{ color: '#8b5cf6' }}
269
+ />
270
+ <Area
271
+ type="monotone"
272
+ dataKey="rollout"
273
+ stroke="#a78bfa"
274
+ strokeWidth={3}
275
+ fillOpacity={1}
276
+ fill="url(#colorRollout)"
277
+ animationDuration={1000}
278
+ />
279
+ </AreaChart>
280
  </div>
281
  </CardContent>
282
  </Card>
 
339
 
340
  <Card className="border-border/50 bg-card/50 col-span-2">
341
  <CardHeader className="pb-2">
342
+ <CardTitle className="text-sm font-medium flex items-center justify-between">
343
+ <div className="flex items-center gap-2">
344
+ <ShieldAlert className="h-4 w-4 text-red-500" />
345
+ Active Alerts & Incidents
346
+ </div>
347
+ <div className="flex items-center gap-4">
348
+ <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground uppercase font-bold">
349
+ <div className="w-1.5 h-1.5 rounded-full bg-green-500" /> Slack
350
+ </div>
351
+ </div>
352
  </CardTitle>
353
  </CardHeader>
354
  <CardContent>
frontend/components/layout/sidebar.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
  Flag,
9
  Beaker,
10
  BrainCircuit,
 
11
  Settings,
12
  } from "lucide-react";
13
  import { motion } from "framer-motion";
@@ -33,6 +34,11 @@ const sidebarNavItems = [
33
  href: "/ai-decisions",
34
  icon: BrainCircuit,
35
  },
 
 
 
 
 
36
  {
37
  title: "Settings",
38
  href: "/settings",
 
8
  Flag,
9
  Beaker,
10
  BrainCircuit,
11
+ Activity,
12
  Settings,
13
  } from "lucide-react";
14
  import { motion } from "framer-motion";
 
34
  href: "/ai-decisions",
35
  icon: BrainCircuit,
36
  },
37
+ {
38
+ title: "Integrations",
39
+ href: "/integrations",
40
+ icon: Activity,
41
+ },
42
  {
43
  title: "Settings",
44
  href: "/settings",
frontend/components/theme-provider.tsx CHANGED
@@ -1,11 +1,71 @@
1
  "use client";
2
 
3
  import * as React from "react";
4
- import { ThemeProvider as NextThemesProvider } from "next-themes";
 
 
 
 
 
 
 
 
 
 
5
 
6
  export function ThemeProvider({
7
  children,
 
 
8
  ...props
9
- }: React.ComponentProps<typeof NextThemesProvider>) {
10
- return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
 
1
  "use client";
2
 
3
  import * as React from "react";
4
+
5
+ type Theme = "dark" | "light" | "system";
6
+
7
+ type ThemeProviderState = {
8
+ theme: Theme;
9
+ setTheme: (theme: Theme) => void;
10
+ };
11
+
12
+ const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>(
13
+ undefined
14
+ );
15
 
16
  export function ThemeProvider({
17
  children,
18
+ defaultTheme = "dark",
19
+ storageKey = "ui-theme",
20
  ...props
21
+ }: {
22
+ children: React.ReactNode;
23
+ defaultTheme?: Theme;
24
+ storageKey?: string;
25
+ [key: string]: unknown;
26
+ }) {
27
+ const [theme, setTheme] = React.useState<Theme>(
28
+ () => (typeof window !== "undefined" && (localStorage.getItem(storageKey) as Theme)) || defaultTheme
29
+ );
30
+
31
+ React.useEffect(() => {
32
+ const root = window.document.documentElement;
33
+
34
+ root.classList.remove("light", "dark");
35
+
36
+ if (theme === "system") {
37
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
38
+ .matches
39
+ ? "dark"
40
+ : "light";
41
+
42
+ root.classList.add(systemTheme);
43
+ return;
44
+ }
45
+
46
+ root.classList.add(theme);
47
+ }, [theme]);
48
+
49
+ const value = {
50
+ theme,
51
+ setTheme: (theme: Theme) => {
52
+ localStorage.setItem(storageKey, theme);
53
+ setTheme(theme);
54
+ },
55
+ };
56
+
57
+ return (
58
+ <ThemeProviderContext.Provider {...props} value={value}>
59
+ {children}
60
+ </ThemeProviderContext.Provider>
61
+ );
62
+ }
63
+
64
+ export const useTheme = () => {
65
+ const context = React.useContext(ThemeProviderContext);
66
+
67
+ if (context === undefined)
68
+ throw new Error("useTheme must be used within a ThemeProvider");
69
+
70
+ return context;
71
  }
frontend/components/theme-toggle.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import * as React from "react";
4
  import { Moon, Sun } from "lucide-react";
5
- import { useTheme } from "next-themes";
6
  import { cn } from "@/lib/utils";
7
 
8
  import {
 
2
 
3
  import * as React from "react";
4
  import { Moon, Sun } from "lucide-react";
5
+ import { useTheme } from "@/components/theme-provider";
6
  import { cn } from "@/lib/utils";
7
 
8
  import {
frontend/lib/api.ts CHANGED
@@ -122,9 +122,12 @@ export interface State {
122
  episode_id: string;
123
  step_count: number;
124
  total_reward: number;
 
125
  is_done: boolean;
126
  scenario_name: string;
127
  difficulty: string;
 
 
128
  history: Array<{
129
  observation?: Observation;
130
  reward?: number;
@@ -323,7 +326,8 @@ export const api = {
323
 
324
  async getDashboard(): Promise<DashboardData> {
325
  try {
326
- return this.request<DashboardData>("/monitoring/dashboard", { method: "GET" });
 
327
  } catch (error) {
328
  // Monitoring can be disabled on backend; fall back to core state/health data.
329
  if (error instanceof Error && /(403|monitoring is not enabled)/i.test(error.message)) {
 
122
  episode_id: string;
123
  step_count: number;
124
  total_reward: number;
125
+ done?: boolean;
126
  is_done: boolean;
127
  scenario_name: string;
128
  difficulty: string;
129
+ action_history?: ActionHistoryItem[];
130
+ rollout_history?: number[];
131
  history: Array<{
132
  observation?: Observation;
133
  reward?: number;
 
326
 
327
  async getDashboard(): Promise<DashboardData> {
328
  try {
329
+ const dashboard = await this.request<unknown>("/monitoring/dashboard", { method: "GET" });
330
+ return normalizeDashboard(dashboard);
331
  } catch (error) {
332
  // Monitoring can be disabled on backend; fall back to core state/health data.
333
  if (error instanceof Error && /(403|monitoring is not enabled)/i.test(error.message)) {
node_modules/frontend/app/ai-decisions/page.tsx CHANGED
@@ -193,6 +193,11 @@ export default function AIDecisionsPage() {
193
  <div className="space-y-0">
194
  {recentDecisions.length > 0 ? (
195
  recentDecisions.map((step, i) => (
 
 
 
 
 
196
  <div key={i} className="flex gap-4 border-b border-border/30 last:border-0 py-4 group">
197
  <div className="flex flex-col items-center">
198
  <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
@@ -240,6 +245,8 @@ export default function AIDecisionsPage() {
240
  </div>
241
  </div>
242
  </div>
 
 
243
  ))
244
  ) : (
245
  <div className="py-20 text-center text-muted-foreground flex flex-col items-center gap-4">
 
193
  <div className="space-y-0">
194
  {recentDecisions.length > 0 ? (
195
  recentDecisions.map((step, i) => (
196
+ (() => {
197
+ const actionType = step.action?.action_type ?? "MAINTAIN";
198
+ const targetPercentage = step.action?.target_percentage ?? 0;
199
+ const reason = step.action?.reason ?? "No reasoning provided.";
200
+ return (
201
  <div key={i} className="flex gap-4 border-b border-border/30 last:border-0 py-4 group">
202
  <div className="flex flex-col items-center">
203
  <div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
 
245
  </div>
246
  </div>
247
  </div>
248
+ );
249
+ })()
250
  ))
251
  ) : (
252
  <div className="py-20 text-center text-muted-foreground flex flex-col items-center gap-4">
node_modules/frontend/app/integrations/page.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ MessageSquare,
8
+ GitGraph,
9
+ ExternalLink,
10
+ RefreshCw,
11
+ Bell
12
+ } from "lucide-react";
13
+ import { useEffect, useState } from "react";
14
+ import { api } from "@/lib/api";
15
+
16
+ const IntegrationsPage = () => {
17
+ const [loading, setLoading] = useState(false);
18
+ const [lastSync, setLastSync] = useState("Never");
19
+ const [isHealthy, setIsHealthy] = useState(false);
20
+
21
+ const syncIntegrations = async () => {
22
+ setLoading(true);
23
+ try {
24
+ const [health] = await Promise.all([
25
+ api.getHealth(),
26
+ api.getDashboard(),
27
+ ]);
28
+ setIsHealthy(Boolean(health?.environment_ready));
29
+ setLastSync(new Date().toLocaleTimeString());
30
+ } catch (error) {
31
+ console.error("Failed to sync integration status:", error);
32
+ setIsHealthy(false);
33
+ setLastSync("Sync failed");
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ useEffect(() => {
40
+ const initialSync = setTimeout(() => {
41
+ void syncIntegrations();
42
+ }, 0);
43
+ return () => clearTimeout(initialSync);
44
+ }, []);
45
+
46
+ const integrations = [
47
+ {
48
+ id: "slack",
49
+ name: "Slack",
50
+ description: "Automated rollout notifications and approval workflows.",
51
+ icon: MessageSquare,
52
+ status: isHealthy ? "connected" : "disconnected",
53
+ details: isHealthy ? "Alerts can be routed through backend automation" : "Backend unavailable",
54
+ lastSync
55
+ },
56
+ {
57
+ id: "github",
58
+ name: "GitHub",
59
+ description: "Trigger rollouts from PRs and sync deployment status.",
60
+ icon: GitGraph,
61
+ status: "disconnected",
62
+ details: "Requires OAuth",
63
+ lastSync
64
+ }
65
+ ];
66
+
67
+ return (
68
+ <div className="flex-1 space-y-6 p-8">
69
+ <div className="flex items-center justify-between">
70
+ <div>
71
+ <h1 className="text-3xl font-bold tracking-tight">Integrations</h1>
72
+ <p className="text-muted-foreground mt-1">Manage external connections and observability sync.</p>
73
+ </div>
74
+ <Button variant="outline" size="sm" className="rounded-full" onClick={syncIntegrations} disabled={loading}>
75
+ <RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
76
+ {loading ? "Syncing..." : "Sync All"}
77
+ </Button>
78
+ </div>
79
+
80
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
81
+ {integrations.map((item) => (
82
+ <Card key={item.id} className="border-border/50 bg-card/50">
83
+ <CardHeader className="pb-3">
84
+ <div className="flex items-center justify-between">
85
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted/50">
86
+ <item.icon className="h-6 w-6 text-primary" />
87
+ </div>
88
+ <Badge
89
+ variant={item.status === "connected" ? "default" : "secondary"}
90
+ className={item.status === "connected" ? "bg-green-500/10 text-green-600 hover:bg-green-500/10" : ""}
91
+ >
92
+ {item.status === "connected" ? "Connected" : "Disconnected"}
93
+ </Badge>
94
+ </div>
95
+ <CardTitle className="mt-4">{item.name}</CardTitle>
96
+ <CardDescription>{item.description}</CardDescription>
97
+ </CardHeader>
98
+ <CardContent>
99
+ <div className="space-y-4">
100
+ <div className="flex items-center justify-between text-sm">
101
+ <span className="text-muted-foreground">Status</span>
102
+ <span className="font-medium">{item.details}</span>
103
+ </div>
104
+ <div className="flex items-center justify-between text-sm">
105
+ <span className="text-muted-foreground">Last Sync</span>
106
+ <span className="text-xs">{item.lastSync}</span>
107
+ </div>
108
+ <div className="pt-4 flex gap-2">
109
+ <Button variant="outline" className="w-full text-xs h-8 rounded-full">
110
+ Configure
111
+ </Button>
112
+ <Button variant="ghost" size="icon" className="h-8 w-8 rounded-full">
113
+ <ExternalLink className="h-4 w-4" />
114
+ </Button>
115
+ </div>
116
+ </div>
117
+ </CardContent>
118
+ </Card>
119
+ ))}
120
+ </div>
121
+
122
+ <Card className="border-primary/20 bg-primary/5">
123
+ <CardHeader>
124
+ <CardTitle className="flex items-center gap-2 text-primary">
125
+ <Bell className="h-5 w-5" />
126
+ Webhook Endpoints
127
+ </CardTitle>
128
+ <CardDescription>Receive automated alerts from your own infrastructure.</CardDescription>
129
+ </CardHeader>
130
+ <CardContent>
131
+ <div className="rounded-lg border bg-background/50 p-4 font-mono text-xs">
132
+ GET {(api.getApiBaseUrl() || "http://localhost:8000")}/monitoring/alerts
133
+ </div>
134
+ <p className="text-[10px] text-muted-foreground mt-4">
135
+ Uses live backend monitoring routes for alert ingestion and observability polling.
136
+ </p>
137
+ </CardContent>
138
+ </Card>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default IntegrationsPage;
node_modules/frontend/app/layout.tsx CHANGED
@@ -30,8 +30,7 @@ export default function RootLayout({
30
  <body className="min-h-full flex flex-col font-sans">
31
  <ThemeProvider
32
  attribute="class"
33
- defaultTheme="system"
34
- enableSystem
35
  disableTransitionOnChange
36
  >
37
  <EnvProvider>
 
30
  <body className="min-h-full flex flex-col font-sans">
31
  <ThemeProvider
32
  attribute="class"
33
+ defaultTheme="dark"
 
34
  disableTransitionOnChange
35
  >
36
  <EnvProvider>
node_modules/frontend/app/page.tsx CHANGED
@@ -16,7 +16,6 @@ import {
16
  import {
17
  Area,
18
  AreaChart,
19
- ResponsiveContainer,
20
  Tooltip,
21
  XAxis,
22
  YAxis,
@@ -27,6 +26,7 @@ import {
27
  } from "recharts";
28
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
29
  import { Badge } from "@/components/ui/badge";
 
30
  import { Progress } from "@/components/ui/progress";
31
  import { useMemo } from "react";
32
  import { Observation } from "@/lib/api";
@@ -38,6 +38,14 @@ const Dashboard = () => {
38
  const { dashboard: data, state, connectionState, connectionText } = useEnv();
39
  const loading = useMemo(() => connectionState === "checking" && !data && !state, [connectionState, data, state]);
40
 
 
 
 
 
 
 
 
 
41
  const lastObs: Observation | undefined = state?.history?.[state.history.length - 1]?.observation;
42
 
43
  const healthScore = lastObs?.system_health_score ?? data?.summary?.health_score ?? 0;
@@ -79,23 +87,42 @@ const Dashboard = () => {
79
  <h1 className="text-3xl font-bold tracking-tight">Enterprise Overview</h1>
80
  <p className="text-muted-foreground mt-1">Real-time autonomous deployment monitoring.</p>
81
  </div>
82
- <Badge
83
- variant="outline"
84
- className={cn(
85
- "px-4 py-1.5 rounded-full border-2",
86
- connectionState === "connected"
87
- ? "border-green-500/30 text-green-600 bg-green-50"
88
- : connectionState === "disconnected"
89
- ? "border-destructive/30 text-destructive bg-destructive/5"
90
- : ""
91
- )}
92
- >
93
- <div className={cn(
94
- "w-2 h-2 rounded-full mr-2 animate-pulse",
95
- connectionState === "connected" ? "bg-green-500" : "bg-destructive"
96
- )} />
97
- {loading ? "Syncing..." : connectionText}
98
- </Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
 
101
  {/* Primary Metrics */}
@@ -204,11 +231,11 @@ const Dashboard = () => {
204
 
205
  <div className="grid gap-6 md:grid-cols-7">
206
  {/* Main Traffic Chart */}
207
- <Card className="md:col-span-4 border-border/50 bg-card/50 shadow-sm">
208
  <CardHeader>
209
  <CardTitle className="flex items-center justify-between">
210
  Evaluation Traffic
211
- <Badge variant="outline" className="font-normal text-xs">
212
  Live Stream
213
  </Badge>
214
  </CardTitle>
@@ -219,21 +246,37 @@ const Dashboard = () => {
219
  <AreaChart data={state?.history?.map(h => ({
220
  time: h.observation?.time_step,
221
  rollout: h.observation?.current_rollout_percentage,
222
- error: (h.observation?.error_rate ?? 0) * 10000 // scaling for visibility
223
- })) || []}>
224
- <defs>
225
- <linearGradient id="colorRollout" x1="0" y1="0" x2="0" y2="1">
226
- <stop offset="5%" stopColor="var(--primary)" stopOpacity={0.2}/>
227
- <stop offset="95%" stopColor="var(--primary)" stopOpacity={0}/>
228
- </linearGradient>
229
- </defs>
230
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--border)" opacity={0.5} />
231
- <XAxis dataKey="time" stroke="var(--muted-foreground)" fontSize={10} tickLine={false} axisLine={false} />
232
- <YAxis stroke="var(--muted-foreground)" fontSize={10} tickLine={false} axisLine={false} />
233
- <Tooltip />
234
- <Area type="monotone" dataKey="rollout" stroke="var(--primary)" strokeWidth={2} fill="url(#colorRollout)" />
235
- </AreaChart>
236
- </ResponsiveContainer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </CardContent>
239
  </Card>
@@ -296,9 +339,16 @@ const Dashboard = () => {
296
 
297
  <Card className="border-border/50 bg-card/50 col-span-2">
298
  <CardHeader className="pb-2">
299
- <CardTitle className="text-sm font-medium flex items-center gap-2">
300
- <ShieldAlert className="h-4 w-4 text-red-500" />
301
- Active Alerts & Incidents
 
 
 
 
 
 
 
302
  </CardTitle>
303
  </CardHeader>
304
  <CardContent>
 
16
  import {
17
  Area,
18
  AreaChart,
 
19
  Tooltip,
20
  XAxis,
21
  YAxis,
 
26
  } from "recharts";
27
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
28
  import { Badge } from "@/components/ui/badge";
29
+ import { Button } from "@/components/ui/button";
30
  import { Progress } from "@/components/ui/progress";
31
  import { useMemo } from "react";
32
  import { Observation } from "@/lib/api";
 
38
  const { dashboard: data, state, connectionState, connectionText } = useEnv();
39
  const loading = useMemo(() => connectionState === "checking" && !data && !state, [connectionState, data, state]);
40
 
41
+ useEffect(() => {
42
+ let simInterval: NodeJS.Timeout;
43
+ if (isSimulating) {
44
+ simInterval = setInterval(runSimulationStep, 2000);
45
+ }
46
+ return () => clearInterval(simInterval);
47
+ }, [isSimulating, state]);
48
+
49
  const lastObs: Observation | undefined = state?.history?.[state.history.length - 1]?.observation;
50
 
51
  const healthScore = lastObs?.system_health_score ?? data?.summary?.health_score ?? 0;
 
87
  <h1 className="text-3xl font-bold tracking-tight">Enterprise Overview</h1>
88
  <p className="text-muted-foreground mt-1">Real-time autonomous deployment monitoring.</p>
89
  </div>
90
+ <div className="flex items-center gap-3">
91
+ <Button
92
+ variant={isSimulating ? "destructive" : "default"}
93
+ size="sm"
94
+ onClick={() => setIsSimulating(!isSimulating)}
95
+ className="rounded-full gap-2 px-6"
96
+ >
97
+ {isSimulating ? <Zap className="h-4 w-4 animate-pulse" /> : <Zap className="h-4 w-4" />}
98
+ {isSimulating ? "Stop Autonomous Mode" : "Start Autonomous Mode"}
99
+ </Button>
100
+ <Button
101
+ variant="outline"
102
+ size="sm"
103
+ onClick={() => { api.reset().then(() => fetchData()); }}
104
+ className="rounded-full"
105
+ >
106
+ Reset Session
107
+ </Button>
108
+ <Badge
109
+ variant="outline"
110
+ className={cn(
111
+ "px-4 py-1.5 rounded-full border-2",
112
+ connectionState === "connected"
113
+ ? "border-green-500/30 text-green-600 bg-green-50"
114
+ : connectionState === "disconnected"
115
+ ? "border-destructive/30 text-destructive bg-destructive/5"
116
+ : ""
117
+ )}
118
+ >
119
+ <div className={cn(
120
+ "w-2 h-2 rounded-full mr-2 animate-pulse",
121
+ connectionState === "connected" ? "bg-green-500" : "bg-destructive"
122
+ )} />
123
+ {loading ? "Syncing..." : connectionText}
124
+ </Badge>
125
+ </div>
126
  </div>
127
 
128
  {/* Primary Metrics */}
 
231
 
232
  <div className="grid gap-6 md:grid-cols-7">
233
  {/* Main Traffic Chart */}
234
+ <Card className="md:col-span-4 border-border/50 bg-card/50 shadow-sm overflow-hidden">
235
  <CardHeader>
236
  <CardTitle className="flex items-center justify-between">
237
  Evaluation Traffic
238
+ <Badge variant="outline" className="font-normal text-xs bg-background/50">
239
  Live Stream
240
  </Badge>
241
  </CardTitle>
 
246
  <AreaChart data={state?.history?.map(h => ({
247
  time: h.observation?.time_step,
248
  rollout: h.observation?.current_rollout_percentage,
249
+ error: (h.observation?.error_rate ?? 0) * 100
250
+ })) : [
251
+ { time: -2, rollout: 5, error: 0.1 },
252
+ { time: -1, rollout: 5, error: 0.15 },
253
+ { time: 0, rollout: 5, error: 0.1 }
254
+ ]}
255
+ margin={{ top: 20, right: 30, left: 0, bottom: 0 }}
256
+ >
257
+ <defs>
258
+ <linearGradient id="colorRollout" x1="0" y1="0" x2="0" y2="1">
259
+ <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.4}/>
260
+ <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
261
+ </linearGradient>
262
+ </defs>
263
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#222" />
264
+ <XAxis dataKey="time" stroke="#444" fontSize={10} tickLine={false} axisLine={false} />
265
+ <YAxis stroke="#444" fontSize={10} tickLine={false} axisLine={false} domain={[0, 100]} />
266
+ <Tooltip
267
+ contentStyle={{ backgroundColor: '#111', border: '1px solid #333', borderRadius: '8px' }}
268
+ itemStyle={{ color: '#8b5cf6' }}
269
+ />
270
+ <Area
271
+ type="monotone"
272
+ dataKey="rollout"
273
+ stroke="#a78bfa"
274
+ strokeWidth={3}
275
+ fillOpacity={1}
276
+ fill="url(#colorRollout)"
277
+ animationDuration={1000}
278
+ />
279
+ </AreaChart>
280
  </div>
281
  </CardContent>
282
  </Card>
 
339
 
340
  <Card className="border-border/50 bg-card/50 col-span-2">
341
  <CardHeader className="pb-2">
342
+ <CardTitle className="text-sm font-medium flex items-center justify-between">
343
+ <div className="flex items-center gap-2">
344
+ <ShieldAlert className="h-4 w-4 text-red-500" />
345
+ Active Alerts & Incidents
346
+ </div>
347
+ <div className="flex items-center gap-4">
348
+ <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground uppercase font-bold">
349
+ <div className="w-1.5 h-1.5 rounded-full bg-green-500" /> Slack
350
+ </div>
351
+ </div>
352
  </CardTitle>
353
  </CardHeader>
354
  <CardContent>
node_modules/frontend/components/layout/sidebar.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
  Flag,
9
  Beaker,
10
  BrainCircuit,
 
11
  Settings,
12
  } from "lucide-react";
13
  import { motion } from "framer-motion";
@@ -33,6 +34,11 @@ const sidebarNavItems = [
33
  href: "/ai-decisions",
34
  icon: BrainCircuit,
35
  },
 
 
 
 
 
36
  {
37
  title: "Settings",
38
  href: "/settings",
 
8
  Flag,
9
  Beaker,
10
  BrainCircuit,
11
+ Activity,
12
  Settings,
13
  } from "lucide-react";
14
  import { motion } from "framer-motion";
 
34
  href: "/ai-decisions",
35
  icon: BrainCircuit,
36
  },
37
+ {
38
+ title: "Integrations",
39
+ href: "/integrations",
40
+ icon: Activity,
41
+ },
42
  {
43
  title: "Settings",
44
  href: "/settings",
node_modules/frontend/components/theme-provider.tsx CHANGED
@@ -1,11 +1,71 @@
1
  "use client";
2
 
3
  import * as React from "react";
4
- import { ThemeProvider as NextThemesProvider } from "next-themes";
 
 
 
 
 
 
 
 
 
 
5
 
6
  export function ThemeProvider({
7
  children,
 
 
8
  ...props
9
- }: React.ComponentProps<typeof NextThemesProvider>) {
10
- return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
 
1
  "use client";
2
 
3
  import * as React from "react";
4
+
5
+ type Theme = "dark" | "light" | "system";
6
+
7
+ type ThemeProviderState = {
8
+ theme: Theme;
9
+ setTheme: (theme: Theme) => void;
10
+ };
11
+
12
+ const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>(
13
+ undefined
14
+ );
15
 
16
  export function ThemeProvider({
17
  children,
18
+ defaultTheme = "dark",
19
+ storageKey = "ui-theme",
20
  ...props
21
+ }: {
22
+ children: React.ReactNode;
23
+ defaultTheme?: Theme;
24
+ storageKey?: string;
25
+ [key: string]: unknown;
26
+ }) {
27
+ const [theme, setTheme] = React.useState<Theme>(
28
+ () => (typeof window !== "undefined" && (localStorage.getItem(storageKey) as Theme)) || defaultTheme
29
+ );
30
+
31
+ React.useEffect(() => {
32
+ const root = window.document.documentElement;
33
+
34
+ root.classList.remove("light", "dark");
35
+
36
+ if (theme === "system") {
37
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
38
+ .matches
39
+ ? "dark"
40
+ : "light";
41
+
42
+ root.classList.add(systemTheme);
43
+ return;
44
+ }
45
+
46
+ root.classList.add(theme);
47
+ }, [theme]);
48
+
49
+ const value = {
50
+ theme,
51
+ setTheme: (theme: Theme) => {
52
+ localStorage.setItem(storageKey, theme);
53
+ setTheme(theme);
54
+ },
55
+ };
56
+
57
+ return (
58
+ <ThemeProviderContext.Provider {...props} value={value}>
59
+ {children}
60
+ </ThemeProviderContext.Provider>
61
+ );
62
+ }
63
+
64
+ export const useTheme = () => {
65
+ const context = React.useContext(ThemeProviderContext);
66
+
67
+ if (context === undefined)
68
+ throw new Error("useTheme must be used within a ThemeProvider");
69
+
70
+ return context;
71
  }
node_modules/frontend/components/theme-toggle.tsx CHANGED
@@ -2,7 +2,7 @@
2
 
3
  import * as React from "react";
4
  import { Moon, Sun } from "lucide-react";
5
- import { useTheme } from "next-themes";
6
  import { cn } from "@/lib/utils";
7
 
8
  import {
 
2
 
3
  import * as React from "react";
4
  import { Moon, Sun } from "lucide-react";
5
+ import { useTheme } from "@/components/theme-provider";
6
  import { cn } from "@/lib/utils";
7
 
8
  import {
node_modules/frontend/lib/api.ts CHANGED
@@ -122,9 +122,12 @@ export interface State {
122
  episode_id: string;
123
  step_count: number;
124
  total_reward: number;
 
125
  is_done: boolean;
126
  scenario_name: string;
127
  difficulty: string;
 
 
128
  history: Array<{
129
  observation?: Observation;
130
  reward?: number;
@@ -323,7 +326,8 @@ export const api = {
323
 
324
  async getDashboard(): Promise<DashboardData> {
325
  try {
326
- return this.request<DashboardData>("/monitoring/dashboard", { method: "GET" });
 
327
  } catch (error) {
328
  // Monitoring can be disabled on backend; fall back to core state/health data.
329
  if (error instanceof Error && /(403|monitoring is not enabled)/i.test(error.message)) {
 
122
  episode_id: string;
123
  step_count: number;
124
  total_reward: number;
125
+ done?: boolean;
126
  is_done: boolean;
127
  scenario_name: string;
128
  difficulty: string;
129
+ action_history?: ActionHistoryItem[];
130
+ rollout_history?: number[];
131
  history: Array<{
132
  observation?: Observation;
133
  reward?: number;
 
326
 
327
  async getDashboard(): Promise<DashboardData> {
328
  try {
329
+ const dashboard = await this.request<unknown>("/monitoring/dashboard", { method: "GET" });
330
+ return normalizeDashboard(dashboard);
331
  } catch (error) {
332
  // Monitoring can be disabled on backend; fall back to core state/health data.
333
  if (error instanceof Error && /(403|monitoring is not enabled)/i.test(error.message)) {
pyproject.toml CHANGED
@@ -18,7 +18,6 @@ dependencies = [
18
 
19
  # 🧮 Math & Simulation
20
  "numpy>=1.24.0",
21
- "torch>=2.0.0",
22
 
23
  # 🤖 LLM Integration (OpenAI-compatible client)
24
  "groq>=0.4.0",
 
18
 
19
  # 🧮 Math & Simulation
20
  "numpy>=1.24.0",
 
21
 
22
  # 🤖 LLM Integration (OpenAI-compatible client)
23
  "groq>=0.4.0",