diff --git a/api/cmd/main.go b/api/cmd/main.go
index ba9f8fcb..7abd21a8 100644
--- a/api/cmd/main.go
+++ b/api/cmd/main.go
@@ -569,28 +569,28 @@ func setupRoutes(router *gin.Engine, h *api.Handler, userHandler *handlers.UserH
scaling.GET("/autoscaling/history", loadBalancingHandler.GetScalingHistory)
}
-// // // Compliance & Governance - Admin only
-// compliance := protected.Group("/compliance")
-// compliance.Use(adminMiddleware)
-// {
-// // Frameworks
-// compliance.GET("/frameworks", h.ListComplianceFrameworks)
-// compliance.POST("/frameworks", h.CreateComplianceFramework)
-//
-// // Policies
-// compliance.GET("/policies", h.ListCompliancePolicies)
-// compliance.POST("/policies", h.CreateCompliancePolicy)
-//
-// // Violations
-// compliance.GET("/violations", h.ListViolations)
-// compliance.POST("/violations", h.RecordViolation)
-// compliance.POST("/violations/:violationId/resolve", h.ResolveViolation)
-
-// }
-
-//
-// NOTE: Compliance & Governance is now handled by the streamspace-compliance plugin
-// Install it via: Admin → Plugins → streamspace-compliance
+ // Compliance & Governance - Admin only
+ // NOTE: These are STUB endpoints that return empty data when the compliance plugin
+ // is not installed. Install streamspace-compliance plugin for full functionality.
+ compliance := protected.Group("/compliance")
+ compliance.Use(adminMiddleware)
+ {
+ // Dashboard
+ compliance.GET("/dashboard", h.GetComplianceDashboard)
+
+ // Frameworks
+ compliance.GET("/frameworks", h.ListComplianceFrameworks)
+ compliance.POST("/frameworks", h.CreateComplianceFramework)
+
+ // Policies
+ compliance.GET("/policies", h.ListCompliancePolicies)
+ compliance.POST("/policies", h.CreateCompliancePolicy)
+
+ // Violations
+ compliance.GET("/violations", h.ListViolations)
+ compliance.POST("/violations", h.RecordViolation)
+ compliance.POST("/violations/:violationId/resolve", h.ResolveViolation)
+ }
// Templates (read: all users, write: operators/admins)
templates := protected.Group("/templates")
{
diff --git a/api/internal/api/stubs.go b/api/internal/api/stubs.go
index d40a0b33..33464b70 100644
--- a/api/internal/api/stubs.go
+++ b/api/internal/api/stubs.go
@@ -620,7 +620,7 @@ func (h *Handler) GetMetrics(c *gin.Context) {
usedPods := 0
totalPods := 0
- for _, node := range nodes {
+ for _, node := range nodes.Items {
// Check if node is ready
for _, condition := range node.Status.Conditions {
if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue {
@@ -644,7 +644,7 @@ func (h *Handler) GetMetrics(c *gin.Context) {
// Get all pods to calculate resource usage
pods, err := h.k8sClient.GetPods(ctx, h.namespace)
if err == nil {
- usedPods = len(pods)
+ usedPods = len(pods.Items)
}
// Get session counts from database
@@ -715,9 +715,9 @@ func (h *Handler) GetMetrics(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"cluster": gin.H{
"nodes": gin.H{
- "total": len(nodes),
+ "total": len(nodes.Items),
"ready": readyNodes,
- "notReady": len(nodes) - readyNodes,
+ "notReady": len(nodes.Items) - readyNodes,
},
"sessions": gin.H{
"total": sessionCounts.Total,
@@ -932,3 +932,87 @@ func (h *Handler) WebhookRepositorySync(c *gin.Context) {
"repositoryID": repoID,
})
}
+
+// ====================================================================================
+// COMPLIANCE STUBS
+// ====================================================================================
+// NOTE: These are stub endpoints that return empty data when the compliance plugin
+// is not installed. When streamspace-compliance plugin is installed, it will
+// override these endpoints with actual functionality.
+// ====================================================================================
+
+// ListComplianceFrameworks returns available compliance frameworks
+// Stub returns empty list - install streamspace-compliance plugin for real data
+func (h *Handler) ListComplianceFrameworks(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "frameworks": []gin.H{},
+ })
+}
+
+// CreateComplianceFramework creates a new compliance framework
+// Stub returns not implemented - install streamspace-compliance plugin
+func (h *Handler) CreateComplianceFramework(c *gin.Context) {
+ c.JSON(http.StatusNotImplemented, gin.H{
+ "error": "Compliance features require the streamspace-compliance plugin",
+ "message": "Please install the streamspace-compliance plugin from Admin → Plugins",
+ })
+}
+
+// ListCompliancePolicies returns all compliance policies
+// Stub returns empty list - install streamspace-compliance plugin for real data
+func (h *Handler) ListCompliancePolicies(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "policies": []gin.H{},
+ })
+}
+
+// CreateCompliancePolicy creates a new compliance policy
+// Stub returns not implemented - install streamspace-compliance plugin
+func (h *Handler) CreateCompliancePolicy(c *gin.Context) {
+ c.JSON(http.StatusNotImplemented, gin.H{
+ "error": "Compliance features require the streamspace-compliance plugin",
+ "message": "Please install the streamspace-compliance plugin from Admin → Plugins",
+ })
+}
+
+// ListViolations returns all compliance violations
+// Stub returns empty list - install streamspace-compliance plugin for real data
+func (h *Handler) ListViolations(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "violations": []gin.H{},
+ })
+}
+
+// RecordViolation records a new compliance violation
+// Stub returns not implemented - install streamspace-compliance plugin
+func (h *Handler) RecordViolation(c *gin.Context) {
+ c.JSON(http.StatusNotImplemented, gin.H{
+ "error": "Compliance features require the streamspace-compliance plugin",
+ "message": "Please install the streamspace-compliance plugin from Admin → Plugins",
+ })
+}
+
+// ResolveViolation marks a violation as resolved
+// Stub returns not implemented - install streamspace-compliance plugin
+func (h *Handler) ResolveViolation(c *gin.Context) {
+ c.JSON(http.StatusNotImplemented, gin.H{
+ "error": "Compliance features require the streamspace-compliance plugin",
+ "message": "Please install the streamspace-compliance plugin from Admin → Plugins",
+ })
+}
+
+// GetComplianceDashboard returns compliance dashboard metrics
+// Stub returns zero metrics - install streamspace-compliance plugin for real data
+func (h *Handler) GetComplianceDashboard(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "total_policies": 0,
+ "active_policies": 0,
+ "total_open_violations": 0,
+ "violations_by_severity": gin.H{
+ "critical": 0,
+ "high": 0,
+ "medium": 0,
+ "low": 0,
+ },
+ })
+}
diff --git a/api/internal/handlers/nodes.go b/api/internal/handlers/nodes.go
index c23aa087..bdeab240 100644
--- a/api/internal/handlers/nodes.go
+++ b/api/internal/handlers/nodes.go
@@ -70,8 +70,7 @@ import (
"github.com/streamspace/streamspace/api/internal/db"
"github.com/streamspace/streamspace/api/internal/k8s"
corev1 "k8s.io/api/core/v1"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/api/resource"
)
// NodeHandler handles node management operations
@@ -123,13 +122,20 @@ type NodeSystemInfo struct {
}
// ClusterStats represents aggregate cluster statistics
+// ClusterStatsResources represents resource totals in a JSON-friendly format
+type ClusterStatsResources struct {
+ CPU string `json:"cpu"`
+ Memory string `json:"memory"`
+ Pods int `json:"pods"`
+}
+
type ClusterStats struct {
- TotalNodes int `json:"totalNodes"`
- ReadyNodes int `json:"readyNodes"`
- NotReadyNodes int `json:"notReadyNodes"`
- TotalCapacity corev1.ResourceList `json:"totalCapacity"`
- TotalAllocatable corev1.ResourceList `json:"totalAllocatable"`
- TotalUsage *ClusterUsage `json:"totalUsage,omitempty"`
+ TotalNodes int `json:"totalNodes"`
+ ReadyNodes int `json:"readyNodes"`
+ NotReadyNodes int `json:"notReadyNodes"`
+ TotalCapacity *ClusterStatsResources `json:"totalCapacity"`
+ TotalAllocatable *ClusterStatsResources `json:"totalAllocatable"`
+ TotalUsage *ClusterUsage `json:"totalUsage,omitempty"`
}
// ClusterUsage represents aggregate cluster usage
@@ -486,30 +492,25 @@ func (h *NodeHandler) nodeToNodeInfo(node *corev1.Node) NodeInfo {
// Helper function to calculate cluster statistics
func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterStats {
- stats := ClusterStats{
- TotalNodes: len(nodeList.Items),
- ReadyNodes: 0,
- NotReadyNodes: 0,
- TotalCapacity: corev1.ResourceList{
- corev1.ResourceCPU: *newQuantity(0),
- corev1.ResourceMemory: *newQuantity(0),
- corev1.ResourcePods: *newQuantity(0),
- },
- TotalAllocatable: corev1.ResourceList{
- corev1.ResourceCPU: *newQuantity(0),
- corev1.ResourceMemory: *newQuantity(0),
- corev1.ResourcePods: *newQuantity(0),
- },
- }
+ // Initialize temporary resource totals
+ totalCapacityCPU := newQuantity(0)
+ totalCapacityMemory := newQuantity(0)
+ totalCapacityPods := newQuantity(0)
+ totalAllocatableCPU := newQuantity(0)
+ totalAllocatableMemory := newQuantity(0)
+ totalAllocatablePods := newQuantity(0)
+
+ readyNodes := 0
+ notReadyNodes := 0
for _, node := range nodeList.Items {
// Count ready vs not ready nodes
for _, condition := range node.Status.Conditions {
if condition.Type == corev1.NodeReady {
if condition.Status == corev1.ConditionTrue {
- stats.ReadyNodes++
+ readyNodes++
} else {
- stats.NotReadyNodes++
+ notReadyNodes++
}
break
}
@@ -517,31 +518,75 @@ func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterSt
// Aggregate capacity
if cpu, ok := node.Status.Capacity[corev1.ResourceCPU]; ok {
- stats.TotalCapacity[corev1.ResourceCPU].Add(cpu)
+ totalCapacityCPU.Add(cpu)
}
if mem, ok := node.Status.Capacity[corev1.ResourceMemory]; ok {
- stats.TotalCapacity[corev1.ResourceMemory].Add(mem)
+ totalCapacityMemory.Add(mem)
}
if pods, ok := node.Status.Capacity[corev1.ResourcePods]; ok {
- stats.TotalCapacity[corev1.ResourcePods].Add(pods)
+ totalCapacityPods.Add(pods)
}
// Aggregate allocatable
if cpu, ok := node.Status.Allocatable[corev1.ResourceCPU]; ok {
- stats.TotalAllocatable[corev1.ResourceCPU].Add(cpu)
+ totalAllocatableCPU.Add(cpu)
}
if mem, ok := node.Status.Allocatable[corev1.ResourceMemory]; ok {
- stats.TotalAllocatable[corev1.ResourceMemory].Add(mem)
+ totalAllocatableMemory.Add(mem)
}
if pods, ok := node.Status.Allocatable[corev1.ResourcePods]; ok {
- stats.TotalAllocatable[corev1.ResourcePods].Add(pods)
+ totalAllocatablePods.Add(pods)
}
}
+ // Build the response with properly formatted resources
+ stats := ClusterStats{
+ TotalNodes: len(nodeList.Items),
+ ReadyNodes: readyNodes,
+ NotReadyNodes: notReadyNodes,
+ TotalCapacity: &ClusterStatsResources{
+ CPU: formatCPU(totalCapacityCPU.MilliValue()),
+ Memory: formatMemory(totalCapacityMemory.Value()),
+ Pods: int(totalCapacityPods.Value()),
+ },
+ TotalAllocatable: &ClusterStatsResources{
+ CPU: formatCPU(totalAllocatableCPU.MilliValue()),
+ Memory: formatMemory(totalAllocatableMemory.Value()),
+ Pods: int(totalAllocatablePods.Value()),
+ },
+ }
+
return stats
}
+// formatCPU converts milliCPU to a readable string (e.g., "4" for 4 cores)
+func formatCPU(milliCPU int64) string {
+ cores := float64(milliCPU) / 1000.0
+ return fmt.Sprintf("%.1f", cores)
+}
+
+// formatMemory converts bytes to a readable string (e.g., "8Gi", "16Gi")
+func formatMemory(bytes int64) string {
+ const (
+ KB = 1024
+ MB = 1024 * KB
+ GB = 1024 * MB
+ TB = 1024 * GB
+ )
+
+ if bytes >= TB {
+ return fmt.Sprintf("%.1fTi", float64(bytes)/float64(TB))
+ } else if bytes >= GB {
+ return fmt.Sprintf("%.1fGi", float64(bytes)/float64(GB))
+ } else if bytes >= MB {
+ return fmt.Sprintf("%.1fMi", float64(bytes)/float64(MB))
+ } else if bytes >= KB {
+ return fmt.Sprintf("%.1fKi", float64(bytes)/float64(KB))
+ }
+ return fmt.Sprintf("%d", bytes)
+}
+
// Helper function to create a new Quantity
-func newQuantity(value int64) *corev1.Quantity {
- return &corev1.Quantity{}
+func newQuantity(value int64) resource.Quantity {
+ return *resource.NewQuantity(value, resource.DecimalSI)
}
diff --git a/api/internal/k8s/client.go b/api/internal/k8s/client.go
index 6c70ac93..07c7c446 100644
--- a/api/internal/k8s/client.go
+++ b/api/internal/k8s/client.go
@@ -95,6 +95,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
diff --git a/ui/src/pages/admin/Groups.tsx b/ui/src/pages/admin/Groups.tsx
index 79c5cae3..feb81b95 100644
--- a/ui/src/pages/admin/Groups.tsx
+++ b/ui/src/pages/admin/Groups.tsx
@@ -41,6 +41,7 @@ import { useGroupEvents } from '../../hooks/useEnterpriseWebSocket';
import { useNotificationQueue } from '../../components/NotificationQueue';
import EnhancedWebSocketStatus from '../../components/EnhancedWebSocketStatus';
import WebSocketErrorBoundary from '../../components/WebSocketErrorBoundary';
+import Layout from '../../components/Layout';
export default function Groups() {
const navigate = useNavigate();
@@ -161,7 +162,8 @@ export default function Groups() {
return (
-
+
+
@@ -340,7 +342,8 @@ export default function Groups() {
-
+
+
);
}
diff --git a/ui/src/pages/admin/Users.tsx b/ui/src/pages/admin/Users.tsx
index 956565d9..2a0ebd19 100644
--- a/ui/src/pages/admin/Users.tsx
+++ b/ui/src/pages/admin/Users.tsx
@@ -41,6 +41,7 @@ import { useUserEvents } from '../../hooks/useEnterpriseWebSocket';
import { useNotificationQueue } from '../../components/NotificationQueue';
import EnhancedWebSocketStatus from '../../components/EnhancedWebSocketStatus';
import WebSocketErrorBoundary from '../../components/WebSocketErrorBoundary';
+import Layout from '../../components/Layout';
/**
* Users - User account management for administrators
@@ -238,7 +239,8 @@ export default function Users() {
return (
-
+
+
@@ -448,7 +450,8 @@ export default function Users() {
-
+
+
);
}