diff --git a/decisionmaker/rest/handler.go b/decisionmaker/rest/handler.go index 32ddacf..a6b2d08 100644 --- a/decisionmaker/rest/handler.go +++ b/decisionmaker/rest/handler.go @@ -165,6 +165,8 @@ func (h *Handler) SetupRoutes(engine *echo.Echo) error { apiV1.DELETE("/intents", h.echoHandler(h.DeleteIntent), echo.WrapMiddleware(authMiddleware)) apiV1.GET("/scheduling/strategies", h.echoHandler(h.ListIntents), echo.WrapMiddleware(authMiddleware)) apiV1.POST("/metrics", h.echoHandler(h.UpdateMetrics), echo.WrapMiddleware(authMiddleware)) + // pod routes + apiV1.GET("/pods/pids", h.echoHandler(h.GetPodsPIDs), echo.WrapMiddleware(authMiddleware)) // token routes apiV1.POST("/auth/token", h.echoHandler(h.GenTokenHandler)) } diff --git a/decisionmaker/rest/pod_handler.go b/decisionmaker/rest/pod_handler.go new file mode 100644 index 0000000..cd37a81 --- /dev/null +++ b/decisionmaker/rest/pod_handler.go @@ -0,0 +1,96 @@ +package rest + +import ( + "net/http" + "os" + "time" + + "github.com/Gthulhu/api/decisionmaker/domain" +) + +// PodProcess represents a process information within a pod (for API response) +type PodProcess struct { + PID int `json:"pid"` + Command string `json:"command"` + PPID int `json:"ppid,omitempty"` + ContainerID string `json:"container_id,omitempty"` +} + +// PodInfo represents pod information with associated processes (for API response) +type PodInfo struct { + PodUID string `json:"pod_uid"` + PodID string `json:"pod_id,omitempty"` + Processes []PodProcess `json:"processes"` +} + +// GetPodsPIDsResponse is the response structure for the GET /api/v1/pods/pids endpoint +type GetPodsPIDsResponse struct { + Pods []PodInfo `json:"pods"` + Timestamp string `json:"timestamp"` + NodeName string `json:"node_name"` + NodeID string `json:"node_id,omitempty"` +} + +// GetPodsPIDs godoc +// @Summary Get Pod to PID mappings +// @Description Returns all pods running on this node with their associated process IDs +// @Tags Pods +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SuccessResponse[GetPodsPIDsResponse] +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /api/v1/pods/pids [get] +func (h *Handler) GetPodsPIDs(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Get all pod information from /proc filesystem + podInfoMap, err := h.Service.GetAllPodInfos(ctx) + if err != nil { + h.ErrorResponse(ctx, w, http.StatusInternalServerError, "Failed to retrieve pod information", err) + return + } + + // Convert map to slice for response + pods := make([]PodInfo, 0, len(podInfoMap)) + for _, podInfo := range podInfoMap { + processes := make([]PodProcess, 0, len(podInfo.Processes)) + for _, proc := range podInfo.Processes { + processes = append(processes, PodProcess{ + PID: proc.PID, + Command: proc.Command, + PPID: proc.PPID, + ContainerID: proc.ContainerID, + }) + } + pods = append(pods, PodInfo{ + PodUID: podInfo.PodUID, + PodID: podInfo.PodID, + Processes: processes, + }) + } + + // Get node name from hostname or environment variable + nodeName, _ := os.Hostname() + if envNodeName := os.Getenv("NODE_NAME"); envNodeName != "" { + nodeName = envNodeName + } + + response := GetPodsPIDsResponse{ + Pods: pods, + Timestamp: time.Now().UTC().Format(time.RFC3339), + NodeName: nodeName, + } + + h.JSONResponse(ctx, w, http.StatusOK, NewSuccessResponse(&response)) +} + +// convertDomainPodProcess converts domain.PodProcess to rest.PodProcess +func convertDomainPodProcess(proc domain.PodProcess) PodProcess { + return PodProcess{ + PID: proc.PID, + Command: proc.Command, + PPID: proc.PPID, + ContainerID: proc.ContainerID, + } +} diff --git a/web/static/app.js b/web/static/app.js index 3249715..7f6f9ab 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1,551 +1,1012 @@ -// Global variables -let jwtToken = localStorage.getItem('jwtToken'); -let isAuthenticated = !!jwtToken; -let strategyCounter = 0; // Counter for unique strategy IDs - -// Auto-refresh intervals -let healthInterval = null; -let metricsInterval = null; -let healthHistory = []; // Store last 10 health check results - -// DOM elements -const authStatus = document.getElementById('authStatus'); -const authText = document.getElementById('authText'); -const authBtn = document.getElementById('authBtn'); -const authModal = document.getElementById('authModal'); -const authForm = document.getElementById('authForm'); - -// Initialize the app -document.addEventListener('DOMContentLoaded', function() { - updateAuthStatus(); - setupEventListeners(); - updateAuthRequiredButtons(); - // Add initial strategy form - addStrategy(); -}); - -// Update authentication status display -function updateAuthStatus() { - if (isAuthenticated) { - authText.textContent = 'Authenticated'; - authBtn.textContent = 'Clear Token'; - authBtn.onclick = clearToken; - } else { - authText.textContent = 'Not Authenticated'; - authBtn.textContent = 'Get Token'; - authBtn.onclick = showAuthModal; +/** + * Gthulhu - eBPF Scheduler Control Interface + * Main Application JavaScript + */ + +// =========================================== +// Configuration & State +// =========================================== + +const state = { + jwtToken: localStorage.getItem('jwtToken'), + isAuthenticated: false, + apiBaseUrl: localStorage.getItem('apiBaseUrl') || '', + healthHistory: [], + healthInterval: null, + strategyCounter: 0, + currentUser: null +}; + +state.isAuthenticated = !!state.jwtToken; + +// =========================================== +// API Configuration +// =========================================== + +function getApiUrl(endpoint) { + // Always use relative URLs when apiBaseUrl is empty (same-origin requests via proxy) + if (!state.apiBaseUrl || state.apiBaseUrl === '') { + return endpoint; } - updateAuthRequiredButtons(); + const base = state.apiBaseUrl.replace(/\/$/, ''); + return base + endpoint; } -// Update buttons that require authentication -function updateAuthRequiredButtons() { - const authRequiredElements = document.querySelectorAll('.auth-required'); - authRequiredElements.forEach(element => { - element.disabled = !isAuthenticated; - }); - - // Stop metrics auto-refresh if user is not authenticated - if (!isAuthenticated && metricsInterval) { - clearInterval(metricsInterval); - metricsInterval = null; - const btn = document.getElementById('metricsAutoBtn'); - const intervalInput = document.getElementById('metricsInterval'); - if (btn) { - btn.textContent = 'Start Auto-refresh'; - } - if (intervalInput) { - intervalInput.disabled = false; - } - } +function showConfigModal() { + const modal = document.getElementById('configModal'); + const input = document.getElementById('apiBaseUrl'); + input.value = state.apiBaseUrl; + modal.classList.add('active'); } -// Show authentication modal -function showAuthModal() { - authModal.style.display = 'block'; +function hideConfigModal() { + document.getElementById('configModal').classList.remove('active'); } -// Hide authentication modal -function hideAuthModal() { - authModal.style.display = 'none'; +function saveApiConfig() { + const input = document.getElementById('apiBaseUrl'); + state.apiBaseUrl = input.value.trim(); + localStorage.setItem('apiBaseUrl', state.apiBaseUrl); + hideConfigModal(); + showToast('success', 'Configuration saved successfully'); } -// Clear JWT token -function clearToken() { - jwtToken = null; - isAuthenticated = false; - localStorage.removeItem('jwtToken'); - updateAuthStatus(); - showResult('authResult', 'Authentication token cleared', 'success'); +// =========================================== +// Authentication +// =========================================== + +function showLoginModal() { + document.getElementById('loginModal').classList.add('active'); + document.getElementById('email').focus(); } -// Setup event listeners -function setupEventListeners() { - // Close modal when clicking outside - window.onclick = function(event) { - if (event.target === authModal) { - hideAuthModal(); - } - } +function hideLoginModal() { + const modal = document.getElementById('loginModal'); + modal.classList.remove('active'); + document.getElementById('loginForm').reset(); + hideLoginError(); +} - // Auth form submission - authForm.addEventListener('submit', async function(e) { - e.preventDefault(); - await getJWTToken(); - }); +function showLoginError(message) { + const errorDiv = document.getElementById('loginError'); + errorDiv.textContent = message; + errorDiv.classList.add('show'); } -// Get JWT Token -async function getJWTToken() { - const publicKey = document.getElementById('publicKey').value.trim(); +function hideLoginError() { + const errorDiv = document.getElementById('loginError'); + errorDiv.textContent = ''; + errorDiv.classList.remove('show'); +} + +async function handleLogin(event) { + event.preventDefault(); - if (!publicKey) { - alert('Please enter public key'); + const submitBtn = document.getElementById('loginSubmitBtn'); + const email = document.getElementById('email').value.trim(); + const password = document.getElementById('password').value; + + if (!email || !password) { + showLoginError('Please enter both email and password'); return; } - + + submitBtn.classList.add('loading'); + submitBtn.disabled = true; + hideLoginError(); + try { - const response = await fetch('/api/v1/auth/token', { + const response = await fetch(getApiUrl('/api/v1/auth/login'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - public_key: publicKey - }) + body: JSON.stringify({ username: email, password }) }); - + const data = await response.json(); - if (data.success && data.token) { - jwtToken = data.token; - isAuthenticated = true; - localStorage.setItem('jwtToken', jwtToken); - updateAuthStatus(); - hideAuthModal(); - showResult('authResult', 'Authentication successful!', 'success'); + if (response.ok && data.success && data.data && data.data.token) { + state.jwtToken = data.data.token; + state.isAuthenticated = true; + localStorage.setItem('jwtToken', state.jwtToken); + + updateAuthUI(); + hideLoginModal(); + showToast('success', 'Authentication successful!'); + + await getUserProfile(); + checkHealth(); } else { - showResult('authResult', 'Authentication failed: ' + (data.error || data.message), 'error'); + throw new Error(data.error || data.message || 'Authentication failed'); } } catch (error) { - showResult('authResult', 'Request failed: ' + error.message, 'error'); + console.error('Login error:', error); + showLoginError(error.message || 'Failed to authenticate. Please check your credentials.'); + } finally { + submitBtn.classList.remove('loading'); + submitBtn.disabled = false; } } -// API request helper with authentication -async function makeAuthenticatedRequest(url, options = {}) { - if (!isAuthenticated) { - throw new Error('Authentication required'); +function logout() { + state.jwtToken = null; + state.isAuthenticated = false; + state.currentUser = null; + localStorage.removeItem('jwtToken'); + + if (state.healthInterval) { + clearInterval(state.healthInterval); + state.healthInterval = null; + document.getElementById('healthAutoRefresh').checked = false; } + + document.getElementById('userUsername').textContent = '--'; + document.getElementById('userEmail').textContent = '--'; + document.getElementById('userRole').textContent = '--'; + document.getElementById('userDetails').querySelector('.code-block').textContent = 'Authenticate to view profile...'; + + updateAuthUI(); + showToast('info', 'You have been logged out'); +} +function updateAuthUI() { + const connectionStatus = document.getElementById('connectionStatus'); + const authBtn = document.getElementById('authBtn'); + const statusText = connectionStatus.querySelector('.status-text'); + + if (state.isAuthenticated) { + connectionStatus.classList.add('connected'); + statusText.textContent = 'Connected'; + authBtn.innerHTML = 'Disconnect'; + authBtn.classList.add('logout'); + authBtn.onclick = logout; + } else { + connectionStatus.classList.remove('connected'); + statusText.textContent = 'Disconnected'; + authBtn.innerHTML = 'Connect'; + authBtn.classList.remove('logout'); + authBtn.onclick = showLoginModal; + } + + document.querySelectorAll('.auth-required').forEach(el => { + el.disabled = !state.isAuthenticated; + }); +} + +// =========================================== +// API Requests +// =========================================== + +async function makeAuthenticatedRequest(endpoint, options = {}) { + if (!state.isAuthenticated) { + throw new Error('Authentication required'); + } + const headers = { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${jwtToken}`, + 'Authorization': 'Bearer ' + state.jwtToken, ...options.headers }; - - return fetch(url, { + + const response = await fetch(getApiUrl(endpoint), { ...options, headers }); + + if (response.status === 401) { + logout(); + showToast('error', 'Session expired. Please login again.'); + throw new Error('Session expired'); + } + + return response; } -// Check health endpoint +// =========================================== +// Health Check +// =========================================== + async function checkHealth() { + const healthStatus = document.getElementById('healthStatus'); + const healthRing = document.getElementById('healthRing'); + const healthDetails = document.getElementById('healthDetails'); + + healthStatus.textContent = '...'; + healthStatus.className = 'health-status'; + try { - const response = await fetch('/health'); + const response = await fetch(getApiUrl('/health')); const data = await response.json(); - const isHealthy = response.ok && data.status == "healthy"; + const isHealthy = response.ok && data.status === 'healthy'; - // Add to health history - healthHistory.push({ + state.healthHistory.push({ timestamp: new Date().toISOString(), healthy: isHealthy, data: data }); - // Keep only last 10 results - if (healthHistory.length > 10) { - healthHistory.shift(); + if (state.healthHistory.length > 10) { + state.healthHistory.shift(); } - // Update health grid + healthStatus.textContent = isHealthy ? 'OK' : 'FAIL'; + healthStatus.className = 'health-status ' + (isHealthy ? 'healthy' : 'unhealthy'); + healthRing.className = 'ring-progress ' + (isHealthy ? 'healthy' : 'unhealthy'); + + healthDetails.querySelector('.code-block').textContent = JSON.stringify(data, null, 2); + healthDetails.querySelector('.code-block').className = 'code-block ' + (isHealthy ? 'success' : 'error'); + updateHealthGrid(); - showResult('healthResult', JSON.stringify(data, null, 2), isHealthy ? 'success' : 'error'); } catch (error) { - // Add error to health history - healthHistory.push({ + console.error('Health check error:', error); + + state.healthHistory.push({ timestamp: new Date().toISOString(), healthy: false, error: error.message }); - // Keep only last 10 results - if (healthHistory.length > 10) { - healthHistory.shift(); + if (state.healthHistory.length > 10) { + state.healthHistory.shift(); } - // Update health grid - updateHealthGrid(); + healthStatus.textContent = 'ERR'; + healthStatus.className = 'health-status unhealthy'; + healthRing.className = 'ring-progress unhealthy'; + + healthDetails.querySelector('.code-block').textContent = 'Error: ' + error.message + '\n\nTip: Configure the API Base URL if running from a different origin.'; + healthDetails.querySelector('.code-block').className = 'code-block error'; - showResult('healthResult', 'Request failed: ' + error.message, 'error'); + updateHealthGrid(); } } -// Get Pod-PID mappings -async function getPodPids() { +function updateHealthGrid() { + const grid = document.getElementById('healthGrid'); + grid.innerHTML = ''; + + const emptySlots = 10 - state.healthHistory.length; + for (let i = 0; i < emptySlots; i++) { + const dot = document.createElement('div'); + dot.className = 'history-dot'; + dot.textContent = '-'; + dot.title = 'No data'; + grid.appendChild(dot); + } + + state.healthHistory.forEach(function(result) { + const dot = document.createElement('div'); + dot.className = 'history-dot ' + (result.healthy ? 'healthy' : 'unhealthy'); + dot.textContent = result.healthy ? '✓' : '✗'; + dot.title = new Date(result.timestamp).toLocaleTimeString() + ': ' + (result.healthy ? 'Healthy' : 'Unhealthy'); + grid.appendChild(dot); + }); +} + +function toggleHealthAutoRefresh() { + const checkbox = document.getElementById('healthAutoRefresh'); + + if (checkbox.checked) { + state.healthInterval = setInterval(checkHealth, 5000); + checkHealth(); + showToast('info', 'Auto-refresh enabled (5s interval)'); + } else { + if (state.healthInterval) { + clearInterval(state.healthInterval); + state.healthInterval = null; + } + showToast('info', 'Auto-refresh disabled'); + } +} + +// =========================================== +// Version +// =========================================== + +async function getVersion() { try { - const response = await makeAuthenticatedRequest('/api/v1/pods/pids'); + const response = await fetch(getApiUrl('/version')); const data = await response.json(); + showToast('info', 'Version: ' + (data.version || JSON.stringify(data))); + } catch (error) { + showToast('error', 'Failed to get version: ' + error.message); + } +} + +// =========================================== +// User Profile +// =========================================== + +async function getUserProfile() { + var userDetails = document.getElementById('userDetails'); + + try { + var response = await makeAuthenticatedRequest('/api/v1/users/self'); + var data = await response.json(); - if (data.success) { - showResult('podPidsResult', JSON.stringify(data, null, 2), 'success'); + if (data.success && data.data) { + var user = data.data; + state.currentUser = user; + + // Update enhanced profile display + var displayName = user.username || user.email || 'User'; + var avatarInitial = displayName.charAt(0).toUpperCase(); + + document.getElementById('userAvatar').textContent = avatarInitial; + document.getElementById('userDisplayName').textContent = displayName; + document.getElementById('userEmailDisplay').textContent = user.email || '--'; + document.getElementById('userRoleDisplay').textContent = (user.roles && user.roles[0]) || 'User'; + document.getElementById('userStatusDisplay').textContent = 'Active'; + + // Update status indicator + var statusIndicator = document.getElementById('userStatusIndicator'); + if (statusIndicator) { + statusIndicator.classList.add('online'); + } + + // Update raw details + userDetails.textContent = JSON.stringify(data, null, 2); + userDetails.className = 'code-block success'; } else { - showResult('podPidsResult', 'Failed: ' + (data.error || data.message), 'error'); + throw new Error(data.error || data.message || 'Failed to get user profile'); } } catch (error) { - showResult('podPidsResult', 'Request failed: ' + error.message, 'error'); + userDetails.textContent = 'Error: ' + error.message; + userDetails.className = 'code-block error'; } } -// Get scheduling strategies -async function getStrategies() { +// =========================================== +// Schedule Intents +// =========================================== + +async function getIntents() { + const resultDiv = document.getElementById('intentsResult'); + try { - const response = await makeAuthenticatedRequest('/api/v1/scheduling/strategies'); + const response = await makeAuthenticatedRequest('/api/v1/intents/self'); const data = await response.json(); if (data.success) { - showResult('strategiesResult', JSON.stringify(data, null, 2), 'success'); + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + + const intents = data.data && data.data.intents; + if (intents && intents.length > 0) { + showToast('success', 'Loaded ' + intents.length + ' intent(s)'); + } else { + showToast('info', 'No intents found'); + } } else { - showResult('strategiesResult', 'Failed: ' + (data.error || data.message), 'error'); + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; } } catch (error) { - showResult('strategiesResult', 'Request failed: ' + error.message, 'error'); + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; } } -// Save scheduling strategies (updated for multiple strategies) -async function saveAllStrategies() { - try { - const strategies = []; - const strategyItems = document.querySelectorAll('.strategy-item'); - - for (const item of strategyItems) { - const strategy = { - priority: item.querySelector('input[name="priority"]')?.checked || false, - execution_time: parseInt(item.querySelector('input[name="executionTime"]')?.value) || 20000000 - }; - - // Add PID if specified - const pid = item.querySelector('input[name="pid"]')?.value; - if (pid) { - strategy.pid = parseInt(pid); - } - - // Add command regex if specified - const commandRegex = item.querySelector('input[name="commandRegex"]')?.value; - if (commandRegex) { - strategy.command_regex = commandRegex; - } +function showDeleteIntentsModal() { + document.getElementById('deleteIntentsModal').classList.add('active'); + document.getElementById('intentIds').focus(); +} - // Collect selectors - const selectors = []; - const selectorItems = item.querySelectorAll('.selector'); - - for (const selectorItem of selectorItems) { - const key = selectorItem.querySelector('input[name="selectorKey"]')?.value?.trim(); - const value = selectorItem.querySelector('input[name="selectorValue"]')?.value?.trim(); - if (key && value) { - selectors.push({ key, value }); - } - } - - strategy.selectors = selectors; - strategies.push(strategy); - } +function hideDeleteIntentsModal() { + document.getElementById('deleteIntentsModal').classList.remove('active'); + document.getElementById('intentIds').value = ''; +} - if (strategies.length === 0) { - showResult('strategiesResult', 'No strategies to save', 'info'); - return; +async function deleteIntents() { + const intentIdsInput = document.getElementById('intentIds').value.trim(); + + if (!intentIdsInput) { + showToast('error', 'Please enter intent IDs'); + return; + } + + const intentIds = intentIdsInput.split(',').map(function(id) { return id.trim(); }).filter(function(id) { return id; }); + + if (intentIds.length === 0) { + showToast('error', 'No valid intent IDs provided'); + return; + } + + try { + const response = await makeAuthenticatedRequest('/api/v1/intents', { + method: 'DELETE', + body: JSON.stringify({ intentIds: intentIds }) + }); + + const data = await response.json(); + + if (data.success) { + showToast('success', 'Deleted ' + intentIds.length + ' intent(s)'); + hideDeleteIntentsModal(); + await getIntents(); + } else { + showToast('error', data.error || data.message || 'Failed to delete intents'); } + } catch (error) { + showToast('error', 'Error: ' + error.message); + } +} - const requestBody = { strategies }; - - const response = await makeAuthenticatedRequest('/api/v1/scheduling/strategies', { - method: 'POST', - body: JSON.stringify(requestBody) - }); +// =========================================== +// Scheduling Strategies +// =========================================== +async function getStrategies() { + const resultDiv = document.getElementById('strategiesResult'); + + try { + const response = await makeAuthenticatedRequest('/api/v1/strategies/self'); const data = await response.json(); if (data.success) { - showResult('strategiesResult', `Successfully saved ${strategies.length} strategies: ` + JSON.stringify(data, null, 2), 'success'); + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + + const strategies = data.data && data.data.strategies; + if (strategies && strategies.length > 0) { + showToast('success', 'Loaded ' + strategies.length + ' strategy(ies)'); + } else { + showToast('info', 'No strategies found'); + } } else { - showResult('strategiesResult', 'Save failed: ' + (data.error || data.message), 'error'); + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; } } catch (error) { - showResult('strategiesResult', 'Request failed: ' + error.message, 'error'); + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; } } -// Add a new strategy form function addStrategy() { const container = document.getElementById('strategiesContainer'); - const strategyId = `strategy-${++strategyCounter}`; + const actionsDiv = document.getElementById('strategiesActions'); + state.strategyCounter++; + const strategyId = 'strategy-' + state.strategyCounter; const strategyDiv = document.createElement('div'); strategyDiv.className = 'strategy-item'; strategyDiv.id = strategyId; - strategyDiv.innerHTML = ` -

- Strategy ${strategyCounter} - -

-
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
- - - -
-
- -
-
- `; + strategyDiv.innerHTML = '
' + + '

Strategy #' + state.strategyCounter + '

' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '' + + '
' + + '
'; container.appendChild(strategyDiv); + actionsDiv.style.display = 'flex'; } -// Remove a strategy function removeStrategy(strategyId) { const strategyItem = document.getElementById(strategyId); if (strategyItem) { strategyItem.remove(); } - // If no strategies left, add one const container = document.getElementById('strategiesContainer'); + const actionsDiv = document.getElementById('strategiesActions'); + if (container.children.length === 0) { - addStrategy(); + actionsDiv.style.display = 'none'; } } -// Clear all strategies and add a fresh one function clearAllStrategies() { const container = document.getElementById('strategiesContainer'); + const actionsDiv = document.getElementById('strategiesActions'); + container.innerHTML = ''; - strategyCounter = 0; - addStrategy(); - showResult('strategiesResult', 'All strategies cleared', 'info'); + actionsDiv.style.display = 'none'; + state.strategyCounter = 0; + + showToast('info', 'Form cleared'); } -// Add selector to specific strategy function addSelectorToStrategy(strategyId) { - const selectorsContainer = document.getElementById(`selectors-${strategyId}`); + const selectorsContainer = document.getElementById('selectors-' + strategyId); + const selectorDiv = document.createElement('div'); - selectorDiv.className = 'selector'; - selectorDiv.innerHTML = ` - - - - `; + selectorDiv.className = 'selector-row'; + selectorDiv.innerHTML = '' + + '' + + ''; + selectorsContainer.appendChild(selectorDiv); } -// Get current metrics (replaces submitMetrics) -async function getMetrics() { +function removeSelector(button) { + const selectorDiv = button.parentElement; + const parentContainer = selectorDiv.parentElement; + selectorDiv.remove(); + + if (parentContainer.children.length === 0) { + const strategyId = parentContainer.id.replace('selectors-', ''); + addSelectorToStrategy(strategyId); + } +} + +async function saveAllStrategies() { + const resultDiv = document.getElementById('strategiesResult'); + const strategyItems = document.querySelectorAll('.strategy-item'); + + if (strategyItems.length === 0) { + showToast('error', 'No strategies to save'); + return; + } + + for (let i = 0; i < strategyItems.length; i++) { + const item = strategyItems[i]; + const strategy = {}; + + const strategyNamespace = item.querySelector('input[name="strategyNamespace"]'); + if (strategyNamespace && strategyNamespace.value.trim()) { + strategy.strategyNamespace = strategyNamespace.value.trim(); + } + + const priority = item.querySelector('input[name="priority"]'); + if (priority && priority.value) { + strategy.priority = parseInt(priority.value); + } + + const executionTime = item.querySelector('input[name="executionTime"]'); + if (executionTime && executionTime.value) { + strategy.executionTime = parseInt(executionTime.value); + } + + const commandRegex = item.querySelector('input[name="commandRegex"]'); + if (commandRegex && commandRegex.value.trim()) { + strategy.commandRegex = commandRegex.value.trim(); + } + + const k8sNamespaceInput = item.querySelector('input[name="k8sNamespace"]'); + if (k8sNamespaceInput && k8sNamespaceInput.value.trim()) { + strategy.k8sNamespace = k8sNamespaceInput.value.trim().split(',').map(function(ns) { return ns.trim(); }).filter(function(ns) { return ns; }); + } + + const labelSelectors = []; + const selectorItems = item.querySelectorAll('.selector-row'); + + for (let j = 0; j < selectorItems.length; j++) { + const selectorItem = selectorItems[j]; + const keyInput = selectorItem.querySelector('input[name="selectorKey"]'); + const valueInput = selectorItem.querySelector('input[name="selectorValue"]'); + const key = keyInput && keyInput.value.trim(); + const value = valueInput && valueInput.value.trim(); + if (key && value) { + labelSelectors.push({ key: key, value: value }); + } + } + + if (labelSelectors.length > 0) { + strategy.labelSelectors = labelSelectors; + } + + try { + const response = await makeAuthenticatedRequest('/api/v1/strategies', { + method: 'POST', + body: JSON.stringify(strategy) + }); + + const data = await response.json(); + + if (data.success) { + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + showToast('success', 'Strategy created successfully'); + } else { + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; + showToast('error', data.error || data.message || 'Failed to create strategy'); + } + } catch (error) { + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; + showToast('error', error.message); + } + } +} + +function showDeleteStrategyModal() { + document.getElementById('deleteStrategyModal').classList.add('active'); + document.getElementById('deleteStrategyId').focus(); +} + +function hideDeleteStrategyModal() { + document.getElementById('deleteStrategyModal').classList.remove('active'); + document.getElementById('deleteStrategyId').value = ''; +} + +async function deleteStrategy() { + const strategyId = document.getElementById('deleteStrategyId').value.trim(); + + if (!strategyId) { + showToast('error', 'Please enter a strategy ID'); + return; + } + try { - const response = await makeAuthenticatedRequest('/api/v1/metrics', { - method: 'GET' + const response = await makeAuthenticatedRequest('/api/v1/strategies', { + method: 'DELETE', + body: JSON.stringify({ strategyId: strategyId }) }); const data = await response.json(); + if (data.success) { + showToast('success', 'Strategy deleted successfully'); + hideDeleteStrategyModal(); + await getStrategies(); + } else { + showToast('error', data.error || data.message || 'Failed to delete strategy'); + } + } catch (error) { + showToast('error', 'Error: ' + error.message); + } +} + +// =========================================== +// Users Management +// =========================================== + +async function getUsers() { + const resultDiv = document.getElementById('usersResult'); + + try { + const response = await makeAuthenticatedRequest('/api/v1/users'); + const data = await response.json(); + + if (data.success) { + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + } else { + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; + } + } catch (error) { + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; + } +} + +// =========================================== +// Roles & Permissions +// =========================================== + +async function getRoles() { + const resultDiv = document.getElementById('rolesResult'); + + try { + const response = await makeAuthenticatedRequest('/api/v1/roles'); + const data = await response.json(); + + if (data.success) { + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + } else { + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; + } + } catch (error) { + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; + } +} + +async function getPermissions() { + const resultDiv = document.getElementById('rolesResult'); + + try { + const response = await makeAuthenticatedRequest('/api/v1/permissions'); + const data = await response.json(); + + if (data.success) { + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + } else { + resultDiv.textContent = 'Error: ' + (data.error || data.message); + resultDiv.className = 'code-block error'; + } + } catch (error) { + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; + } +} + +// =========================================== +// Pod-PID Mapping +// =========================================== + +var podPidsInterval = null; + +async function getPodPids() { + var accordion = document.getElementById('podsAccordion'); + var resultDiv = document.getElementById('podsResult'); + var nodeNameDisplay = document.getElementById('nodeNameDisplay'); + var podsCount = document.getElementById('podsCount'); + var processesCount = document.getElementById('processesCount'); + var lastUpdated = document.getElementById('lastUpdated'); + + try { + var response = await makeAuthenticatedRequest('/api/v1/pods/pids'); + var data = await response.json(); + if (data.success && data.data) { - // Format the metrics data nicely - const metrics = data.data; - const formattedMetrics = { - "Last Update": data.metrics_timestamp, - "UserSched Last Run": metrics.usersched_last_run_at, - "Queued Tasks": metrics.nr_queued, - "Scheduled Tasks": metrics.nr_scheduled, - "Running Tasks": metrics.nr_running, - "Online CPUs": metrics.nr_online_cpus, - "User Dispatches": metrics.nr_user_dispatches, - "Kernel Dispatches": metrics.nr_kernel_dispatches, - "Cancel Dispatches": metrics.nr_cancel_dispatches, - "Bounce Dispatches": metrics.nr_bounce_dispatches, - "Failed Dispatches": metrics.nr_failed_dispatches, - "Scheduler Congested": metrics.nr_sched_congested - }; + var pods = data.data.pods || []; + var totalProcesses = 0; + var nodeName = data.data.node_name || 'Unknown'; + + // Build accordion items + var accordionHtml = ''; + + if (pods.length === 0) { + accordionHtml = '
No pods found on this node
'; + } else { + pods.forEach(function(pod, podIndex) { + var processes = pod.processes || []; + totalProcesses += processes.length; + + var podUid = pod.pod_uid || '--'; + var podId = pod.pod_id || '--'; + var processCount = processes.length; + + accordionHtml += '
' + + '
' + + '
' + + '' + + '
' + + '' + escapeHtml(podUid) + '' + + '' + escapeHtml(podId) + '' + + '
' + + '
' + + '
' + + '' + processCount + ' process' + (processCount !== 1 ? 'es' : '') + '' + + '
' + + '
' + + '
'; + + if (processes.length === 0) { + accordionHtml += '
No processes in this pod
'; + } else { + accordionHtml += '' + + '' + + '' + + '' + + '' + + '' + + ''; + + processes.forEach(function(proc) { + accordionHtml += '' + + '' + + '' + + '' + + '' + + ''; + }); + + accordionHtml += '
PIDCommandPPIDContainer ID
' + (proc.pid || '--') + '' + escapeHtml(proc.command || '--') + '' + (proc.ppid || '--') + '' + truncateText(proc.container_id || '--', 16) + '
'; + } + + accordionHtml += '
'; + }); + } - showResult('metricsResult', JSON.stringify(formattedMetrics, null, 2), 'success'); + accordion.innerHTML = accordionHtml; + + // Update summary + nodeNameDisplay.textContent = nodeName; + podsCount.textContent = pods.length; + processesCount.textContent = totalProcesses; + lastUpdated.textContent = data.data.timestamp ? formatTimestamp(data.data.timestamp) : 'Now'; + + // Update raw JSON + resultDiv.textContent = JSON.stringify(data, null, 2); + resultDiv.className = 'code-block success'; + + showToast('success', 'Loaded ' + pods.length + ' pod(s) with ' + totalProcesses + ' process(es)'); } else { - showResult('metricsResult', data.message || 'No metrics data available', 'info'); + throw new Error(data.error || data.message || 'Failed to get pod-PID mappings'); } } catch (error) { - showResult('metricsResult', 'Request failed: ' + error.message, 'error'); + console.error('Pod-PID mapping error:', error); + accordion.innerHTML = '
Error: ' + escapeHtml(error.message) + '
'; + resultDiv.textContent = 'Error: ' + error.message; + resultDiv.className = 'code-block error'; + nodeNameDisplay.textContent = '--'; + podsCount.textContent = '--'; + processesCount.textContent = '--'; + lastUpdated.textContent = '--'; } } -// Add selector input (updated for multiple strategies) -function addSelector() { - // This function is kept for backward compatibility - // but should use addSelectorToStrategy for new functionality - console.warn('addSelector() is deprecated, use addSelectorToStrategy() instead'); +function togglePod(headerElement) { + var accordionItem = headerElement.parentElement; + accordionItem.classList.toggle('expanded'); } -// Remove selector input -function removeSelector(button) { - const selectorDiv = button.parentElement; - const parentContainer = selectorDiv.parentElement; - selectorDiv.remove(); +function toggleAllPods(expand) { + var items = document.querySelectorAll('.pod-accordion-item'); + items.forEach(function(item) { + if (expand) { + item.classList.add('expanded'); + } else { + item.classList.remove('expanded'); + } + }); + showToast('info', expand ? 'Expanded all pods' : 'Collapsed all pods'); +} + +function togglePodPidsAutoRefresh() { + var checkbox = document.getElementById('podPidsAutoRefresh'); - // Ensure at least one selector remains in each strategy - if (parentContainer.children.length === 0) { - const strategyId = parentContainer.id.replace('selectors-', ''); - addSelectorToStrategy(strategyId); + if (checkbox.checked) { + podPidsInterval = setInterval(getPodPids, 5000); + getPodPids(); + showToast('info', 'Pod-PID auto-refresh enabled (5s interval)'); + } else { + if (podPidsInterval) { + clearInterval(podPidsInterval); + podPidsInterval = null; + } + showToast('info', 'Pod-PID auto-refresh disabled'); } } -// Show result in specified element -function showResult(elementId, message, type = 'success') { - const element = document.getElementById(elementId); - if (element) { - element.textContent = message; - element.className = `result ${type}`; - } +function truncateText(text, maxLen) { + if (!text) return '--'; + if (text.length <= maxLen) return text; + return text.substring(0, maxLen - 3) + '...'; } -// Fill sample public key for testing -function fillSampleKey() { - const sampleKey = `-----BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAny28YMC2/+yYj3T29lz6 -0uryNz8gNVrqD7lTJuHQ3DMTE6ADqnERy8VgHve0tWzhJc5ZBZ1Hduvj+z/kNqbc -U81YGhmfOrQ3iFNYBlSAseIHdAw39HGyC6OKzTXI4HRpc8CwcF6hKExkyWlkALr5 -i+IQDfimvarjjZ6Nm368L0Rthv3KOkI5CqRZ6bsVwwBug7GcdkvFs3LiRSKlMBpH -2tCkZ5ZZE8VyuK7VnlwV7n6EHzN5BqaHq8HVLw2KzvibSi+/5wIZV2Yx33tViLbh -OsZqLt6qQCGGgKzNX4TGwRLGAiVV1NCpgQhimZ4YP2thqSsqbaISOuvFlYq+QGP1 -bcvcHB7UhT1ZnHSDYcbT2qiD3VoqytXVKLB1X5XCD99YLSP9B32f1lvZD4MhDtE4 -IhAuqn15MGB5ct4yj/uMldFScs9KhqnWcwS4K6Qx3IfdB+ZxT5hEOWJLEcGqe/CS -XITNG7oS9mrSAJJvHSLz++4R/Sh1MnT2YWjyDk6qeeqAwut0w5iDKWt7qsGEcHFP -IVVlos+xLfrPDtgHQk8upjslUcMyMDTf21Y3RdJ3k1gTR9KHEwzKeiNlLjen9ekF -WupF8jik1aYRWL6h54ZyGxwKEyMYi9o18G2pXPzvVaPYtU+TGXdO4QwiES72TNCD -bNaGj75Gj0sN+LfjjQ4A898CAwEAAQ== ------END PUBLIC KEY-----`; - document.getElementById('publicKey').value = sampleKey; +function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } -// Update health status grid -function updateHealthGrid() { - const healthGrid = document.getElementById('healthGrid'); - if (!healthGrid) return; +function formatTimestamp(isoString) { + try { + var date = new Date(isoString); + return date.toLocaleTimeString(); + } catch (e) { + return isoString; + } +} + +// =========================================== +// Refresh All Data +// =========================================== + +async function refreshAllData() { + showToast('info', 'Refreshing all data...'); - healthGrid.innerHTML = ''; + await checkHealth(); - // Fill empty slots if we have less than 10 results - const totalSlots = 10; - const emptySlots = totalSlots - healthHistory.length; + if (state.isAuthenticated) { + await getUserProfile(); + await getStrategies(); + await getIntents(); + await getPodPids(); + } - // Add empty slots first - for (let i = 0; i < emptySlots; i++) { - const box = document.createElement('div'); - box.className = 'status-box'; - box.style.backgroundColor = '#f0f0f0'; - box.style.borderColor = '#ddd'; - box.title = 'No data'; - healthGrid.appendChild(box); - } - - // Add actual health results - healthHistory.forEach((result, index) => { - const box = document.createElement('div'); - box.className = `status-box ${result.healthy ? 'healthy' : 'unhealthy'}`; - box.textContent = result.healthy ? '✓' : '✗'; - box.title = `${new Date(result.timestamp).toLocaleTimeString()}: ${result.healthy ? 'Healthy' : 'Unhealthy'}`; - healthGrid.appendChild(box); - }); + showToast('success', 'Data refreshed'); } -// Toggle health auto-refresh -function toggleHealthAutoRefresh() { - const btn = document.getElementById('healthAutoBtn'); - const intervalInput = document.getElementById('healthInterval'); - - if (healthInterval) { - // Stop auto-refresh - clearInterval(healthInterval); - healthInterval = null; - btn.textContent = 'Start Auto-refresh'; - intervalInput.disabled = false; - } else { - // Start auto-refresh - const interval = parseInt(intervalInput.value) * 1000; - if (interval < 1000) { - alert('Interval must be at least 1 second'); - return; +// =========================================== +// Toast Notifications +// =========================================== + +function showToast(type, message) { + const container = document.getElementById('toastContainer'); + + const toast = document.createElement('div'); + toast.className = 'toast ' + type; + + const icons = { + success: '✓', + error: '✕', + info: 'ℹ', + warning: '⚠' + }; + + toast.innerHTML = '' + (icons[type] || 'ℹ') + '' + + '' + message + '' + + ''; + + container.appendChild(toast); + + setTimeout(function() { + if (toast.parentElement) { + toast.style.animation = 'toastOut 0.3s ease forwards'; + setTimeout(function() { toast.remove(); }, 300); } - - healthInterval = setInterval(checkHealth, interval); - btn.textContent = 'Stop Auto-refresh'; - intervalInput.disabled = true; - - // Do an immediate check - checkHealth(); - } + }, 4000); } -// Toggle metrics auto-refresh -function toggleMetricsAutoRefresh() { - const btn = document.getElementById('metricsAutoBtn'); - const intervalInput = document.getElementById('metricsInterval'); +// Add CSS for toast out animation +var style = document.createElement('style'); +style.textContent = '@keyframes toastOut { to { opacity: 0; transform: translateX(100%); } }'; +document.head.appendChild(style); + +// =========================================== +// Event Listeners & Initialization +// =========================================== + +document.addEventListener('DOMContentLoaded', function() { + updateAuthUI(); + updateHealthGrid(); - if (metricsInterval) { - // Stop auto-refresh - clearInterval(metricsInterval); - metricsInterval = null; - btn.textContent = 'Start Auto-refresh'; - intervalInput.disabled = false; - } else { - // Start auto-refresh - const interval = parseInt(intervalInput.value) * 1000; - if (interval < 1000) { - alert('Interval must be at least 1 second'); - return; + // Close modals on outside click + document.querySelectorAll('.modal-overlay').forEach(function(modal) { + modal.addEventListener('click', function(e) { + if (e.target === this) { + this.classList.remove('active'); + } + }); + }); + + // Keyboard shortcuts + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + document.querySelectorAll('.modal-overlay.active').forEach(function(modal) { + modal.classList.remove('active'); + }); } - if (!isAuthenticated) { - alert('Authentication required for metrics auto-refresh'); - return; + if (e.ctrlKey && e.key === 'Enter') { + var loginModal = document.getElementById('loginModal'); + if (loginModal.classList.contains('active')) { + document.getElementById('loginForm').dispatchEvent(new Event('submit')); + } } - - metricsInterval = setInterval(getMetrics, interval); - btn.textContent = 'Stop Auto-refresh'; - intervalInput.disabled = true; - - // Do an immediate check - getMetrics(); + }); + + // Initial health check + setTimeout(checkHealth, 500); + + // Load user profile if already authenticated + if (state.isAuthenticated) { + getUserProfile(); } -} +}); + +// Handle token from URL (for OAuth flows) +(function() { + var urlParams = new URLSearchParams(window.location.search); + var token = urlParams.get('token'); + if (token) { + state.jwtToken = token; + state.isAuthenticated = true; + localStorage.setItem('jwtToken', token); + window.history.replaceState({}, document.title, window.location.pathname); + updateAuthUI(); + } +})(); diff --git a/web/static/index.html b/web/static/index.html index 5c7d0d3..470dd98 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -3,110 +3,452 @@ - Gthulhu API server Client + Gthulhu • eBPF Scheduler Control + + + + -
-

Gthulhu API server Client

-
- Not Authenticated - + +
+
+ + +
+
+
+
+ Gthulhu Logo +
+
+

GTHULHU

+ eBPF Scheduler Control Interface +
+
+
+
+ + Disconnected +
+ +
-
- -
+
+ + + + + +
diff --git a/web/static/logo.png b/web/static/logo.png new file mode 100644 index 0000000..97fc72e Binary files /dev/null and b/web/static/logo.png differ diff --git a/web/static/server.py b/web/static/server.py new file mode 100644 index 0000000..028bd46 --- /dev/null +++ b/web/static/server.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Gthulhu Web UI Development Server with API Proxy +Serves static files and proxies API requests to avoid CORS issues. +""" + +import http.server +import urllib.request +import urllib.error +import json +import os +import sys + +# Configuration +STATIC_DIR = os.path.dirname(os.path.abspath(__file__)) +API_BASE_URL = os.environ.get('API_BASE_URL', 'http://localhost:8080') +DECISION_MAKER_URL = os.environ.get('DECISION_MAKER_URL', 'http://localhost:8081') +PORT = int(os.environ.get('PORT', '3000')) + +# Endpoints that should be routed to the Decision Maker instead of Manager +DECISION_MAKER_ENDPOINTS = [ + '/api/v1/pods/pids', +] + +class ProxyHandler(http.server.SimpleHTTPRequestHandler): + """HTTP handler that serves static files and proxies API requests.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=STATIC_DIR, **kwargs) + + def do_OPTIONS(self): + """Handle CORS preflight requests.""" + self.send_response(204) + self.send_cors_headers() + self.end_headers() + + def do_GET(self): + """Handle GET requests - proxy API or serve static files.""" + if self.path.startswith('/api/') or self.path.startswith('/health') or self.path.startswith('/version'): + self.proxy_request('GET', self._get_backend_url()) + else: + super().do_GET() + + def _get_backend_url(self): + """Determine which backend to use based on the request path.""" + path_without_query = self.path.split('?')[0] + for endpoint in DECISION_MAKER_ENDPOINTS: + if path_without_query.startswith(endpoint): + return DECISION_MAKER_URL + return API_BASE_URL + + def do_POST(self): + """Handle POST requests - proxy to API.""" + self.proxy_request('POST', self._get_backend_url()) + + def do_PUT(self): + """Handle PUT requests - proxy to API.""" + self.proxy_request('PUT', self._get_backend_url()) + + def do_DELETE(self): + """Handle DELETE requests - proxy to API.""" + self.proxy_request('DELETE', self._get_backend_url()) + + def send_cors_headers(self): + """Add CORS headers to response.""" + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') + self.send_header('Access-Control-Max-Age', '86400') + + def proxy_request(self, method, backend_url=None): + """Proxy request to the appropriate backend server.""" + if backend_url is None: + backend_url = API_BASE_URL + url = f"{backend_url}{self.path}" + + # Read request body for POST/PUT + body = None + if method in ('POST', 'PUT'): + content_length = int(self.headers.get('Content-Length', 0)) + if content_length > 0: + body = self.rfile.read(content_length) + + # Build request headers + headers = {} + if 'Content-Type' in self.headers: + headers['Content-Type'] = self.headers['Content-Type'] + if 'Authorization' in self.headers: + headers['Authorization'] = self.headers['Authorization'] + + try: + req = urllib.request.Request(url, data=body, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=30) as response: + response_body = response.read() + + self.send_response(response.status) + self.send_cors_headers() + + # Forward content-type + content_type = response.headers.get('Content-Type', 'application/json') + self.send_header('Content-Type', content_type) + self.send_header('Content-Length', len(response_body)) + self.end_headers() + + self.wfile.write(response_body) + + except urllib.error.HTTPError as e: + error_body = e.read() + self.send_response(e.code) + self.send_cors_headers() + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', len(error_body)) + self.end_headers() + self.wfile.write(error_body) + + except urllib.error.URLError as e: + error_msg = json.dumps({ + 'success': False, + 'error': f'API server unreachable: {str(e.reason)}' + }).encode() + self.send_response(503) + self.send_cors_headers() + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', len(error_msg)) + self.end_headers() + self.wfile.write(error_msg) + + except Exception as e: + error_msg = json.dumps({ + 'success': False, + 'error': f'Proxy error: {str(e)}' + }).encode() + self.send_response(500) + self.send_cors_headers() + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', len(error_msg)) + self.end_headers() + self.wfile.write(error_msg) + + def log_message(self, format, *args): + """Custom log format.""" + method = args[0].split()[0] if args else '?' + path = args[0].split()[1] if args and len(args[0].split()) > 1 else '?' + status = args[1] if len(args) > 1 else '?' + + # Color coding + if str(status).startswith('2'): + color = '\033[92m' # Green + elif str(status).startswith('3'): + color = '\033[93m' # Yellow + elif str(status).startswith('4'): + color = '\033[91m' # Red + elif str(status).startswith('5'): + color = '\033[95m' # Magenta + else: + color = '\033[0m' # Default + + reset = '\033[0m' + print(f"{color}[{method}]{reset} {path} → {color}{status}{reset}") + + +def main(): + print(f""" +\033[96m╔═══════════════════════════════════════════════════════════╗ +║ Gthulhu Web UI Development Server ║ +╚═══════════════════════════════════════════════════════════╝\033[0m + + \033[92m●\033[0m Static files: {STATIC_DIR} + \033[92m●\033[0m Manager API: {API_BASE_URL} + \033[92m●\033[0m DecisionMaker: {DECISION_MAKER_URL} + \033[92m●\033[0m Server: http://localhost:{PORT} + + \033[93mPress Ctrl+C to stop\033[0m +""") + + # Use ThreadingHTTPServer to handle multiple concurrent requests + class ThreadingHTTPServer(http.server.ThreadingHTTPServer): + allow_reuse_address = True + daemon_threads = True + + with ThreadingHTTPServer(('', PORT), ProxyHandler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n\033[93mShutting down...\033[0m") + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/web/static/start-dev.sh b/web/static/start-dev.sh new file mode 100755 index 0000000..3920ec5 --- /dev/null +++ b/web/static/start-dev.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Gthulhu Web UI Development Environment Startup Script + +set -e + +echo "╔═══════════════════════════════════════════════════════════╗" +echo "║ Gthulhu Development Environment Setup ║" +echo "╚═══════════════════════════════════════════════════════════╝" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Get current scheduler pod name (it changes on restart) +SCHEDULER_POD=$(microk8s kubectl get pods 2>/dev/null | grep gthulhu-scheduler | awk '{print $1}') + +if [ -z "$SCHEDULER_POD" ]; then + echo -e "${YELLOW}⚠ Warning: Could not find gthulhu-scheduler pod${NC}" + SCHEDULER_POD="gthulhu-scheduler-XXXXX" +fi + +echo -e "${GREEN}✓ Found scheduler pod: ${SCHEDULER_POD}${NC}" +echo "" +echo -e "${CYAN}Please open 3 terminals and run these commands:${NC}" +echo "" +echo "┌─────────────────────────────────────────────────────────────────────────────┐" +echo "│ Terminal 1 - Manager API (Port 8080) │" +echo "├─────────────────────────────────────────────────────────────────────────────┤" +echo -e "│ ${GREEN}microk8s kubectl port-forward svc/gthulhu-manager 8080:8080 --address 0.0.0.0${NC} │" +echo "│ Purpose: Auth, Users, Intents, Strategies APIs │" +echo "└─────────────────────────────────────────────────────────────────────────────┘" +echo "" +echo "┌─────────────────────────────────────────────────────────────────────────────┐" +echo "│ Terminal 2 - Decision Maker API (Port 8081) │" +echo "├─────────────────────────────────────────────────────────────────────────────┤" +echo -e "│ ${GREEN}microk8s kubectl port-forward pod/${SCHEDULER_POD} 8081:8080 --address 0.0.0.0${NC}" +echo "│ Purpose: Pod-PID Mapping API (scans /proc filesystem) │" +echo "└─────────────────────────────────────────────────────────────────────────────┘" +echo "" +echo "┌─────────────────────────────────────────────────────────────────────────────┐" +echo "│ Terminal 3 - Web UI Dev Server (Port 3000) │" +echo "├─────────────────────────────────────────────────────────────────────────────┤" +echo -e "│ ${GREEN}cd /home/ubuntu/Gthulhu/api/web/static && python3 server.py${NC} │" +echo "│ Purpose: Serves frontend & proxies API requests │" +echo "└─────────────────────────────────────────────────────────────────────────────┘" +echo "" +echo "┌─────────────────────────────────────────────────────────────────────────────┐" +echo "│ Access the Web UI at: http://localhost:3000 │" +echo "└─────────────────────────────────────────────────────────────────────────────┘" +echo "" + +# Optionally copy commands to clipboard if xclip is available +if command -v xclip &> /dev/null; then + echo "microk8s kubectl port-forward svc/gthulhu-manager 8080:8080 --address 0.0.0.0" | xclip -selection clipboard + echo -e "${YELLOW}📋 Terminal 1 command copied to clipboard${NC}" +fi diff --git a/web/static/style.css b/web/static/style.css index 33bb07b..2b786d3 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -1,567 +1,1909 @@ -* { +/* ============================================ + GTHULHU - eBPF Scheduler Control Interface + Dark Cyberpunk Theme + ============================================ */ + +/* CSS Custom Properties */ +:root { + /* Core Colors */ + --bg-primary: #0a0e14; + --bg-secondary: #0d1117; + --bg-tertiary: #161b22; + --bg-card: #12171e; + --bg-elevated: #1c2128; + + /* Accent Colors */ + --accent-primary: #00ff88; + --accent-secondary: #00d4ff; + --accent-tertiary: #7c3aed; + --accent-warning: #ffb800; + --accent-danger: #ff4757; + --accent-success: #00ff88; + + /* Text Colors */ + --text-primary: #f0f6fc; + --text-secondary: #c9d1d9; + --text-muted: #6e7681; + --text-accent: #00ff88; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%); + --gradient-accent: linear-gradient(135deg, #7c3aed 0%, #00d4ff 100%); + --gradient-dark: linear-gradient(180deg, #0a0e14 0%, #161b22 100%); + + /* Borders */ + --border-color: #21262d; + --border-glow: rgba(0, 255, 136, 0.3); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 20px rgba(0, 255, 136, 0.2); + + /* Typography */ + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --font-display: 'Orbitron', sans-serif; + + /* Spacing */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 3rem; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-base: 250ms ease; + --transition-slow: 400ms ease; +} + +/* Reset & Base */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } +html { + font-size: 16px; + scroll-behavior: smooth; +} + body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-family: var(--font-mono); + background: var(--bg-primary); + color: var(--text-primary); line-height: 1.6; - color: #333; - background-color: #f5f5f5; + min-height: 100vh; + overflow-x: hidden; +} + +/* Ambient Background Effects */ +.ambient-grid { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + linear-gradient(rgba(0, 255, 136, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 255, 136, 0.03) 1px, transparent 1px); + background-size: 50px 50px; + pointer-events: none; + z-index: 0; +} + +.scan-line { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--gradient-primary); + opacity: 0.5; + animation: scan 8s linear infinite; + pointer-events: none; + z-index: 1; +} + +@keyframes scan { + 0% { transform: translateY(0); opacity: 0; } + 10% { opacity: 0.5; } + 90% { opacity: 0.5; } + 100% { transform: translateY(100vh); opacity: 0; } } -header { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 1rem 2rem; +/* Header */ +.main-header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(10, 14, 20, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border-color); + padding: var(--space-md) var(--space-xl); +} + +.header-content { + max-width: 1400px; + margin: 0 auto; display: flex; justify-content: space-between; align-items: center; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); } -header h1 { - font-size: 1.8rem; - font-weight: 300; +.logo-section { + display: flex; + align-items: center; + gap: var(--space-md); } -.auth-status { +.logo-icon { + width: 52px; + height: 52px; display: flex; align-items: center; - gap: 1rem; + justify-content: center; } -#authText { - font-size: 0.9rem; - opacity: 0.9; +.logo-image { + width: 100%; + height: 100%; + object-fit: contain; + border-radius: var(--radius-md); + filter: drop-shadow(0 0 8px rgba(0, 255, 136, 0.4)); + transition: filter var(--transition-base); } -#authBtn { - background: rgba(255,255,255,0.2); - border: 1px solid rgba(255,255,255,0.3); - color: white; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - transition: background 0.3s; +.logo-image:hover { + filter: drop-shadow(0 0 16px rgba(0, 255, 136, 0.6)); } -#authBtn:hover { - background: rgba(255,255,255,0.3); +.logo-text h1 { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 0.2em; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -main { - max-width: 1200px; - margin: 2rem auto; - padding: 0 2rem; +.tagline { + font-size: 0.7rem; + color: var(--text-secondary); + letter-spacing: 0.1em; + text-transform: uppercase; } -.api-section { - background: white; - margin-bottom: 2rem; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.1); +.auth-section { + display: flex; + align-items: center; + gap: var(--space-lg); } -.api-section h2 { - color: #667eea; - margin-bottom: 1rem; - font-size: 1.5rem; - font-weight: 400; +.connection-status { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: var(--bg-tertiary); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); } -.api-section h3 { - color: #555; - margin: 1.5rem 0 1rem 0; - font-size: 1.2rem; +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-danger); + box-shadow: 0 0 8px var(--accent-danger); + animation: blink 2s ease-in-out infinite; } -.api-section h4 { - color: #666; - margin: 1rem 0 0.5rem 0; - font-size: 1rem; +.connection-status.connected .status-dot { + background: var(--accent-success); + box-shadow: 0 0 8px var(--accent-success); + animation: none; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.status-text { + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; } -.api-btn { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; +.auth-btn { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-lg); + background: var(--gradient-primary); border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; + border-radius: var(--radius-md); + color: var(--bg-primary); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.85rem; cursor: pointer; - font-size: 1rem; - transition: transform 0.2s, box-shadow 0.2s; - margin-bottom: 1rem; + transition: var(--transition-base); + text-transform: uppercase; + letter-spacing: 0.05em; } -.api-btn:hover { +.auth-btn:hover { transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + box-shadow: var(--shadow-glow); } -.api-btn:disabled { - background: #ccc; - cursor: not-allowed; - transform: none; - box-shadow: none; +.auth-btn.logout { + background: transparent; + border: 1px solid var(--accent-danger); + color: var(--accent-danger); } -.result { - background: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 4px; - padding: 1rem; - margin-top: 1rem; - font-family: 'Courier New', monospace; - font-size: 0.9rem; - white-space: pre-wrap; - max-height: 400px; - overflow-y: auto; - color: #495057; +.auth-btn.logout:hover { + background: rgba(255, 71, 87, 0.1); } -.modal { - display: none; +/* Modal Styles */ +.modal-overlay { position: fixed; - z-index: 1000; - left: 0; top: 0; + left: 0; width: 100%; height: 100%; - background-color: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(8px); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + opacity: 0; + transition: opacity var(--transition-base); } -.modal-content { - background-color: white; - margin: 10% auto; - padding: 2rem; - border-radius: 8px; - width: 80%; - max-width: 500px; - position: relative; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); +.modal-overlay.active { + display: flex; + opacity: 1; } -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; - position: absolute; - right: 15px; - top: 10px; +.modal-container { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + width: 90%; + max-width: 480px; + box-shadow: var(--shadow-lg), 0 0 60px rgba(0, 255, 136, 0.1); + animation: modalSlideIn 0.3s ease; } -.close:hover { - color: #000; +.modal-container.modal-sm { + max-width: 380px; } -form { - display: flex; - flex-direction: column; - gap: 1rem; +@keyframes modalSlideIn { + from { + transform: translateY(-20px) scale(0.95); + opacity: 0; + } + to { + transform: translateY(0) scale(1); + opacity: 1; + } } -label { - font-weight: 500; - color: #555; +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-lg); + border-bottom: 1px solid var(--border-color); } -input, textarea, select { - padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; - transition: border-color 0.3s; +.modal-header h2 { + display: flex; + align-items: center; + gap: var(--space-sm); + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); } -input:focus, textarea:focus, select:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); +.modal-icon { + font-size: 1.2rem; } -textarea { - min-height: 150px; - resize: vertical; - font-family: 'Courier New', monospace; +.modal-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 1.2rem; + cursor: pointer; + transition: var(--transition-fast); } -button[type="submit"] { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border: none; - padding: 0.75rem; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: transform 0.2s; +.modal-close:hover { + background: var(--bg-tertiary); + color: var(--accent-danger); + border-color: var(--accent-danger); } -button[type="submit"]:hover { - transform: translateY(-2px); +.modal-body { + padding: var(--space-lg); } -button[type="submit"]:disabled { - background: #ccc; - cursor: not-allowed; - transform: none; +.modal-footer { + padding: var(--space-md) var(--space-lg); + border-top: 1px solid var(--border-color); + text-align: center; } -/* Strategy management styles */ -.strategies-manager { - margin-top: 1rem; +.security-note { + font-size: 0.75rem; + color: var(--text-muted); } -.strategy-item { - border: 1px solid #e9ecef; - padding: 1.5rem; - margin-bottom: 1rem; - border-radius: 8px; - background: #f8f9fa; +/* Form Styles */ +.input-group { position: relative; + margin-bottom: var(--space-lg); } -.strategy-item h4 { - color: #667eea; - margin-bottom: 1rem; +.input-group label { display: flex; - justify-content: space-between; align-items: center; + gap: var(--space-xs); + font-size: 0.8rem; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: var(--space-sm); + text-transform: uppercase; + letter-spacing: 0.05em; } -.strategy-item .remove-strategy-btn { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.8rem; +.label-icon { + font-size: 0.9rem; } -.strategy-item .remove-strategy-btn:hover { - background: #c82333; +.input-group input { + width: 100%; + padding: var(--space-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 0.95rem; + transition: var(--transition-fast); } -.strategy-form { - display: grid; - gap: 1rem; - grid-template-columns: 1fr 1fr; +.input-group input::placeholder { + color: var(--text-muted); } -.strategy-form .full-width { - grid-column: 1 / -1; +.input-group input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1); } -.strategy-form label { +.input-hint { display: block; - margin-bottom: 0.25rem; - font-weight: 500; - color: #555; - font-size: 0.9rem; + margin-top: var(--space-xs); + font-size: 0.7rem; + color: var(--text-muted); } -.strategy-form input, .strategy-form textarea { - width: 100%; - padding: 0.5rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 0.9rem; +.input-glow { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 2px; + background: var(--gradient-primary); + transition: width var(--transition-base); } -.strategy-form input[type="checkbox"] { - width: auto; - margin-right: 0.5rem; +.input-group input:focus + .input-glow { + width: 100%; } -.strategy-controls { - display: flex; - gap: 1rem; - margin-top: 1.5rem; - flex-wrap: wrap; +.form-actions { + margin-top: var(--space-lg); } -.add-strategy-btn { - background: #28a745; - color: white; +.submit-btn { + width: 100%; + padding: var(--space-md) var(--space-lg); + background: var(--gradient-primary); border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; + border-radius: var(--radius-md); + color: var(--bg-primary); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.9rem; cursor: pointer; - font-size: 1rem; - transition: background 0.3s; -} - -.add-strategy-btn:hover { - background: #218838; + transition: var(--transition-base); + text-transform: uppercase; + letter-spacing: 0.1em; + position: relative; + overflow: hidden; } -.clear-btn { - background: #6c757d; - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background 0.3s; +.submit-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-glow); } -.clear-btn:hover { - background: #5a6268; +.submit-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; } -.selectors-container { - grid-column: 1 / -1; +.submit-btn.loading .btn-text { + opacity: 0; } -.selectors-list { - display: flex; - flex-direction: column; - gap: 0.5rem; +.submit-btn.loading .btn-loader { + display: block; } -.add-selector-btn { - background: #17a2b8; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; +.btn-loader { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top-color: var(--bg-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: translate(-50%, -50%) rotate(360deg); } +} + +.error-message { + margin-top: var(--space-md); + padding: var(--space-sm) var(--space-md); + background: rgba(255, 71, 87, 0.1); + border: 1px solid var(--accent-danger); + border-radius: var(--radius-sm); + color: var(--accent-danger); font-size: 0.85rem; - margin-top: 0.5rem; - width: fit-content; -} - -.add-selector-btn:hover { - background: #138496; + display: none; } -@media (max-width: 768px) { - .strategy-form { - grid-template-columns: 1fr; - } - - .strategy-controls { - flex-direction: column; - } +.error-message.show { + display: block; } -.strategy-input { - border: 1px solid #e9ecef; - padding: 1rem; - border-radius: 4px; - background: #f8f9fa; +/* Dashboard */ +.dashboard { + max-width: 1400px; + margin: 0 auto; + padding: var(--space-xl); + position: relative; + z-index: 2; } -.metrics-grid { - display: grid; - grid-template-columns: 1fr 2fr; - gap: 1rem; - align-items: center; +/* Quick Actions */ +.quick-actions { + display: flex; + gap: var(--space-sm); + margin-bottom: var(--space-xl); + flex-wrap: wrap; } -.selector { +.action-chip { display: flex; - gap: 0.5rem; - margin-bottom: 0.5rem; align-items: center; -} - -.selector input { - flex: 1; -} - -.selector button { - background: #dc3545; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + color: var(--text-secondary); + font-family: var(--font-mono); + font-size: 0.8rem; cursor: pointer; - font-size: 0.9rem; + transition: var(--transition-fast); } -.selector button:hover { - background: #c82333; +.action-chip:hover { + background: var(--bg-elevated); + border-color: var(--accent-primary); + color: var(--accent-primary); } -button[type="button"] { - background: #28a745; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - margin-top: 0.5rem; +.action-chip:disabled { + opacity: 0.5; + cursor: not-allowed; } -button[type="button"]:hover { - background: #218838; +/* Dashboard Grid */ +.dashboard-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-lg); } -.auth-required:disabled { - opacity: 0.6; - cursor: not-allowed; +@media (max-width: 1024px) { + .dashboard-grid { + grid-template-columns: 1fr; + } } -.success { - color: #28a745; - background: #d4edda; - border-color: #c3e6cb; +.full-width { + grid-column: 1 / -1; } -.error { - color: #721c24; - background: #f8d7da; - border-color: #f5c6cb; +/* Card Styles */ +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; + transition: var(--transition-base); } -.info { - color: #0c5460; - background: #d1ecf1; - border-color: #bee5eb; +.card:hover { + border-color: rgba(0, 255, 136, 0.3); + box-shadow: 0 0 30px rgba(0, 255, 136, 0.05); } -.metrics-info { - margin-top: 1rem; - padding: 1rem; - background: #e3f2fd; - border-radius: 4px; - border-left: 4px solid #667eea; +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md) var(--space-lg); + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); } -.metrics-info h3 { - margin-bottom: 0.5rem; - color: #4a5568; - font-size: 1.1rem; +.card-title { + display: flex; + align-items: center; + gap: var(--space-sm); } -.metrics-info ul { - list-style-type: none; - padding-left: 0; +.card-icon { + font-size: 1.2rem; } -.metrics-info li { - padding: 0.25rem 0; - color: #666; +.card-title h2 { + font-family: var(--font-display); font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-primary); } -.metrics-info strong { - color: #4a5568; - font-weight: 600; +.card-actions { + display: flex; + align-items: center; + gap: var(--space-sm); } -@media (max-width: 768px) { - header { - flex-direction: column; - gap: 1rem; - text-align: center; - } - - .metrics-grid { - grid-template-columns: 1fr; - } - - .modal-content { - width: 95%; - margin: 5% auto; - } - - .selector { - flex-direction: column; - } +.card-body { + padding: var(--space-lg); } -/* Control panel styles */ -.control-panel { +/* Icon Button */ +.icon-btn { + width: 36px; + height: 36px; display: flex; align-items: center; - gap: 20px; - margin-bottom: 15px; - flex-wrap: wrap; + justify-content: center; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 1rem; + cursor: pointer; + transition: var(--transition-fast); } -.interval-controls { - display: flex; - align-items: center; - gap: 10px; +.icon-btn:hover:not(:disabled) { + background: var(--bg-tertiary); + border-color: var(--accent-primary); + color: var(--accent-primary); } -.interval-controls label { - margin: 0; - font-weight: normal; +.icon-btn:disabled { + opacity: 0.4; + cursor: not-allowed; } -.interval-controls input[type="number"] { - width: 80px; - padding: 5px; - border: 1px solid #ddd; - border-radius: 4px; +/* Primary Button */ +.primary-btn { + display: flex; + align-items: center; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + background: var(--gradient-primary); + border: none; + border-radius: var(--radius-sm); + color: var(--bg-primary); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.8rem; + cursor: pointer; + transition: var(--transition-fast); } -/* Health status grid */ -.health-status-grid { - margin-bottom: 15px; +.primary-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: var(--shadow-glow); } -.status-grid { - display: flex; - gap: 5px; - margin: 10px 0; - flex-wrap: wrap; +.primary-btn:disabled { + opacity: 0.4; + cursor: not-allowed; } -.status-box { - width: 30px; - height: 30px; - border: 2px solid #ddd; - border-radius: 4px; +/* Success/Danger Buttons */ +.success-btn { display: flex; align-items: center; - justify-content: center; - font-size: 12px; - font-weight: bold; - color: white; - position: relative; - transition: all 0.3s ease; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + background: var(--accent-success); + border: none; + border-radius: var(--radius-sm); + color: var(--bg-primary); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: var(--transition-fast); } -.status-box.healthy { - background-color: #4caf50; - border-color: #388e3c; +.success-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 0 20px rgba(0, 255, 136, 0.3); } -.status-box.unhealthy { - background-color: #f44336; - border-color: #d32f2f; +.success-btn:disabled { + opacity: 0.4; + cursor: not-allowed; } -.status-box.loading { - background-color: #ff9800; - border-color: #f57c00; +.danger-btn { + display: flex; + align-items: center; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + background: transparent; + border: 1px solid var(--accent-danger); + border-radius: var(--radius-sm); + color: var(--accent-danger); + font-family: var(--font-mono); + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: var(--transition-fast); } -.grid-legend { - display: flex; - gap: 15px; - font-size: 14px; - margin-top: 10px; +.danger-btn:hover { + background: rgba(255, 71, 87, 0.1); } -.legend-item { +/* Auto Refresh Toggle */ +.auto-refresh-toggle { display: flex; align-items: center; - gap: 5px; + gap: var(--space-xs); } -.legend-item .status-box { - width: 15px; - height: 15px; +.auto-refresh-toggle input[type="checkbox"] { + appearance: none; + width: 36px; + height: 20px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 10px; + cursor: pointer; + position: relative; + transition: var(--transition-fast); } + +.auto-refresh-toggle input[type="checkbox"]::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: var(--text-muted); + border-radius: 50%; + transition: var(--transition-fast); +} + +.auto-refresh-toggle input[type="checkbox"]:checked { + background: var(--accent-primary); + border-color: var(--accent-primary); +} + +.auto-refresh-toggle input[type="checkbox"]:checked::after { + transform: translateX(16px); + background: var(--bg-primary); +} + +.auto-refresh-toggle label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; +} + +/* Health Card Specific */ +.health-display { + display: flex; + gap: var(--space-xl); + align-items: center; + margin-bottom: var(--space-lg); +} + +.health-indicator { + flex-shrink: 0; +} + +.health-ring { + position: relative; + width: 100px; + height: 100px; +} + +.health-ring svg { + transform: rotate(-90deg); +} + +.ring-bg { + fill: none; + stroke: var(--bg-tertiary); + stroke-width: 8; +} + +.ring-progress { + fill: none; + stroke: var(--text-muted); + stroke-width: 8; + stroke-linecap: round; + stroke-dasharray: 283; + stroke-dashoffset: 283; + transition: stroke-dashoffset 0.5s ease, stroke 0.3s ease; +} + +.ring-progress.healthy { + stroke: var(--accent-success); + stroke-dashoffset: 0; +} + +.ring-progress.unhealthy { + stroke: var(--accent-danger); + stroke-dashoffset: 141; +} + +.health-status { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: var(--font-display); + font-size: 0.8rem; + font-weight: 600; + color: var(--text-muted); + text-align: center; +} + +.health-status.healthy { + color: var(--accent-success); +} + +.health-status.unhealthy { + color: var(--accent-danger); +} + +.health-history { + flex: 1; +} + +.history-label { + display: block; + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: var(--space-sm); +} + +.history-grid { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.history-dot { + width: 24px; + height: 24px; + border-radius: var(--radius-sm); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7rem; + color: var(--text-muted); + transition: var(--transition-fast); +} + +.history-dot.healthy { + background: rgba(0, 255, 136, 0.2); + border-color: var(--accent-success); + color: var(--accent-success); +} + +.history-dot.unhealthy { + background: rgba(255, 71, 87, 0.2); + border-color: var(--accent-danger); + color: var(--accent-danger); +} + +/* Code Block */ +.code-block { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: var(--space-md); + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-secondary); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 300px; + overflow-y: auto; +} + +.code-block.success { + border-color: var(--accent-success); + background: rgba(0, 255, 136, 0.05); +} + +.code-block.error { + border-color: var(--accent-danger); + background: rgba(255, 71, 87, 0.05); +} + +/* Metrics Grid */ +.metrics-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.metric-item { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: var(--space-md); + text-align: center; + transition: var(--transition-fast); +} + +.metric-item:hover { + border-color: var(--accent-secondary); + box-shadow: 0 0 15px rgba(0, 212, 255, 0.1); +} + +.metric-item.accent { + border-color: rgba(0, 255, 136, 0.3); + background: rgba(0, 255, 136, 0.05); +} + +.metric-value { + display: block; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: var(--space-xs); +} + +.metric-label { + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Strategies Container */ +.strategies-container { + display: flex; + flex-direction: column; + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.strategy-item { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: var(--space-lg); + position: relative; + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.strategy-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border-color); +} + +.strategy-header h4 { + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 600; + color: var(--accent-secondary); +} + +.remove-strategy-btn { + padding: var(--space-xs) var(--space-sm); + background: transparent; + border: 1px solid var(--accent-danger); + border-radius: var(--radius-sm); + color: var(--accent-danger); + font-size: 0.75rem; + cursor: pointer; + transition: var(--transition-fast); +} + +.remove-strategy-btn:hover { + background: rgba(255, 71, 87, 0.1); +} + +.strategy-form { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-md); +} + +.strategy-form .full-width { + grid-column: 1 / -1; +} + +.strategy-form label { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: var(--space-xs); + text-transform: uppercase; +} + +.strategy-form input[type="text"], +.strategy-form input[type="number"] { + width: 100%; + padding: var(--space-sm); + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 0.85rem; +} + +.strategy-form input:focus { + outline: none; + border-color: var(--accent-secondary); +} + +.checkbox-group { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.checkbox-group input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + cursor: pointer; + position: relative; +} + +.checkbox-group input[type="checkbox"]:checked { + background: var(--accent-primary); + border-color: var(--accent-primary); +} + +.checkbox-group input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--bg-primary); + font-size: 0.7rem; + font-weight: bold; +} + +.checkbox-group label { + margin: 0; + cursor: pointer; +} + +/* Selectors */ +.selectors-container { + margin-top: var(--space-sm); +} + +.selectors-list { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.selector-row { + display: flex; + gap: var(--space-sm); + align-items: center; +} + +.selector-row input { + flex: 1; + padding: var(--space-sm); + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 0.8rem; +} + +.selector-row input:focus { + outline: none; + border-color: var(--accent-secondary); +} + +.selector-row button { + padding: var(--space-xs) var(--space-sm); + background: transparent; + border: 1px solid var(--accent-danger); + border-radius: var(--radius-sm); + color: var(--accent-danger); + font-size: 0.7rem; + cursor: pointer; + transition: var(--transition-fast); +} + +.selector-row button:hover { + background: rgba(255, 71, 87, 0.1); +} + +.add-selector-btn { + margin-top: var(--space-sm); + padding: var(--space-xs) var(--space-sm); + background: transparent; + border: 1px dashed var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-muted); + font-size: 0.75rem; + cursor: pointer; + transition: var(--transition-fast); +} + +.add-selector-btn:hover { + border-color: var(--accent-secondary); + color: var(--accent-secondary); +} + +/* Strategies Actions */ +.strategies-actions { + display: flex; + justify-content: flex-end; + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +/* Footer */ +.main-footer { + padding: var(--space-lg); + text-align: center; + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +.footer-content { + display: flex; + justify-content: center; + align-items: center; + gap: var(--space-md); + font-size: 0.75rem; + color: var(--text-muted); +} + +.separator { + color: var(--border-color); +} + +.github-link { + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition-fast); +} + +.github-link:hover { + color: var(--accent-primary); +} + +/* Toast Notifications */ +.toast-container { + position: fixed; + bottom: var(--space-xl); + right: var(--space-xl); + display: flex; + flex-direction: column; + gap: var(--space-sm); + z-index: 2000; +} + +.toast { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-md) var(--space-lg); + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + animation: toastIn 0.3s ease; + min-width: 280px; +} + +@keyframes toastIn { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.toast.success { + border-color: var(--accent-success); +} + +.toast.error { + border-color: var(--accent-danger); +} + +.toast.info { + border-color: var(--accent-secondary); +} + +.toast-icon { + font-size: 1.2rem; +} + +.toast-message { + flex: 1; + font-size: 0.85rem; + color: var(--text-primary); +} + +.toast-close { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1rem; + padding: 0; +} + +.toast-close:hover { + color: var(--text-primary); +} + +/* Responsive */ +@media (max-width: 768px) { + .main-header { + padding: var(--space-md); + } + + .header-content { + flex-direction: column; + gap: var(--space-md); + } + + .logo-text h1 { + font-size: 1.2rem; + } + + .auth-section { + width: 100%; + justify-content: space-between; + } + + .dashboard { + padding: var(--space-md); + } + + .health-display { + flex-direction: column; + align-items: flex-start; + } + + .metrics-grid { + grid-template-columns: repeat(2, 1fr); + } + + .strategy-form { + grid-template-columns: 1fr; + } + + .strategies-actions { + flex-direction: column; + } + + .toast-container { + left: var(--space-md); + right: var(--space-md); + bottom: var(--space-md); + } + + .toast { + min-width: unset; + } +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-tertiary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* Selection */ +::selection { + background: rgba(0, 255, 136, 0.3); + color: var(--text-primary); +} + +/* ============================================ + Pod-PID Mapping Card Styles + ============================================ */ + +.pods-card { + background: linear-gradient(145deg, var(--bg-card) 0%, rgba(0, 212, 255, 0.05) 100%); + border-color: rgba(0, 212, 255, 0.3); +} + +.pods-summary { + display: flex; + gap: var(--space-xl); + padding: var(--space-lg); + background: var(--bg-tertiary); + border-radius: var(--radius-md); + margin-bottom: var(--space-lg); + flex-wrap: wrap; +} + +.summary-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 80px; +} + +.summary-value { + font-family: var(--font-display); + font-size: 1.75rem; + font-weight: 700; + color: var(--accent-secondary); + text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); +} + +.summary-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: var(--space-xs); +} + +.pods-table-container { + overflow-x: auto; + margin-bottom: var(--space-lg); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); +} + +.pods-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.pods-table th, +.pods-table td { + padding: var(--space-sm) var(--space-md); + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.pods-table th { + background: var(--bg-tertiary); + color: var(--accent-secondary); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + position: sticky; + top: 0; + z-index: 1; +} + +.pods-table tbody tr:hover { + background: rgba(0, 212, 255, 0.05); +} + +.pods-table td { + color: var(--text-secondary); + font-family: var(--font-mono); +} + +.pods-table .pid-cell { + color: var(--accent-primary); + font-weight: 600; +} + +.pods-table .command-cell { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--accent-warning); +} + +.pods-table .empty-state { + text-align: center; + color: var(--text-muted); + padding: var(--space-xl); + font-style: italic; +} + +.pods-table .empty-state.error { + color: var(--accent-danger); +} + +.pods-result details { + margin-top: var(--space-md); +} + +.pods-result summary { + cursor: pointer; + color: var(--text-muted); + font-size: 0.85rem; + padding: var(--space-sm); + user-select: none; +} + +.pods-result summary:hover { + color: var(--text-secondary); +} + +.pods-result details[open] summary { + margin-bottom: var(--space-sm); + color: var(--accent-secondary); +} + +/* Responsive adjustments for pods table */ +@media (max-width: 768px) { + .pods-summary { + gap: var(--space-md); + justify-content: space-around; + } + + .summary-value { + font-size: 1.25rem; + } + + .pods-table { + font-size: 0.75rem; + } + + .pods-table th, + .pods-table td { + padding: var(--space-xs) var(--space-sm); + } + + .pods-table .command-cell { + max-width: 100px; + } +} +/* ============================================ + User Profile Card - Enhanced Design + ============================================ */ + +.user-profile-display { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-lg); + padding: var(--space-lg); + background: linear-gradient(135deg, rgba(0, 255, 136, 0.03) 0%, rgba(0, 212, 255, 0.03) 100%); + border-radius: var(--radius-lg); + border: 1px solid rgba(0, 255, 136, 0.1); +} + +.user-avatar { + position: relative; +} + +.avatar-circle { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-display); + font-size: 2rem; + font-weight: 700; + color: var(--bg-primary); + box-shadow: 0 0 30px rgba(0, 255, 136, 0.3); + animation: avatarPulse 3s ease-in-out infinite; +} + +@keyframes avatarPulse { + 0%, 100% { box-shadow: 0 0 20px rgba(0, 255, 136, 0.3); } + 50% { box-shadow: 0 0 40px rgba(0, 255, 136, 0.5); } +} + +.user-status-indicator { + position: absolute; + bottom: 4px; + right: 4px; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-muted); + border: 3px solid var(--bg-card); + transition: background var(--transition-base); +} + +.user-status-indicator.online { + background: var(--accent-success); + box-shadow: 0 0 10px var(--accent-success); +} + +.user-main-info { + text-align: center; +} + +.user-name { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: var(--space-xs); + text-shadow: 0 0 20px rgba(0, 255, 136, 0.2); +} + +.user-email-display { + font-size: 0.85rem; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.user-meta-grid { + display: flex; + gap: var(--space-xl); + padding-top: var(--space-md); + border-top: 1px solid var(--border-color); + width: 100%; + justify-content: center; +} + +.user-meta-item { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.user-meta-item .meta-icon { + font-size: 1.2rem; +} + +.user-meta-item .meta-content { + display: flex; + flex-direction: column; +} + +.user-meta-item .meta-label { + font-size: 0.65rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.user-meta-item .meta-value { + font-size: 0.9rem; + font-weight: 600; + color: var(--accent-secondary); +} + +.user-raw-details { + margin-top: var(--space-md); +} + +.user-raw-details summary { + cursor: pointer; + font-size: 0.8rem; + color: var(--text-muted); + padding: var(--space-sm); +} + +.user-raw-details summary:hover { + color: var(--text-secondary); +} + +/* ============================================ + Scheduling Strategies Card - Enhanced + ============================================ */ + +.strategies-card .card-title h2 { + font-size: 1.1rem; + color: var(--accent-warning); + text-shadow: 0 0 10px rgba(255, 184, 0, 0.3); +} + +.strategy-item { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + margin-bottom: var(--space-md); + overflow: hidden; +} + +.strategy-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-md); + background: var(--bg-elevated); + border-bottom: 1px solid var(--border-color); +} + +.strategy-header h4 { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + color: var(--accent-warning); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.strategy-form { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-md); + padding: var(--space-lg); +} + +.strategy-form label { + display: block; + font-size: 0.8rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.03em; + margin-bottom: var(--space-xs); +} + +.strategy-form input { + width: 100%; + padding: var(--space-sm) var(--space-md); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.strategy-form input:focus { + outline: none; + border-color: var(--accent-warning); + box-shadow: 0 0 0 3px rgba(255, 184, 0, 0.1); +} + +.remove-strategy-btn { + padding: var(--space-xs) var(--space-sm); + background: transparent; + border: 1px solid var(--accent-danger); + border-radius: var(--radius-sm); + color: var(--accent-danger); + font-size: 0.75rem; + cursor: pointer; + transition: var(--transition-fast); +} + +.remove-strategy-btn:hover { + background: rgba(255, 71, 87, 0.1); +} + +/* ============================================ + Pod-PID Mapping - Accordion Design + ============================================ */ + +.pods-accordion { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.pods-empty-state { + text-align: center; + color: var(--text-muted); + padding: var(--space-xl); + font-style: italic; +} + +.pod-accordion-item { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + overflow: hidden; + transition: border-color var(--transition-fast); +} + +.pod-accordion-item:hover { + border-color: rgba(0, 212, 255, 0.3); +} + +.pod-accordion-item.expanded { + border-color: var(--accent-secondary); +} + +.pod-accordion-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-md) var(--space-lg); + background: var(--bg-elevated); + cursor: pointer; + transition: background var(--transition-fast); + user-select: none; +} + +.pod-accordion-header:hover { + background: rgba(0, 212, 255, 0.05); +} + +.pod-accordion-info { + display: flex; + align-items: center; + gap: var(--space-md); + flex: 1; + min-width: 0; +} + +.pod-accordion-toggle { + font-size: 0.8rem; + color: var(--accent-secondary); + transition: transform var(--transition-fast); + flex-shrink: 0; +} + +.pod-accordion-item.expanded .pod-accordion-toggle { + transform: rotate(90deg); +} + +.pod-accordion-title { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 0; +} + +.pod-uid-full { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--accent-secondary); + word-break: break-all; + line-height: 1.3; +} + +.pod-accordion-meta { + display: flex; + align-items: center; + gap: var(--space-md); + flex-shrink: 0; +} + +.process-count-badge { + font-size: 0.75rem; + padding: var(--space-xs) var(--space-sm); + background: rgba(255, 184, 0, 0.15); + border: 1px solid rgba(255, 184, 0, 0.3); + border-radius: var(--radius-sm); + color: var(--accent-warning); + font-weight: 600; +} + +.pod-header-left { + display: flex; + align-items: center; + gap: var(--space-md); + flex: 1; + min-width: 0; +} + +.pod-expand-icon { + font-size: 0.8rem; + color: var(--accent-secondary); + transition: transform var(--transition-fast); + flex-shrink: 0; +} + +.pod-accordion-item.expanded .pod-expand-icon { + transform: rotate(90deg); +} + +.pod-uid-display { + font-family: var(--font-mono); + font-size: 0.85rem; + color: var(--accent-secondary); + word-break: break-all; +} + +.pod-id-badge { + font-size: 0.75rem; + padding: var(--space-xs) var(--space-sm); + background: rgba(0, 255, 136, 0.1); + border: 1px solid rgba(0, 255, 136, 0.2); + border-radius: var(--radius-sm); + color: var(--accent-primary); + white-space: nowrap; +} + +.pod-process-count { + display: flex; + align-items: center; + gap: var(--space-xs); + font-size: 0.8rem; + color: var(--text-muted); + flex-shrink: 0; +} + +.pod-process-count .count-badge { + font-family: var(--font-display); + font-weight: 700; + color: var(--accent-warning); +} + +.pod-accordion-content { + display: none; + padding: 0; + background: var(--bg-secondary); +} + +.pod-accordion-item.expanded .pod-accordion-content { + display: block; +} + +.pod-processes-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8rem; +} + +.pod-processes-table th { + padding: var(--space-sm) var(--space-md); + background: var(--bg-tertiary); + color: var(--accent-secondary); + font-weight: 600; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.pod-processes-table td { + padding: var(--space-sm) var(--space-md); + color: var(--text-secondary); + font-family: var(--font-mono); + border-bottom: 1px solid rgba(255, 255, 255, 0.03); +} + +.pod-processes-table tr:hover td { + background: rgba(0, 212, 255, 0.03); +} + +.pod-processes-table .pid-value { + color: var(--accent-primary); + font-weight: 600; +} + +.pod-processes-table .command-value { + color: var(--accent-warning); + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pod-processes-table .ppid-value { + color: var(--text-muted); +} + +/* Node name in summary */ +#nodeNameDisplay { + color: var(--accent-primary) !important; + font-size: 1.2rem !important; +} \ No newline at end of file