Spaces:
Sleeping
Sleeping
Merge upstream main, preferring our changes
Browse files- feature-flag-agent-env/agents/rl_agent.py +53 -27
- feature-flag-agent-env/feature_flag_env/tools/connectors.py +0 -1
- feature-flag-agent-env/pyproject.toml +0 -1
- feature-flag-agent-env/requirements.txt +0 -1
- frontend/app/ai-decisions/page.tsx +7 -0
- frontend/app/integrations/page.tsx +143 -0
- frontend/app/layout.tsx +1 -2
- frontend/app/page.tsx +88 -38
- frontend/components/layout/sidebar.tsx +6 -0
- frontend/components/theme-provider.tsx +63 -3
- frontend/components/theme-toggle.tsx +1 -1
- frontend/lib/api.ts +5 -1
- node_modules/frontend/app/ai-decisions/page.tsx +7 -0
- node_modules/frontend/app/integrations/page.tsx +143 -0
- node_modules/frontend/app/layout.tsx +1 -2
- node_modules/frontend/app/page.tsx +88 -38
- node_modules/frontend/components/layout/sidebar.tsx +6 -0
- node_modules/frontend/components/theme-provider.tsx +63 -3
- node_modules/frontend/components/theme-toggle.tsx +1 -1
- node_modules/frontend/lib/api.ts +5 -1
- pyproject.toml +0 -1
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
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
nn.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
"
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
"
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) *
|
| 223 |
-
}))
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
<
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
<
|
| 235 |
-
</
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 300 |
-
<
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
export function ThemeProvider({
|
| 7 |
children,
|
|
|
|
|
|
|
| 8 |
...props
|
| 9 |
-
}:
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 "
|
| 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 |
-
|
|
|
|
| 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="
|
| 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 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
"
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
"
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) *
|
| 223 |
-
}))
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
<
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
<
|
| 235 |
-
</
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 300 |
-
<
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
export function ThemeProvider({
|
| 7 |
children,
|
|
|
|
|
|
|
| 8 |
...props
|
| 9 |
-
}:
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 "
|
| 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 |
-
|
|
|
|
| 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",
|