mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Feat: Implement frontend charting, alert handling, and UI optimizations
This commit is contained in:
@@ -16,9 +16,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
PulseApp.ui.pbs?.updatePbsInfo(pbsDataArray);
|
||||
PulseApp.ui.backups?.updateBackupsTab();
|
||||
|
||||
// Update tab availability based on PBS data
|
||||
PulseApp.ui.tabs?.updateTabAvailability();
|
||||
|
||||
updateLoadingOverlayVisibility(); // Call the helper function
|
||||
|
||||
PulseApp.thresholds?.logging?.checkThresholdViolations();
|
||||
|
||||
// Update alerts when state changes
|
||||
const state = PulseApp.state.getFullState();
|
||||
PulseApp.alerts?.updateAlertsFromState?.(state);
|
||||
}
|
||||
|
||||
function updateLoadingOverlayVisibility() {
|
||||
@@ -52,6 +59,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// If socketHandler.init only expects one, this might need adjustment in socketHandler.js
|
||||
PulseApp.socketHandler?.init?.(updateAllUITables, updateLoadingOverlayVisibility);
|
||||
PulseApp.tooltips?.init?.();
|
||||
PulseApp.alerts?.init?.();
|
||||
|
||||
PulseApp.ui = PulseApp.ui || {};
|
||||
PulseApp.ui.tabs?.init?.();
|
||||
@@ -64,7 +72,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
PulseApp.ui.common?.init?.();
|
||||
|
||||
PulseApp.thresholds = PulseApp.thresholds || {};
|
||||
PulseApp.thresholds.logging?.init?.();
|
||||
}
|
||||
|
||||
function validateCriticalElements() {
|
||||
|
||||
+353
-117
@@ -1,156 +1,392 @@
|
||||
PulseApp.socketHandler = (() => {
|
||||
let socket = null;
|
||||
let uiUpdateCallback = () => { console.warn('[socketHandler] uiUpdateCallback not assigned.'); };
|
||||
let loadingOverlayCallback = () => { console.warn('[socketHandler] loadingOverlayCallback not assigned.'); };
|
||||
let isConnected = false;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 10;
|
||||
const reconnectDelay = 2000; // 2 seconds
|
||||
|
||||
function init(updateFunctionRef, overlayUpdateRef) {
|
||||
socket = io();
|
||||
uiUpdateCallback = updateFunctionRef;
|
||||
loadingOverlayCallback = overlayUpdateRef;
|
||||
|
||||
socket.on('connect', handleConnect);
|
||||
socket.on('disconnect', handleDisconnect);
|
||||
socket.on('initialState', handleInitialState);
|
||||
socket.on('rawData', handleRawData);
|
||||
socket.on('pbsInitialStatus', handlePbsInitialStatus);
|
||||
socket.on('hotReload', () => {
|
||||
console.log('[socketHandler] Hot reload requested. Reloading page...');
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// Optional: for debugging all events
|
||||
// socket.onAny((eventName, ...args) => {
|
||||
// console.log(`[Socket Event Debug] Event: ${eventName}`, args);
|
||||
// });
|
||||
function init() {
|
||||
console.log('[Socket] Initializing socket connection...');
|
||||
createSocket();
|
||||
}
|
||||
|
||||
function updateConnectionStatusUI(isConnected) {
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
if (!connectionStatus) return;
|
||||
function createSocket() {
|
||||
if (socket) {
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
}
|
||||
|
||||
const statusText = isConnected ? 'Connected' : 'Disconnected';
|
||||
const addClasses = isConnected
|
||||
? ['connected', 'bg-green-100', 'dark:bg-green-800/30', 'text-green-700', 'dark:text-green-300']
|
||||
: ['disconnected', 'bg-red-100', 'dark:bg-red-800/30', 'text-red-700', 'dark:text-red-300'];
|
||||
const removeClasses = isConnected
|
||||
? ['disconnected', 'bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400', 'bg-red-100', 'dark:bg-red-800/30', 'text-red-700', 'dark:text-red-300']
|
||||
: ['connected', 'bg-green-100', 'dark:bg-green-800/30', 'text-green-700', 'dark:text-green-300', 'bg-gray-200', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400'];
|
||||
socket = io();
|
||||
window.socket = socket; // Make socket available globally for alerts
|
||||
|
||||
connectionStatus.textContent = statusText;
|
||||
connectionStatus.classList.remove(...removeClasses);
|
||||
connectionStatus.classList.add(...addClasses);
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
socket.on('connect', handleConnect);
|
||||
socket.on('disconnect', handleDisconnect);
|
||||
socket.on('rawData', handleRawData);
|
||||
socket.on('initialState', handleInitialState);
|
||||
socket.on('requestError', handleRequestError);
|
||||
|
||||
// Enhanced monitoring events
|
||||
socket.on('alert', handleAlert);
|
||||
socket.on('alertResolved', handleAlertResolved);
|
||||
|
||||
// Development features
|
||||
socket.on('hotReload', handleHotReload);
|
||||
|
||||
// Handle connection errors
|
||||
socket.on('connect_error', handleConnectError);
|
||||
socket.on('reconnect', handleReconnect);
|
||||
socket.on('reconnect_error', handleReconnectError);
|
||||
socket.on('reconnect_failed', handleReconnectFailed);
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
updateConnectionStatusUI(true);
|
||||
PulseApp.state.set('wasConnected', true);
|
||||
requestFullData(); // Request data on new connection or reconnection
|
||||
if (typeof loadingOverlayCallback === 'function') {
|
||||
loadingOverlayCallback(); // Update overlay based on current state
|
||||
}
|
||||
console.log('[Socket] Connected to server');
|
||||
isConnected = true;
|
||||
reconnectAttempts = 0;
|
||||
updateConnectionStatus('connected');
|
||||
|
||||
// Request initial data
|
||||
socket.emit('requestData');
|
||||
}
|
||||
|
||||
function handleDisconnect(reason) {
|
||||
updateConnectionStatusUI(false);
|
||||
PulseApp.state.set('wasConnected', false);
|
||||
if (typeof loadingOverlayCallback === 'function') {
|
||||
loadingOverlayCallback(); // This should show the overlay with "Connection lost"
|
||||
}
|
||||
}
|
||||
|
||||
function handleInitialState(state) {
|
||||
console.log('[socketHandler] Received initial state:', state);
|
||||
const isPlaceholder = state.isConfigPlaceholder || false;
|
||||
PulseApp.state.set('isConfigPlaceholder', isPlaceholder);
|
||||
PulseApp.state.set('initialDataReceived', false); // Mark that full data hasn't arrived yet
|
||||
|
||||
const statusText = document.getElementById('dashboard-status-text');
|
||||
if (statusText) {
|
||||
statusText.textContent = isPlaceholder ? 'Configuration Required' : 'Loading initial data...';
|
||||
}
|
||||
console.log('[Socket] Disconnected from server:', reason);
|
||||
isConnected = false;
|
||||
updateConnectionStatus('disconnected');
|
||||
|
||||
if (typeof loadingOverlayCallback === 'function') {
|
||||
loadingOverlayCallback(); // Update overlay based on placeholder status and lack of data
|
||||
// If it's not a planned disconnect, try to reconnect
|
||||
if (reason !== 'io client disconnect') {
|
||||
attemptReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function handleRawData(jsonData) {
|
||||
function handleRawData(data) {
|
||||
try {
|
||||
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
||||
// Update main application state
|
||||
if (PulseApp.state) {
|
||||
PulseApp.state.updateState(data);
|
||||
}
|
||||
|
||||
PulseApp.state.set('isConfigPlaceholder', data.isConfigPlaceholder || false);
|
||||
PulseApp.state.set('nodesData', data.nodes || []);
|
||||
PulseApp.state.set('vmsData', data.vms || []);
|
||||
PulseApp.state.set('containersData', data.containers || []);
|
||||
PulseApp.state.set('metricsData', data.metrics || []); // Ensure metrics are always set
|
||||
PulseApp.state.set('pbsDataArray', Array.isArray(data.pbs) ? data.pbs : []);
|
||||
|
||||
if (PulseApp.ui?.tabs) {
|
||||
PulseApp.ui.tabs.updateTabAvailability();
|
||||
// Update alerts system with new state
|
||||
if (PulseApp.alerts && data.alerts) {
|
||||
PulseApp.alerts.updateAlertsFromState(data);
|
||||
}
|
||||
|
||||
// Set initialDataReceived to true only after successfully processing raw data
|
||||
PulseApp.state.set('initialDataReceived', true);
|
||||
|
||||
if (typeof loadingOverlayCallback === 'function') {
|
||||
loadingOverlayCallback(); // Hide overlay if not placeholder and data received
|
||||
}
|
||||
|
||||
if (typeof uiUpdateCallback === 'function') {
|
||||
uiUpdateCallback();
|
||||
} else {
|
||||
console.error('[socketHandler] uiUpdateCallback is not a function!');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error processing received rawData:', e, jsonData);
|
||||
|
||||
// Process UI updates based on tab
|
||||
updateUIFromData(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error processing raw data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePbsInitialStatus(pbsStatusArray) {
|
||||
if (Array.isArray(pbsStatusArray)) {
|
||||
const initialPbsData = pbsStatusArray.map(statusInfo => ({
|
||||
...statusInfo, // Spread existing status info
|
||||
// Initialize other fields if they might be missing from statusInfo
|
||||
backupTasks: statusInfo.backupTasks || { recentTasks: [], summary: {} },
|
||||
datastores: statusInfo.datastores || [],
|
||||
verificationTasks: statusInfo.verificationTasks || { summary: {} },
|
||||
syncTasks: statusInfo.syncTasks || { summary: {} },
|
||||
pruneTasks: statusInfo.pruneTasks || { summary: {} },
|
||||
nodeName: statusInfo.nodeName || null
|
||||
}));
|
||||
PulseApp.state.set('pbsDataArray', initialPbsData);
|
||||
// Don't set initialDataReceived here; wait for full rawData
|
||||
function handleInitialState(data) {
|
||||
console.log('[Socket] Received initial state:', data);
|
||||
|
||||
try {
|
||||
if (PulseApp.state) {
|
||||
PulseApp.state.updateState(data);
|
||||
}
|
||||
|
||||
updateUIFromData(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error processing initial state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (PulseApp.ui?.pbs) {
|
||||
PulseApp.ui.pbs.updatePbsInfo(initialPbsData);
|
||||
}
|
||||
if (PulseApp.ui?.tabs) {
|
||||
PulseApp.ui.tabs.updateTabAvailability();
|
||||
}
|
||||
if (typeof loadingOverlayCallback === 'function') {
|
||||
loadingOverlayCallback(); // Update overlay, might still show "Loading..."
|
||||
}
|
||||
function handleRequestError(error) {
|
||||
console.error('[Socket] Request error:', error);
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
function handleAlert(alert) {
|
||||
console.log('[Socket] Received alert:', alert);
|
||||
|
||||
// Forward to alerts handler
|
||||
if (PulseApp.alerts) {
|
||||
// The alerts handler will be called directly from its socket listeners
|
||||
// This is just for any additional processing
|
||||
}
|
||||
}
|
||||
|
||||
function handleAlertResolved(alert) {
|
||||
console.log('[Socket] Alert resolved:', alert);
|
||||
|
||||
// Forward to alerts handler
|
||||
if (PulseApp.alerts) {
|
||||
// The alerts handler will be called directly from its socket listeners
|
||||
// This is just for any additional processing
|
||||
}
|
||||
}
|
||||
|
||||
function handleHotReload() {
|
||||
console.log('[Socket] Hot reload triggered');
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function handleConnectError(error) {
|
||||
console.error('[Socket] Connection error:', error);
|
||||
updateConnectionStatus('error');
|
||||
}
|
||||
|
||||
function handleReconnect() {
|
||||
console.log('[Socket] Reconnected successfully');
|
||||
reconnectAttempts = 0;
|
||||
updateConnectionStatus('connected');
|
||||
}
|
||||
|
||||
function handleReconnectError(error) {
|
||||
console.error('[Socket] Reconnection error:', error);
|
||||
reconnectAttempts++;
|
||||
updateConnectionStatus('reconnecting');
|
||||
}
|
||||
|
||||
function handleReconnectFailed() {
|
||||
console.error('[Socket] Reconnection failed - max attempts reached');
|
||||
updateConnectionStatus('failed');
|
||||
}
|
||||
|
||||
function attemptReconnect() {
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
updateConnectionStatus('reconnecting');
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`[Socket] Attempting to reconnect... (${reconnectAttempts}/${maxReconnectAttempts})`);
|
||||
socket.connect();
|
||||
}, reconnectDelay * reconnectAttempts); // Exponential backoff
|
||||
} else {
|
||||
console.warn('[socket] Received non-array data for pbsInitialStatus:', pbsStatusArray);
|
||||
updateConnectionStatus('failed');
|
||||
}
|
||||
}
|
||||
|
||||
function requestFullData() {
|
||||
console.log('Requesting full data reload from server...');
|
||||
if (socket && socket.connected) { // Check if socket is connected before emitting
|
||||
function updateConnectionStatus(status) {
|
||||
const statusElement = document.getElementById('connection-status');
|
||||
if (!statusElement) return;
|
||||
|
||||
// Clear previous classes
|
||||
statusElement.className = statusElement.className
|
||||
.replace(/\b(connected|disconnected|reconnecting|error|failed)\b/g, '')
|
||||
.trim();
|
||||
|
||||
let statusText, statusClass;
|
||||
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
statusText = 'Connected';
|
||||
statusClass = 'connected text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400';
|
||||
break;
|
||||
case 'disconnected':
|
||||
statusText = 'Disconnected';
|
||||
statusClass = 'disconnected text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
|
||||
break;
|
||||
case 'reconnecting':
|
||||
statusText = `Reconnecting... (${reconnectAttempts}/${maxReconnectAttempts})`;
|
||||
statusClass = 'reconnecting text-xs px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900 text-yellow-600 dark:text-yellow-400 animate-pulse';
|
||||
break;
|
||||
case 'error':
|
||||
statusText = 'Connection Error';
|
||||
statusClass = 'error text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400';
|
||||
break;
|
||||
case 'failed':
|
||||
statusText = 'Connection Failed';
|
||||
statusClass = 'failed text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400';
|
||||
break;
|
||||
default:
|
||||
statusText = 'Unknown';
|
||||
statusClass = 'text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
|
||||
statusElement.textContent = statusText;
|
||||
statusElement.className = statusClass;
|
||||
}
|
||||
|
||||
function updateUIFromData(data) {
|
||||
try {
|
||||
// Hide loading overlay when we receive data
|
||||
const loadingOverlay = document.getElementById('loading-overlay');
|
||||
if (loadingOverlay && (data.nodes || data.vms || data.containers || data.pbs)) {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update different UI sections based on current tab
|
||||
const activeTab = document.querySelector('.tab.active');
|
||||
if (!activeTab) return;
|
||||
|
||||
const tabName = activeTab.getAttribute('data-tab');
|
||||
|
||||
switch (tabName) {
|
||||
case 'main':
|
||||
updateMainTab(data);
|
||||
break;
|
||||
case 'storage':
|
||||
updateStorageTab(data);
|
||||
break;
|
||||
case 'pbs':
|
||||
updatePbsTab(data);
|
||||
break;
|
||||
case 'backups':
|
||||
updateBackupsTab(data);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update performance indicators if available
|
||||
updatePerformanceIndicators(data);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating UI from data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMainTab(data) {
|
||||
try {
|
||||
// Update node summary cards
|
||||
if (PulseApp.ui && PulseApp.ui.nodes && data.nodes) {
|
||||
PulseApp.ui.nodes.updateNodeSummaryCards(data.nodes);
|
||||
}
|
||||
|
||||
// Update main dashboard
|
||||
if (PulseApp.ui && PulseApp.ui.dashboard) {
|
||||
PulseApp.ui.dashboard.updateDashboardTable();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating main tab:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStorageTab(data) {
|
||||
try {
|
||||
if (PulseApp.ui && PulseApp.ui.storage && data.nodes) {
|
||||
PulseApp.ui.storage.updateStorageInfo();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating storage tab:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePbsTab(data) {
|
||||
try {
|
||||
if (PulseApp.ui && PulseApp.ui.pbs && data.pbs) {
|
||||
PulseApp.ui.pbs.updatePbsInfo(data.pbs);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating PBS tab:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBackupsTab(data) {
|
||||
try {
|
||||
if (PulseApp.ui && PulseApp.ui.backups && data.pbs) {
|
||||
PulseApp.ui.backups.updateBackupsTab();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating backups tab:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePerformanceIndicators(data) {
|
||||
try {
|
||||
// Update performance stats if available
|
||||
if (data.stats) {
|
||||
updateStatsDisplay(data.stats);
|
||||
}
|
||||
|
||||
// Update any health indicators
|
||||
if (data.performance) {
|
||||
updateHealthDisplay(data.performance);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating performance indicators:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatsDisplay(stats) {
|
||||
// Update various stats in the UI
|
||||
try {
|
||||
const statusText = document.getElementById('dashboard-status-text');
|
||||
if (statusText && stats.totalGuests !== undefined) {
|
||||
const runningText = stats.runningGuests ? `${stats.runningGuests} running` : '0 running';
|
||||
const stoppedText = stats.stoppedGuests ? `${stats.stoppedGuests} stopped` : '0 stopped';
|
||||
statusText.textContent = `${stats.totalGuests} total guests (${runningText}, ${stoppedText})`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating stats display:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateHealthDisplay(performance) {
|
||||
// Update health indicators in the UI
|
||||
try {
|
||||
// Could add health indicators to the header or other parts of the UI
|
||||
// For now, this is just a placeholder for future enhancements
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Socket] Error updating health display:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Manual data request
|
||||
function requestData() {
|
||||
if (socket && isConnected) {
|
||||
socket.emit('requestData');
|
||||
} else {
|
||||
console.error('Cannot request data: socket not initialized or not connected.');
|
||||
console.warn('[Socket] Cannot request data - not connected');
|
||||
}
|
||||
}
|
||||
|
||||
function isConnected() {
|
||||
return socket && socket.connected;
|
||||
// Get connection status
|
||||
function getConnectionStatus() {
|
||||
return {
|
||||
connected: isConnected,
|
||||
reconnectAttempts,
|
||||
socket: socket ? socket.id : null
|
||||
};
|
||||
}
|
||||
|
||||
// Manual reconnect
|
||||
function reconnect() {
|
||||
if (socket) {
|
||||
reconnectAttempts = 0;
|
||||
socket.disconnect();
|
||||
socket.connect();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function destroy() {
|
||||
if (socket) {
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
isConnected = false;
|
||||
window.socket = null;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
requestFullData,
|
||||
isConnected
|
||||
requestData,
|
||||
getConnectionStatus,
|
||||
reconnect,
|
||||
destroy
|
||||
};
|
||||
})();
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', PulseApp.socketHandler.init);
|
||||
} else {
|
||||
PulseApp.socketHandler.init();
|
||||
}
|
||||
|
||||
|
||||
+134
-28
@@ -19,8 +19,39 @@ PulseApp.state = (() => {
|
||||
backupsFilterHealth: savedFilterState.backupsFilterHealth || 'all',
|
||||
backupsFilterGuestType: savedFilterState.backupsFilterGuestType || 'all',
|
||||
backupsSearchTerm: '',
|
||||
|
||||
// Enhanced monitoring data
|
||||
alerts: {
|
||||
active: [],
|
||||
stats: {},
|
||||
rules: []
|
||||
},
|
||||
performance: {
|
||||
lastDiscoveryTime: null,
|
||||
lastMetricsTime: null,
|
||||
discoveryDuration: 0,
|
||||
metricsDuration: 0,
|
||||
errorCount: 0,
|
||||
successCount: 0,
|
||||
avgResponseTime: 0,
|
||||
peakMemoryUsage: 0
|
||||
},
|
||||
stats: {
|
||||
totalGuests: 0,
|
||||
runningGuests: 0,
|
||||
stoppedGuests: 0,
|
||||
totalNodes: 0,
|
||||
healthyNodes: 0,
|
||||
warningNodes: 0,
|
||||
errorNodes: 0,
|
||||
avgCpuUsage: 0,
|
||||
avgMemoryUsage: 0,
|
||||
avgDiskUsage: 0,
|
||||
lastUpdated: null
|
||||
},
|
||||
isConfigPlaceholder: false,
|
||||
|
||||
sortState: {
|
||||
nodes: { column: null, direction: 'asc', ...(savedSortState.nodes || {}) },
|
||||
main: { column: 'id', direction: 'asc', ...(savedSortState.main || {}) },
|
||||
backups: { column: 'latestBackupTime', direction: 'desc', ...(savedSortState.backups || {}) }
|
||||
},
|
||||
@@ -32,34 +63,28 @@ PulseApp.state = (() => {
|
||||
diskwrite:{ value: 0 },
|
||||
netin: { value: 0 },
|
||||
netout: { value: 0 }
|
||||
},
|
||||
activeLogSessions: {},
|
||||
thresholdLogEntries: [],
|
||||
activeLoggingThresholds: null
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize thresholdState by merging saved state with defaults
|
||||
Object.keys(internalState.thresholdState).forEach(type => {
|
||||
const savedTypeState = savedThresholdState[type] || {};
|
||||
if (internalState.thresholdState[type].hasOwnProperty('operator')) { // Assuming this structure means it's the advanced threshold type
|
||||
if (internalState.thresholdState[type].hasOwnProperty('operator')) {
|
||||
internalState.thresholdState[type] = {
|
||||
operator: savedTypeState.operator || '>=',
|
||||
input: savedTypeState.input || '',
|
||||
// Preserve any other default properties if they exist
|
||||
...internalState.thresholdState[type],
|
||||
...savedTypeState // This ensures saved values overwrite defaults but keeps other default props
|
||||
...savedTypeState
|
||||
};
|
||||
} else { // Simple value threshold
|
||||
} else {
|
||||
internalState.thresholdState[type] = {
|
||||
value: savedTypeState.value || 0,
|
||||
// Preserve any other default properties
|
||||
...internalState.thresholdState[type],
|
||||
...savedTypeState
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function saveFilterState() {
|
||||
const stateToSave = {
|
||||
groupByNode: internalState.groupByNode,
|
||||
@@ -74,13 +99,94 @@ PulseApp.state = (() => {
|
||||
|
||||
function saveSortState() {
|
||||
const stateToSave = {
|
||||
nodes: internalState.sortState.nodes,
|
||||
main: internalState.sortState.main,
|
||||
backups: internalState.sortState.backups
|
||||
};
|
||||
localStorage.setItem('pulseSortState', JSON.stringify(stateToSave));
|
||||
}
|
||||
|
||||
function updateState(newData) {
|
||||
try {
|
||||
console.log('[State] Updating state with new data:', Object.keys(newData));
|
||||
|
||||
// Update core data arrays
|
||||
if (newData.nodes) internalState.nodesData = newData.nodes;
|
||||
if (newData.vms) internalState.vmsData = newData.vms;
|
||||
if (newData.containers) internalState.containersData = newData.containers;
|
||||
if (newData.metrics) internalState.metricsData = newData.metrics;
|
||||
if (newData.pbs) internalState.pbsDataArray = newData.pbs;
|
||||
|
||||
// Update enhanced monitoring data
|
||||
if (newData.alerts) {
|
||||
internalState.alerts = {
|
||||
active: newData.alerts.active || [],
|
||||
stats: newData.alerts.stats || {},
|
||||
rules: newData.alerts.rules || []
|
||||
};
|
||||
}
|
||||
|
||||
if (newData.performance) {
|
||||
internalState.performance = { ...internalState.performance, ...newData.performance };
|
||||
}
|
||||
|
||||
if (newData.stats) {
|
||||
internalState.stats = { ...internalState.stats, ...newData.stats };
|
||||
}
|
||||
|
||||
// Update configuration status
|
||||
if (newData.hasOwnProperty('isConfigPlaceholder')) {
|
||||
internalState.isConfigPlaceholder = newData.isConfigPlaceholder;
|
||||
}
|
||||
|
||||
// Combine VMs and containers for dashboard
|
||||
internalState.dashboardData = [...internalState.vmsData, ...internalState.containersData];
|
||||
|
||||
// Mark that we've received initial data
|
||||
if (!internalState.initialDataReceived && internalState.dashboardData.length > 0) {
|
||||
internalState.initialDataReceived = true;
|
||||
console.log('[State] Initial data received and processed');
|
||||
}
|
||||
|
||||
// Update dashboard history for charts
|
||||
updateDashboardHistoryFromMetrics();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[State] Error updating state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDashboardHistoryFromMetrics() {
|
||||
try {
|
||||
internalState.metricsData.forEach(metric => {
|
||||
if (metric && metric.current) {
|
||||
const guestId = `${metric.endpointId}-${metric.node}-${metric.id}`;
|
||||
const dataPoint = {
|
||||
timestamp: Date.now(),
|
||||
cpu: metric.current.cpu * 100 || 0,
|
||||
memory: metric.current.mem || 0,
|
||||
disk: metric.current.disk || 0,
|
||||
diskread: metric.current.diskread || 0,
|
||||
diskwrite: metric.current.diskwrite || 0,
|
||||
netin: metric.current.netin || 0,
|
||||
netout: metric.current.netout || 0
|
||||
};
|
||||
|
||||
if (!internalState.dashboardHistory[guestId] || !Array.isArray(internalState.dashboardHistory[guestId])) {
|
||||
internalState.dashboardHistory[guestId] = [];
|
||||
}
|
||||
|
||||
const history = internalState.dashboardHistory[guestId];
|
||||
history.push(dataPoint);
|
||||
if (history.length > PulseApp.config.AVERAGING_WINDOW_SIZE) {
|
||||
history.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[State] Error updating dashboard history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get: (key) => internalState[key],
|
||||
set: (key, value) => {
|
||||
@@ -89,6 +195,11 @@ PulseApp.state = (() => {
|
||||
saveFilterState();
|
||||
}
|
||||
},
|
||||
|
||||
// Enhanced state management
|
||||
updateState,
|
||||
getFullState: () => ({ ...internalState }),
|
||||
|
||||
setSortState: (tableType, column, direction) => {
|
||||
if (internalState.sortState[tableType]) {
|
||||
internalState.sortState[tableType] = { column, direction };
|
||||
@@ -108,21 +219,6 @@ PulseApp.state = (() => {
|
||||
console.warn(`Attempted to set threshold for unknown type: ${type}`);
|
||||
}
|
||||
},
|
||||
getActiveLogSession: (sessionId) => internalState.activeLogSessions[sessionId],
|
||||
getAllActiveLogSessions: () => internalState.activeLogSessions,
|
||||
addActiveLogSession: (sessionId, sessionData) => {
|
||||
internalState.activeLogSessions[sessionId] = sessionData;
|
||||
},
|
||||
removeActiveLogSession: (sessionId) => {
|
||||
delete internalState.activeLogSessions[sessionId];
|
||||
},
|
||||
addLogEntry: (sessionId, entry) => {
|
||||
if (internalState.activeLogSessions[sessionId]) {
|
||||
internalState.activeLogSessions[sessionId].entries.push(entry);
|
||||
} else {
|
||||
console.warn(`Attempted to add log entry to non-existent session: ${sessionId}`);
|
||||
}
|
||||
},
|
||||
getDashboardHistory: () => internalState.dashboardHistory,
|
||||
updateDashboardHistory: (guestId, dataPoint) => {
|
||||
if (!internalState.dashboardHistory[guestId] || !Array.isArray(internalState.dashboardHistory[guestId])) {
|
||||
@@ -134,6 +230,16 @@ PulseApp.state = (() => {
|
||||
},
|
||||
clearDashboardHistoryEntry: (guestId) => {
|
||||
delete internalState.dashboardHistory[guestId];
|
||||
}
|
||||
},
|
||||
forceRefreshDashboard: () => {
|
||||
if (PulseApp.socketHandler && typeof PulseApp.socketHandler.requestData === 'function') {
|
||||
PulseApp.socketHandler.requestData();
|
||||
}
|
||||
},
|
||||
|
||||
// Alert and performance data getters
|
||||
getAlerts: () => internalState.alerts,
|
||||
getPerformance: () => internalState.performance,
|
||||
getStats: () => internalState.stats
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -5,13 +5,15 @@ PulseApp.ui.tabs = (() => {
|
||||
let tabContents = [];
|
||||
let nestedTabsContainer = null;
|
||||
let nestedTabContentContainer = null;
|
||||
let mainTabsContainer = null;
|
||||
let logSessionArea = null;
|
||||
|
||||
function init() {
|
||||
tabs = document.querySelectorAll('.tab');
|
||||
tabs = Array.from(document.querySelectorAll('.tab'));
|
||||
tabContents = document.querySelectorAll('.tab-content');
|
||||
nestedTabsContainer = document.querySelector('.nested-tabs');
|
||||
nestedTabContentContainer = document.querySelector('#log-content-area');
|
||||
nestedTabContentContainer = document.getElementById('nested-tab-content-container');
|
||||
mainTabsContainer = document.getElementById('main-tabs-container');
|
||||
logSessionArea = document.getElementById('log-session-area');
|
||||
|
||||
// Initial styling pass for all tabs to ensure consistent look from the start
|
||||
@@ -350,8 +352,6 @@ PulseApp.ui.tabs = (() => {
|
||||
return {
|
||||
init,
|
||||
activateNestedTab,
|
||||
updateTabAvailability,
|
||||
addLogTab,
|
||||
removeLogTabAndContent
|
||||
updateTabAvailability
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -102,9 +102,27 @@ PulseApp.tooltips = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function showTooltip(event, content) {
|
||||
if (!tooltipElement) return;
|
||||
|
||||
tooltipElement.innerHTML = content;
|
||||
positionTooltip(event);
|
||||
tooltipElement.classList.remove('hidden', 'opacity-0');
|
||||
tooltipElement.classList.add('opacity-100');
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
if (tooltipElement) {
|
||||
tooltipElement.classList.add('hidden', 'opacity-0');
|
||||
tooltipElement.classList.remove('opacity-100');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
updateSliderTooltip,
|
||||
hideSliderTooltip
|
||||
hideSliderTooltip,
|
||||
showTooltip,
|
||||
hideTooltip
|
||||
};
|
||||
})();
|
||||
+85
-56
@@ -57,33 +57,68 @@ PulseApp.ui.backups = (() => {
|
||||
)
|
||||
);
|
||||
|
||||
return { allGuests, initialDataReceived, allRecentBackupTasks, allSnapshots };
|
||||
}
|
||||
// Pre-index data by guest ID and type for performance
|
||||
const tasksByGuest = new Map();
|
||||
const snapshotsByGuest = new Map();
|
||||
|
||||
function _determineGuestBackupStatus(guest, allSnapshots, allRecentBackupTasks) {
|
||||
const guestId = String(guest.vmid);
|
||||
const guestTypePve = guest.type === 'qemu' ? 'vm' : 'ct';
|
||||
const now = new Date(); // Use Date object for easier day calculations
|
||||
now.setHours(0, 0, 0, 0); // Normalize to start of today
|
||||
allRecentBackupTasks.forEach(task => {
|
||||
const key = `${task.guestId}-${task.guestTypePbs}`;
|
||||
if (!tasksByGuest.has(key)) tasksByGuest.set(key, []);
|
||||
tasksByGuest.get(key).push(task);
|
||||
});
|
||||
|
||||
allSnapshots.forEach(snap => {
|
||||
const key = `${snap.backupVMID}-${snap.backupType}`;
|
||||
if (!snapshotsByGuest.has(key)) snapshotsByGuest.set(key, []);
|
||||
snapshotsByGuest.get(key).push(snap);
|
||||
});
|
||||
|
||||
// Pre-calculate day boundaries for 7-day analysis
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
const dayBoundaries = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const dayStart = new Date(now);
|
||||
dayStart.setDate(now.getDate() - i);
|
||||
const dayEnd = new Date(dayStart);
|
||||
dayEnd.setDate(dayStart.getDate() + 1);
|
||||
dayBoundaries.push({
|
||||
start: Math.floor(dayStart.getTime() / 1000),
|
||||
end: Math.floor(dayEnd.getTime() / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
const threeDaysAgo = Math.floor(new Date(now).setDate(now.getDate() - 3) / 1000);
|
||||
const sevenDaysAgoTimestamp = Math.floor(new Date(now).setDate(now.getDate() - 7) / 1000);
|
||||
const sevenDaysAgo = Math.floor(new Date(now).setDate(now.getDate() - 7) / 1000);
|
||||
|
||||
const guestSnapshots = allSnapshots.filter(snap =>
|
||||
String(snap.backupVMID) === guestId && snap.backupType === guestTypePve
|
||||
);
|
||||
const totalBackups = guestSnapshots.length;
|
||||
const latestSnapshot = guestSnapshots.reduce((latest, snap) => {
|
||||
return (!latest || (snap['backup-time'] && snap['backup-time'] > latest['backup-time'])) ? snap : latest;
|
||||
}, null);
|
||||
return {
|
||||
allGuests,
|
||||
initialDataReceived,
|
||||
tasksByGuest,
|
||||
snapshotsByGuest,
|
||||
dayBoundaries,
|
||||
threeDaysAgo,
|
||||
sevenDaysAgo
|
||||
};
|
||||
}
|
||||
|
||||
function _determineGuestBackupStatus(guest, guestSnapshots, guestTasks, dayBoundaries, threeDaysAgo, sevenDaysAgo) {
|
||||
const guestId = String(guest.vmid);
|
||||
|
||||
// Use pre-filtered data instead of filtering large arrays
|
||||
const totalBackups = guestSnapshots ? guestSnapshots.length : 0;
|
||||
const latestSnapshot = guestSnapshots && guestSnapshots.length > 0
|
||||
? guestSnapshots.reduce((latest, snap) => {
|
||||
return (!latest || (snap['backup-time'] && snap['backup-time'] > latest['backup-time'])) ? snap : latest;
|
||||
}, null)
|
||||
: null;
|
||||
const latestSnapshotTime = latestSnapshot ? latestSnapshot['backup-time'] : null;
|
||||
|
||||
const guestTasks = allRecentBackupTasks.filter(task =>
|
||||
task.guestId === guestId && task.guestTypePbs === guestTypePve
|
||||
);
|
||||
const latestTask = guestTasks.reduce((latest, task) => {
|
||||
return (!latest || (task.startTime && task.startTime > latest.startTime)) ? task : latest;
|
||||
}, null);
|
||||
const latestTask = guestTasks && guestTasks.length > 0
|
||||
? guestTasks.reduce((latest, task) => {
|
||||
return (!latest || (task.startTime && task.startTime > latest.startTime)) ? task : latest;
|
||||
}, null)
|
||||
: null;
|
||||
|
||||
let healthStatus = 'none';
|
||||
let displayTimestamp = latestSnapshotTime;
|
||||
@@ -92,56 +127,50 @@ PulseApp.ui.backups = (() => {
|
||||
displayTimestamp = latestTask.startTime;
|
||||
if (latestTask.status === 'OK') {
|
||||
if (latestTask.startTime >= threeDaysAgo) healthStatus = 'ok';
|
||||
else if (latestTask.startTime >= sevenDaysAgoTimestamp) healthStatus = 'stale';
|
||||
else if (latestTask.startTime >= sevenDaysAgo) healthStatus = 'stale';
|
||||
else healthStatus = 'old';
|
||||
} else {
|
||||
healthStatus = 'failed';
|
||||
}
|
||||
} else if (latestSnapshotTime) {
|
||||
if (latestSnapshotTime >= threeDaysAgo) healthStatus = 'ok';
|
||||
else if (latestSnapshotTime >= sevenDaysAgoTimestamp) healthStatus = 'stale';
|
||||
else if (latestSnapshotTime >= sevenDaysAgo) healthStatus = 'stale';
|
||||
else healthStatus = 'old';
|
||||
} else {
|
||||
healthStatus = 'none';
|
||||
displayTimestamp = null;
|
||||
}
|
||||
|
||||
// Calculate 7-day backup status (dot matrix)
|
||||
const last7DaysBackupStatus = [];
|
||||
for (let i = 6; i >= 0; i--) { // Iterate from 6 days ago to today
|
||||
const dayTarget = new Date(now);
|
||||
dayTarget.setDate(now.getDate() - i);
|
||||
const dayStartTimestamp = Math.floor(dayTarget.getTime() / 1000);
|
||||
|
||||
const dayEndTarget = new Date(dayTarget);
|
||||
dayEndTarget.setDate(dayTarget.getDate() + 1);
|
||||
const dayEndTimestamp = Math.floor(dayEndTarget.getTime() / 1000);
|
||||
// Optimized 7-day backup status calculation using pre-calculated boundaries
|
||||
const last7DaysBackupStatus = dayBoundaries.map(day => {
|
||||
let dailyStatus = 'none';
|
||||
|
||||
let dailyStatus = 'none'; // Default to 'none'
|
||||
|
||||
// Check tasks for this day
|
||||
const tasksOnThisDay = guestTasks.filter(task =>
|
||||
task.startTime >= dayStartTimestamp && task.startTime < dayEndTimestamp
|
||||
);
|
||||
|
||||
const failedTaskOnThisDay = tasksOnThisDay.find(task => task.status !== 'OK');
|
||||
const successfulTaskOnThisDay = tasksOnThisDay.find(task => task.status === 'OK');
|
||||
|
||||
if (failedTaskOnThisDay) {
|
||||
dailyStatus = 'failed';
|
||||
} else if (successfulTaskOnThisDay) {
|
||||
dailyStatus = 'ok';
|
||||
} else {
|
||||
// If no tasks, check for snapshots as a fallback for successful backup indication
|
||||
const snapshotOnThisDay = guestSnapshots.some(
|
||||
snap => snap['backup-time'] >= dayStartTimestamp && snap['backup-time'] < dayEndTimestamp
|
||||
// Check tasks for this day - using pre-filtered guest tasks
|
||||
if (guestTasks) {
|
||||
const failedTaskOnThisDay = guestTasks.find(task =>
|
||||
task.startTime >= day.start && task.startTime < day.end && task.status !== 'OK'
|
||||
);
|
||||
if (snapshotOnThisDay) {
|
||||
const successfulTaskOnThisDay = guestTasks.find(task =>
|
||||
task.startTime >= day.start && task.startTime < day.end && task.status === 'OK'
|
||||
);
|
||||
|
||||
if (failedTaskOnThisDay) {
|
||||
dailyStatus = 'failed';
|
||||
} else if (successfulTaskOnThisDay) {
|
||||
dailyStatus = 'ok';
|
||||
} else if (guestSnapshots) {
|
||||
// Check snapshots as fallback - using pre-filtered guest snapshots
|
||||
const snapshotOnThisDay = guestSnapshots.some(
|
||||
snap => snap['backup-time'] >= day.start && snap['backup-time'] < day.end
|
||||
);
|
||||
if (snapshotOnThisDay) {
|
||||
dailyStatus = 'ok';
|
||||
}
|
||||
}
|
||||
}
|
||||
last7DaysBackupStatus.push(dailyStatus);
|
||||
}
|
||||
|
||||
return dailyStatus;
|
||||
});
|
||||
|
||||
return {
|
||||
guestName: guest.name || `Guest ${guest.vmid}`,
|
||||
@@ -269,7 +298,7 @@ PulseApp.ui.backups = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { allGuests, initialDataReceived, allRecentBackupTasks, allSnapshots } = _getInitialBackupData();
|
||||
const { allGuests, initialDataReceived, tasksByGuest, snapshotsByGuest, dayBoundaries, threeDaysAgo, sevenDaysAgo } = _getInitialBackupData();
|
||||
|
||||
if (!initialDataReceived) {
|
||||
loadingMsg.classList.remove('hidden');
|
||||
@@ -288,7 +317,7 @@ PulseApp.ui.backups = (() => {
|
||||
}
|
||||
loadingMsg.classList.add('hidden');
|
||||
|
||||
const backupStatusByGuest = allGuests.map(guest => _determineGuestBackupStatus(guest, allSnapshots, allRecentBackupTasks));
|
||||
const backupStatusByGuest = allGuests.map(guest => _determineGuestBackupStatus(guest, snapshotsByGuest.get(`${guest.vmid}-${guest.type === 'qemu' ? 'vm' : 'ct'}`) || [], tasksByGuest.get(`${guest.vmid}-${guest.type === 'qemu' ? 'vm' : 'ct'}`) || [], dayBoundaries, threeDaysAgo, sevenDaysAgo));
|
||||
const filteredBackupStatus = _filterBackupData(backupStatusByGuest, backupsSearchInput);
|
||||
|
||||
const sortStateBackups = PulseApp.state.getSortState('backups');
|
||||
|
||||
@@ -8,7 +8,6 @@ PulseApp.ui.common = (() => {
|
||||
searchInput = document.getElementById('dashboard-search');
|
||||
backupsSearchInput = document.getElementById('backups-search');
|
||||
|
||||
setupTableSorting('nodes-table');
|
||||
setupTableSorting('main-table');
|
||||
setupTableSorting('backups-overview-table');
|
||||
|
||||
@@ -41,7 +40,6 @@ PulseApp.ui.common = (() => {
|
||||
|
||||
function applyInitialSortUI() {
|
||||
const mainSortState = PulseApp.state.getSortState('main');
|
||||
const nodesSortState = PulseApp.state.getSortState('nodes');
|
||||
const backupsSortState = PulseApp.state.getSortState('backups');
|
||||
|
||||
const initialMainHeader = document.querySelector(`#main-table th[data-sort="${mainSortState.column}"]`);
|
||||
@@ -49,11 +47,6 @@ PulseApp.ui.common = (() => {
|
||||
updateSortUI('main-table', initialMainHeader);
|
||||
}
|
||||
|
||||
const initialNodesHeader = document.querySelector(`#nodes-table th[data-sort="${nodesSortState.column}"]`);
|
||||
if (initialNodesHeader) {
|
||||
updateSortUI('nodes-table', initialNodesHeader);
|
||||
}
|
||||
|
||||
const initialBackupsHeader = document.querySelector(`#backups-overview-table th[data-sort="${backupsSortState.column}"]`);
|
||||
if (initialBackupsHeader) {
|
||||
updateSortUI('backups-overview-table', initialBackupsHeader);
|
||||
@@ -79,7 +72,9 @@ PulseApp.ui.common = (() => {
|
||||
PulseApp.ui.dashboard.updateDashboardTable();
|
||||
if (searchInput) searchInput.dispatchEvent(new Event('input'));
|
||||
PulseApp.state.saveFilterState();
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
if (PulseApp.ui.thresholds && typeof PulseApp.ui.thresholds.updateLogControlsVisibility === 'function') {
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -91,7 +86,9 @@ PulseApp.ui.common = (() => {
|
||||
PulseApp.ui.dashboard.updateDashboardTable();
|
||||
if (searchInput) searchInput.dispatchEvent(new Event('input'));
|
||||
PulseApp.state.saveFilterState();
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
if (PulseApp.ui.thresholds && typeof PulseApp.ui.thresholds.updateLogControlsVisibility === 'function') {
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -99,7 +96,9 @@ PulseApp.ui.common = (() => {
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
PulseApp.ui.dashboard.updateDashboardTable();
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
if (PulseApp.ui.thresholds && typeof PulseApp.ui.thresholds.updateLogControlsVisibility === 'function') {
|
||||
PulseApp.ui.thresholds.updateLogControlsVisibility();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Element #dashboard-search not found - text filtering disabled.');
|
||||
@@ -259,9 +258,6 @@ PulseApp.ui.common = (() => {
|
||||
PulseApp.state.setSortState(tableType, column, newDirection);
|
||||
|
||||
switch(tableType) {
|
||||
case 'nodes':
|
||||
PulseApp.ui.nodes.updateNodesTable(PulseApp.state.get('nodesData'));
|
||||
break;
|
||||
case 'main':
|
||||
PulseApp.ui.dashboard.updateDashboardTable();
|
||||
break;
|
||||
|
||||
+129
-41
@@ -29,6 +29,17 @@ PulseApp.ui.dashboard = (() => {
|
||||
tableBodyEl = document.querySelector('#main-table tbody');
|
||||
statusElementEl = document.getElementById('dashboard-status-text');
|
||||
|
||||
// Initialize chart system
|
||||
if (PulseApp.charts) {
|
||||
PulseApp.charts.startChartUpdates();
|
||||
}
|
||||
|
||||
// Initialize charts toggle
|
||||
const chartsToggleButton = document.getElementById('toggle-charts-button');
|
||||
if (chartsToggleButton) {
|
||||
chartsToggleButton.addEventListener('click', toggleChartsMode);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// Handle Escape for resetting filters
|
||||
if (event.key === 'Escape') {
|
||||
@@ -132,7 +143,12 @@ PulseApp.ui.dashboard = (() => {
|
||||
avgNetOutRate = snapshot.netout;
|
||||
|
||||
if (guest.status === STATUS_RUNNING && metrics && metrics.current) {
|
||||
const currentDataPoint = { timestamp: Date.now(), ...metrics.current };
|
||||
const currentDataPoint = {
|
||||
timestamp: Date.now(),
|
||||
...metrics.current,
|
||||
// Convert CPU to percentage for consistency
|
||||
cpu: (metrics.current.cpu || 0) * 100
|
||||
};
|
||||
PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint);
|
||||
const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || [];
|
||||
avgCpu = _calculateAverage(history, 'cpu') ?? 0;
|
||||
@@ -159,6 +175,7 @@ PulseApp.ui.dashboard = (() => {
|
||||
const currentDataPoint = {
|
||||
timestamp: Date.now(),
|
||||
...metrics.current,
|
||||
cpu: (metrics.current.cpu || 0) * 100,
|
||||
effective_mem: currentMemForAvg,
|
||||
effective_mem_total: currentMemTotalForDisplay,
|
||||
effective_mem_source: effectiveMemorySource
|
||||
@@ -236,8 +253,9 @@ PulseApp.ui.dashboard = (() => {
|
||||
if (uptimeFormatted.length > maxUptimeLength) maxUptimeLength = uptimeFormatted.length;
|
||||
});
|
||||
|
||||
const nameColWidth = Math.min(Math.max(maxNameLength * 8 + 16, 100), 300);
|
||||
const uptimeColWidth = Math.max(maxUptimeLength * 7 + 16, 80);
|
||||
// More aggressive space optimization
|
||||
const nameColWidth = Math.min(Math.max(maxNameLength * 7 + 12, 80), 250);
|
||||
const uptimeColWidth = Math.max(maxUptimeLength * 6.5 + 8, 40);
|
||||
const htmlElement = document.documentElement;
|
||||
if (htmlElement) {
|
||||
htmlElement.style.setProperty('--name-col-width', `${nameColWidth}px`);
|
||||
@@ -280,7 +298,7 @@ PulseApp.ui.dashboard = (() => {
|
||||
const state = thresholdState[type];
|
||||
let guestValue;
|
||||
|
||||
if (type === METRIC_CPU) guestValue = guest.cpu * 100;
|
||||
if (type === METRIC_CPU) guestValue = guest.cpu;
|
||||
else if (type === METRIC_MEMORY) guestValue = guest.memory;
|
||||
else if (type === METRIC_DISK) guestValue = guest.disk;
|
||||
else if (type === METRIC_DISK_READ) guestValue = guest.diskread;
|
||||
@@ -426,35 +444,71 @@ PulseApp.ui.dashboard = (() => {
|
||||
} else {
|
||||
console.warn('[Dashboard] PulseApp.ui.common not available for updateSortUI');
|
||||
}
|
||||
|
||||
// Update charts immediately after table is rendered, but only if in charts mode
|
||||
const mainContainer = document.getElementById('main');
|
||||
if (PulseApp.charts && visibleCount > 0 && mainContainer && mainContainer.classList.contains('charts-mode')) {
|
||||
// Use requestAnimationFrame to ensure DOM is fully updated
|
||||
requestAnimationFrame(() => {
|
||||
PulseApp.charts.updateAllCharts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _createCpuBarHtml(guest) {
|
||||
if (guest.status !== STATUS_RUNNING) return '-';
|
||||
const cpuPercent = Math.round(guest.cpu * 100);
|
||||
const cpuTooltipText = `${cpuPercent}% ${guest.cpus ? `(${(guest.cpu * guest.cpus).toFixed(1)}/${guest.cpus} cores)` : ''}`;
|
||||
const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent);
|
||||
return PulseApp.utils.createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass);
|
||||
const cpuPercent = Math.round(guest.cpu);
|
||||
const cpuTooltipText = `${cpuPercent}% ${guest.cpus ? `(${(guest.cpu * guest.cpus / 100).toFixed(1)}/${guest.cpus} cores)` : ''}`;
|
||||
const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent, 'cpu');
|
||||
const progressBar = PulseApp.utils.createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass);
|
||||
|
||||
// Create both text and chart versions
|
||||
const guestId = guest.uniqueId;
|
||||
const chartHtml = PulseApp.charts ? PulseApp.charts.createUsageChartHTML(guestId, 'cpu') : '';
|
||||
|
||||
return `
|
||||
<div class="metric-text">${progressBar}</div>
|
||||
<div class="metric-chart">${chartHtml}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _createMemoryBarHtml(guest) {
|
||||
if (guest.status !== STATUS_RUNNING) return '-';
|
||||
const memoryPercent = guest.memory; // This is already a percentage
|
||||
const memoryPercent = guest.memory;
|
||||
let memoryTooltipText = `${PulseApp.utils.formatBytes(guest.memoryCurrent)} / ${PulseApp.utils.formatBytes(guest.memoryTotal)} (${memoryPercent}%)`;
|
||||
if (guest.type === GUEST_TYPE_VM && guest.memorySource === 'guest' && guest.rawHostMemory !== null && guest.rawHostMemory !== undefined) {
|
||||
memoryTooltipText += ` (Host: ${PulseApp.utils.formatBytes(guest.rawHostMemory)})`;
|
||||
}
|
||||
const memColorClass = PulseApp.utils.getUsageColor(memoryPercent);
|
||||
return PulseApp.utils.createProgressTextBarHTML(memoryPercent, memoryTooltipText, memColorClass);
|
||||
const memColorClass = PulseApp.utils.getUsageColor(memoryPercent, 'memory');
|
||||
const progressBar = PulseApp.utils.createProgressTextBarHTML(memoryPercent, memoryTooltipText, memColorClass);
|
||||
|
||||
// Create both text and chart versions
|
||||
const guestId = guest.uniqueId;
|
||||
const chartHtml = PulseApp.charts ? PulseApp.charts.createUsageChartHTML(guestId, 'memory') : '';
|
||||
|
||||
return `
|
||||
<div class="metric-text">${progressBar}</div>
|
||||
<div class="metric-chart">${chartHtml}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _createDiskBarHtml(guest) {
|
||||
if (guest.status !== STATUS_RUNNING) return '-';
|
||||
if (guest.type === GUEST_TYPE_CT) {
|
||||
const diskPercent = guest.disk; // This is already a percentage
|
||||
const diskPercent = guest.disk;
|
||||
const diskTooltipText = guest.diskTotal ? `${PulseApp.utils.formatBytes(guest.diskCurrent)} / ${PulseApp.utils.formatBytes(guest.diskTotal)} (${diskPercent}%)` : `${diskPercent}%`;
|
||||
const diskColorClass = PulseApp.utils.getUsageColor(diskPercent);
|
||||
return PulseApp.utils.createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass);
|
||||
} else { // For VMs, show total disk size, not a progress bar
|
||||
const diskColorClass = PulseApp.utils.getUsageColor(diskPercent, 'disk');
|
||||
const progressBar = PulseApp.utils.createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass);
|
||||
|
||||
// Create both text and chart versions
|
||||
const guestId = guest.uniqueId;
|
||||
const chartHtml = PulseApp.charts ? PulseApp.charts.createUsageChartHTML(guestId, 'disk') : '';
|
||||
|
||||
return `
|
||||
<div class="metric-text">${progressBar}</div>
|
||||
<div class="metric-chart">${chartHtml}</div>
|
||||
`;
|
||||
} else {
|
||||
if (guest.diskTotal) {
|
||||
return `<span class="text-xs text-gray-700 dark:text-gray-200 truncate">${PulseApp.utils.formatBytes(guest.diskTotal)}</span>`;
|
||||
}
|
||||
@@ -474,29 +528,40 @@ PulseApp.ui.dashboard = (() => {
|
||||
const memoryBarHTML = _createMemoryBarHtml(guest);
|
||||
const diskBarHTML = _createDiskBarHtml(guest);
|
||||
|
||||
const diskReadFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.diskread, 0) : '-';
|
||||
const diskWriteFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.diskwrite, 0) : '-';
|
||||
const diskReadFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeedWithStyling(guest.diskread, 0) : '-';
|
||||
const diskWriteFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeedWithStyling(guest.diskwrite, 0) : '-';
|
||||
const netInFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeedWithStyling(guest.netin, 0) : '-';
|
||||
const netOutFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeedWithStyling(guest.netout, 0) : '-';
|
||||
|
||||
// Icons and colors
|
||||
const upArrow = '↑';
|
||||
const downArrow = '↓';
|
||||
|
||||
let netInIcon = '';
|
||||
let netOutIcon = '';
|
||||
|
||||
if (guest.status === STATUS_RUNNING) {
|
||||
const netInActive = guest.netin > 0;
|
||||
const netOutActive = guest.netout > 0;
|
||||
|
||||
netInIcon = `<span class="text-xs mr-1 ${netInActive ? 'text-green-500' : 'text-gray-400 dark:text-gray-500'}">${downArrow}</span>`;
|
||||
netOutIcon = `<span class="text-xs mr-1 ${netOutActive ? 'text-red-500' : 'text-gray-400 dark:text-gray-500'}">${upArrow}</span>`;
|
||||
} else {
|
||||
netInIcon = `<span class="text-xs mr-1 text-gray-400 dark:text-gray-500">${downArrow}</span>`;
|
||||
netOutIcon = `<span class="text-xs mr-1 text-gray-400 dark:text-gray-500">${upArrow}</span>`;
|
||||
}
|
||||
// Create I/O cells with both text and chart versions
|
||||
const guestId = guest.uniqueId;
|
||||
|
||||
const netInFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.netin, 0) : '-';
|
||||
const netOutFormatted = guest.status === STATUS_RUNNING ? PulseApp.utils.formatSpeed(guest.netout, 0) : '-';
|
||||
let diskReadCell, diskWriteCell, netInCell, netOutCell;
|
||||
|
||||
if (guest.status === STATUS_RUNNING && PulseApp.charts) {
|
||||
// Text versions - clean, no arrows
|
||||
const diskReadText = diskReadFormatted;
|
||||
const diskWriteText = diskWriteFormatted;
|
||||
const netInText = netInFormatted;
|
||||
const netOutText = netOutFormatted;
|
||||
|
||||
// Chart versions - clean, no arrows
|
||||
const diskReadChart = PulseApp.charts.createSparklineHTML(guestId, 'diskread');
|
||||
const diskWriteChart = PulseApp.charts.createSparklineHTML(guestId, 'diskwrite');
|
||||
const netInChart = PulseApp.charts.createSparklineHTML(guestId, 'netin');
|
||||
const netOutChart = PulseApp.charts.createSparklineHTML(guestId, 'netout');
|
||||
|
||||
diskReadCell = `<div class="metric-text">${diskReadText}</div><div class="metric-chart">${diskReadChart}</div>`;
|
||||
diskWriteCell = `<div class="metric-text">${diskWriteText}</div><div class="metric-chart">${diskWriteChart}</div>`;
|
||||
netInCell = `<div class="metric-text">${netInText}</div><div class="metric-chart">${netInChart}</div>`;
|
||||
netOutCell = `<div class="metric-text">${netOutText}</div><div class="metric-chart">${netOutChart}</div>`;
|
||||
} else {
|
||||
// Fallback to text only for stopped guests - no arrows
|
||||
diskReadCell = diskReadFormatted;
|
||||
diskWriteCell = diskWriteFormatted;
|
||||
netInCell = netInFormatted;
|
||||
netOutCell = netOutFormatted;
|
||||
}
|
||||
|
||||
const typeIconClass = guest.type === GUEST_TYPE_VM
|
||||
? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium'
|
||||
@@ -519,10 +584,10 @@ PulseApp.ui.dashboard = (() => {
|
||||
<td class="p-1 px-2">${cpuBarHTML}</td>
|
||||
<td class="p-1 px-2">${memoryBarHTML}</td>
|
||||
<td class="p-1 px-2">${diskBarHTML}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${diskReadFormatted}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${diskWriteFormatted}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${netInIcon}${netInFormatted}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap">${netOutIcon}${netOutFormatted}</td>
|
||||
<td class="p-1 px-2">${diskReadCell}</td>
|
||||
<td class="p-1 px-2">${diskWriteCell}</td>
|
||||
<td class="p-1 px-2">${netInCell}</td>
|
||||
<td class="p-1 px-2">${netOutCell}</td>
|
||||
`;
|
||||
return row;
|
||||
}
|
||||
@@ -547,12 +612,35 @@ PulseApp.ui.dashboard = (() => {
|
||||
guestMetricDragSnapshot = {};
|
||||
}
|
||||
|
||||
function toggleChartsMode() {
|
||||
const mainContainer = document.getElementById('main');
|
||||
const button = document.getElementById('toggle-charts-button');
|
||||
|
||||
if (mainContainer.classList.contains('charts-mode')) {
|
||||
// Switch to metrics mode
|
||||
mainContainer.classList.remove('charts-mode');
|
||||
button.title = 'Toggle Charts View';
|
||||
} else {
|
||||
// Switch to charts mode
|
||||
mainContainer.classList.add('charts-mode');
|
||||
button.title = 'Toggle Metrics View';
|
||||
|
||||
// Immediately render charts when switching to charts mode
|
||||
if (PulseApp.charts) {
|
||||
requestAnimationFrame(() => {
|
||||
PulseApp.charts.updateAllCharts();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
refreshDashboardData,
|
||||
updateDashboardTable,
|
||||
createGuestRow,
|
||||
snapshotGuestMetricsForDrag, // Export snapshot function
|
||||
clearGuestMetricSnapshots // Export clear function
|
||||
clearGuestMetricSnapshots, // Export clear function
|
||||
toggleChartsMode
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -4,7 +4,7 @@ PulseApp.ui.nodes = (() => {
|
||||
|
||||
function _createNodeCpuBarHtml(node) {
|
||||
const cpuPercent = node.cpu ? (node.cpu * 100) : 0;
|
||||
const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent);
|
||||
const cpuColorClass = PulseApp.utils.getUsageColor(cpuPercent, 'cpu');
|
||||
const cpuTooltipText = `${cpuPercent.toFixed(1)}%${node.maxcpu && node.maxcpu > 0 ? ` (${(node.cpu * node.maxcpu).toFixed(1)}/${node.maxcpu} cores)` : ''}`;
|
||||
return PulseApp.utils.createProgressTextBarHTML(cpuPercent, cpuTooltipText, cpuColorClass);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ PulseApp.ui.nodes = (() => {
|
||||
const memUsed = node.mem || 0;
|
||||
const memTotal = node.maxmem || 0;
|
||||
const memPercent = (memUsed && memTotal > 0) ? (memUsed / memTotal * 100) : 0;
|
||||
const memColorClass = PulseApp.utils.getUsageColor(memPercent);
|
||||
const memColorClass = PulseApp.utils.getUsageColor(memPercent, 'memory');
|
||||
const memTooltipText = `${PulseApp.utils.formatBytes(memUsed)} / ${PulseApp.utils.formatBytes(memTotal)} (${memPercent.toFixed(1)}%)`;
|
||||
return PulseApp.utils.createProgressTextBarHTML(memPercent, memTooltipText, memColorClass);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ PulseApp.ui.nodes = (() => {
|
||||
const diskUsed = node.disk || 0;
|
||||
const diskTotal = node.maxdisk || 0;
|
||||
const diskPercent = (diskUsed && diskTotal > 0) ? (diskUsed / diskTotal * 100) : 0;
|
||||
const diskColorClass = PulseApp.utils.getUsageColor(diskPercent);
|
||||
const diskColorClass = PulseApp.utils.getUsageColor(diskPercent, 'disk');
|
||||
const diskTooltipText = `${PulseApp.utils.formatBytes(diskUsed)} / ${PulseApp.utils.formatBytes(diskTotal)} (${diskPercent.toFixed(1)}%)`;
|
||||
return PulseApp.utils.createProgressTextBarHTML(diskPercent, diskTooltipText, diskColorClass);
|
||||
}
|
||||
@@ -181,10 +181,11 @@ PulseApp.ui.nodes = (() => {
|
||||
function updateNodesTable(nodes) {
|
||||
const tbody = document.getElementById('nodes-table-body');
|
||||
if (!tbody) {
|
||||
console.error('Critical element #nodes-table-body not found for nodes table update!');
|
||||
// Node table doesn't exist in current UI - nodes are displayed as summary cards instead
|
||||
console.log('[Nodes] Node table not found - using summary cards display instead');
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = ''; // Clear previous content
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="p-4 text-center text-gray-500 dark:text-gray-400">No nodes found or data unavailable</td></tr>';
|
||||
@@ -193,7 +194,7 @@ PulseApp.ui.nodes = (() => {
|
||||
|
||||
// Group nodes by clusterIdentifier
|
||||
const clusters = nodes.reduce((acc, node) => {
|
||||
const key = node.clusterIdentifier || 'Unknown Cluster'; // Fallback for safety
|
||||
const key = node.clusterIdentifier || 'Unknown Cluster';
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
@@ -207,8 +208,6 @@ PulseApp.ui.nodes = (() => {
|
||||
for (const clusterIdentifier in clusters) {
|
||||
if (clusters.hasOwnProperty(clusterIdentifier)) {
|
||||
const nodesInCluster = clusters[clusterIdentifier];
|
||||
// All nodes in this group should have the same endpointType, so pick from the first.
|
||||
// Default to 'standalone' if nodesInCluster is empty or endpointType is missing for some reason.
|
||||
const endpointType = (nodesInCluster && nodesInCluster.length > 0 && nodesInCluster[0].endpointType)
|
||||
? nodesInCluster[0].endpointType
|
||||
: 'standalone';
|
||||
@@ -218,7 +217,6 @@ PulseApp.ui.nodes = (() => {
|
||||
: PulseApp.ui.common.NODE_GROUP_STANDALONE_ICON_SVG;
|
||||
|
||||
const clusterHeaderRow = document.createElement('tr');
|
||||
// Applying base background, then overlaying with stripe pattern classes
|
||||
clusterHeaderRow.innerHTML = PulseApp.ui.common.generateNodeGroupHeaderCellHTML(clusterIdentifier, 7, 'th');
|
||||
tbody.appendChild(clusterHeaderRow);
|
||||
|
||||
|
||||
+61
-18
@@ -1,29 +1,50 @@
|
||||
PulseApp.ui = PulseApp.ui || {};
|
||||
|
||||
PulseApp.ui.storage = (() => {
|
||||
// Cache for computed values to avoid recalculation
|
||||
const contentBadgeCache = new Map();
|
||||
const iconCache = new Map();
|
||||
const contentBadgeHTMLCache = new Map(); // Cache for complete content badge HTML
|
||||
|
||||
function getStorageTypeIcon(type) {
|
||||
if (iconCache.has(type)) {
|
||||
return iconCache.get(type);
|
||||
}
|
||||
|
||||
let icon;
|
||||
switch(type) {
|
||||
case 'dir':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-yellow-600 dark:text-yellow-400"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-yellow-600 dark:text-yellow-400"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
|
||||
break;
|
||||
case 'lvm':
|
||||
case 'lvmthin':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-purple-600 dark:text-purple-400"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-purple-600 dark:text-purple-400"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>';
|
||||
break;
|
||||
case 'zfs':
|
||||
case 'zfspool':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-red-600 dark:text-red-400"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-red-600 dark:text-red-400"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>';
|
||||
break;
|
||||
case 'nfs':
|
||||
case 'cifs':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-blue-600 dark:text-blue-400"><path d="M16 17l5-5-5-5"></path><path d="M8 17l-5-5 5-5"></path></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-blue-600 dark:text-blue-400"><path d="M16 17l5-5-5-5"></path><path d="M8 17l-5-5 5-5"></path></svg>';
|
||||
break;
|
||||
case 'cephfs':
|
||||
case 'rbd':
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-indigo-600 dark:text-indigo-400"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-indigo-600 dark:text-indigo-400"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg>';
|
||||
break;
|
||||
default:
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-gray-500"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
|
||||
icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block mr-1 align-middle text-gray-500"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
|
||||
}
|
||||
|
||||
iconCache.set(type, icon);
|
||||
return icon;
|
||||
}
|
||||
|
||||
function getContentBadgeDetails(contentType) {
|
||||
if (contentBadgeCache.has(contentType)) {
|
||||
return contentBadgeCache.get(contentType);
|
||||
}
|
||||
|
||||
let details = {
|
||||
badgeClass: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300',
|
||||
tooltip: `Content type: ${contentType}`
|
||||
@@ -55,9 +76,31 @@ PulseApp.ui.storage = (() => {
|
||||
details.tooltip = 'Snippet files (e.g., cloud-init configs)';
|
||||
break;
|
||||
}
|
||||
|
||||
contentBadgeCache.set(contentType, details);
|
||||
return details;
|
||||
}
|
||||
|
||||
function getContentBadgesHTML(contentString) {
|
||||
if (!contentString) return '-';
|
||||
|
||||
if (contentBadgeHTMLCache.has(contentString)) {
|
||||
return contentBadgeHTMLCache.get(contentString);
|
||||
}
|
||||
|
||||
const contentTypes = contentString.split(',').map(ct => ct.trim()).filter(ct => ct);
|
||||
contentTypes.sort();
|
||||
|
||||
const contentBadges = contentTypes.map(ct => {
|
||||
const details = getContentBadgeDetails(ct);
|
||||
return `<span data-tooltip="${details.tooltip}" class="storage-tooltip-trigger inline-block ${details.badgeClass} rounded px-1.5 py-0.5 text-xs font-medium mr-1 cursor-default">${ct}</span>`;
|
||||
}).join('');
|
||||
|
||||
const result = contentBadges || '-';
|
||||
contentBadgeHTMLCache.set(contentString, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function sortNodeStorageData(storageArray) {
|
||||
if (!storageArray || !Array.isArray(storageArray)) return [];
|
||||
const sortedArray = [...storageArray];
|
||||
@@ -82,9 +125,11 @@ PulseApp.ui.storage = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-sort storage data for each node to avoid repeated sorting
|
||||
const storageByNode = nodes.reduce((acc, node) => {
|
||||
if (node && node.node) {
|
||||
acc[node.node] = Array.isArray(node.storage) ? node.storage : [];
|
||||
const storageData = Array.isArray(node.storage) ? node.storage : [];
|
||||
acc[node.node] = sortNodeStorageData(storageData);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -119,7 +164,7 @@ PulseApp.ui.storage = (() => {
|
||||
const sortedNodeNames = Object.keys(storageByNode).sort((a, b) => a.localeCompare(b));
|
||||
|
||||
sortedNodeNames.forEach(nodeName => {
|
||||
const nodeStorageData = storageByNode[nodeName];
|
||||
const nodeStorageData = storageByNode[nodeName]; // Already sorted
|
||||
|
||||
const nodeHeaderRow = document.createElement('tr');
|
||||
nodeHeaderRow.className = 'bg-gray-100 dark:bg-gray-700/80 font-semibold text-gray-700 dark:text-gray-300 text-xs node-storage-header';
|
||||
@@ -133,8 +178,8 @@ PulseApp.ui.storage = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedNodeStorageData = sortNodeStorageData(nodeStorageData);
|
||||
sortedNodeStorageData.forEach(store => {
|
||||
// Use pre-sorted data instead of sorting again
|
||||
nodeStorageData.forEach(store => {
|
||||
const row = _createStorageRow(store);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -156,19 +201,17 @@ PulseApp.ui.storage = (() => {
|
||||
const usageBarHTML = PulseApp.utils.createProgressTextBarHTML(usagePercent, usageTooltipText, usageColorClass);
|
||||
|
||||
const sharedIconTooltip = store.shared === 1 ? 'Shared across cluster' : 'Local to node';
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
const localIconGrayClass = isDarkMode ? 'text-gray-400' : 'text-gray-300';
|
||||
const sharedIcon = store.shared === 1 ? `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block text-green-600 dark:text-green-400"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`
|
||||
: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block text-gray-400 dark:text-gray-500 opacity-50"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`;
|
||||
: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block ${localIconGrayClass} opacity-50"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>`;
|
||||
|
||||
const contentTypes = (store.content || '').split(',').map(ct => ct.trim()).filter(ct => ct);
|
||||
contentTypes.sort();
|
||||
const contentBadges = contentTypes.map(ct => {
|
||||
const details = getContentBadgeDetails(ct);
|
||||
return `<span data-tooltip="${details.tooltip}" class="storage-tooltip-trigger inline-block ${details.badgeClass} rounded px-1.5 py-0.5 text-xs font-medium mr-1 cursor-default">${ct}</span>`;
|
||||
}).join('');
|
||||
// Use cached content badge HTML instead of processing inline
|
||||
const contentBadges = getContentBadgesHTML(store.content);
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-900 dark:text-gray-100 font-medium">${store.storage || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs flex items-center">${contentBadges || '-'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300 text-xs flex items-center">${contentBadges}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${store.type || 'N/A'}</td>
|
||||
<td class="p-1 px-2 whitespace-nowrap storage-tooltip-trigger cursor-default" data-tooltip="${sharedIconTooltip}">${sharedIcon}</td>
|
||||
<td class="p-1 px-2 text-gray-600 dark:text-gray-300 min-w-[250px]">${usageBarHTML}</td>
|
||||
|
||||
@@ -6,14 +6,12 @@ PulseApp.ui.thresholds = (() => {
|
||||
let thresholdBadge = null;
|
||||
let sliders = {};
|
||||
let thresholdSelects = {};
|
||||
let startLogButton = null;
|
||||
let isDraggingSlider = false;
|
||||
|
||||
function init() {
|
||||
thresholdRow = document.getElementById('threshold-slider-row');
|
||||
toggleThresholdsButton = document.getElementById('toggle-thresholds-button');
|
||||
thresholdBadge = document.getElementById('threshold-count-badge');
|
||||
startLogButton = document.getElementById('start-log-button');
|
||||
|
||||
sliders = {
|
||||
cpu: document.getElementById('threshold-slider-cpu'),
|
||||
@@ -30,7 +28,6 @@ PulseApp.ui.thresholds = (() => {
|
||||
applyInitialThresholdUI();
|
||||
updateThresholdIndicator();
|
||||
updateThresholdRowVisibility();
|
||||
updateLogControlsVisibility();
|
||||
|
||||
if (toggleThresholdsButton) {
|
||||
toggleThresholdsButton.addEventListener('click', () => {
|
||||
@@ -113,8 +110,6 @@ PulseApp.ui.thresholds = (() => {
|
||||
document.addEventListener('touchend', _handleThresholdDragEnd);
|
||||
}
|
||||
|
||||
// Old setupThresholdListeners function removed.
|
||||
|
||||
function updateThreshold(type, value) {
|
||||
PulseApp.state.setThresholdValue(type, value);
|
||||
|
||||
@@ -124,7 +119,6 @@ PulseApp.ui.thresholds = (() => {
|
||||
console.warn('[Thresholds] PulseApp.ui.dashboard not available for updateDashboardTable');
|
||||
}
|
||||
updateThresholdIndicator();
|
||||
updateLogControlsVisibility();
|
||||
}
|
||||
|
||||
function updateThresholdRowVisibility() {
|
||||
@@ -196,38 +190,6 @@ PulseApp.ui.thresholds = (() => {
|
||||
PulseApp.state.set('isThresholdRowVisible', false);
|
||||
updateThresholdRowVisibility();
|
||||
updateThresholdIndicator();
|
||||
updateLogControlsVisibility(); // Ensure log button visibility updates
|
||||
}
|
||||
|
||||
function updateLogControlsVisibility() {
|
||||
if (!startLogButton) return;
|
||||
|
||||
let isAnyFilterActive = false;
|
||||
const thresholdState = PulseApp.state.getThresholdState();
|
||||
const searchInput = document.getElementById('dashboard-search');
|
||||
const filterGuestType = PulseApp.state.get('filterGuestType');
|
||||
const filterStatus = PulseApp.state.get('filterStatus');
|
||||
|
||||
for (const type in thresholdState) {
|
||||
if (thresholdState[type].value > 0) {
|
||||
isAnyFilterActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAnyFilterActive && searchInput && searchInput.value.trim() !== '') {
|
||||
isAnyFilterActive = true;
|
||||
}
|
||||
|
||||
if (!isAnyFilterActive && filterGuestType !== 'all') {
|
||||
isAnyFilterActive = true;
|
||||
}
|
||||
|
||||
if (!isAnyFilterActive && filterStatus !== 'all') {
|
||||
isAnyFilterActive = true;
|
||||
}
|
||||
|
||||
startLogButton.classList.toggle('hidden', !isAnyFilterActive);
|
||||
}
|
||||
|
||||
// Getter for dashboard.js to check drag state
|
||||
@@ -238,7 +200,6 @@ PulseApp.ui.thresholds = (() => {
|
||||
return {
|
||||
init,
|
||||
resetThresholds,
|
||||
updateLogControlsVisibility,
|
||||
isThresholdDragInProgress
|
||||
};
|
||||
})();
|
||||
|
||||
+50
-4
@@ -1,8 +1,27 @@
|
||||
PulseApp.utils = (() => {
|
||||
function getUsageColor(percentage) {
|
||||
if (percentage >= 90) return 'red';
|
||||
if (percentage >= 75) return 'yellow';
|
||||
return 'green';
|
||||
function getUsageColor(percentage, metric = 'generic') {
|
||||
// Progress bars use traditional green/yellow/red with metric-specific thresholds
|
||||
if (metric === 'cpu') {
|
||||
// CPU: show color for significant usage
|
||||
if (percentage >= 90) return 'red';
|
||||
if (percentage >= 80) return 'yellow';
|
||||
return 'green'; // Healthy green for normal CPU usage
|
||||
} else if (metric === 'memory') {
|
||||
// Memory: be more conservative due to critical nature
|
||||
if (percentage >= 85) return 'red';
|
||||
if (percentage >= 75) return 'yellow';
|
||||
return 'green'; // Healthy green for normal memory usage
|
||||
} else if (metric === 'disk') {
|
||||
// Disk: can run higher before concerning
|
||||
if (percentage >= 90) return 'red';
|
||||
if (percentage >= 80) return 'yellow';
|
||||
return 'green'; // Healthy green for normal disk usage
|
||||
} else {
|
||||
// Generic/legacy fallback (for other uses like storage, etc.)
|
||||
if (percentage >= 90) return 'red';
|
||||
if (percentage >= 75) return 'yellow';
|
||||
return 'green'; // Keep green for non-dashboard usage
|
||||
}
|
||||
}
|
||||
|
||||
function createProgressTextBarHTML(percentage, text, color) {
|
||||
@@ -37,6 +56,32 @@ PulseApp.utils = (() => {
|
||||
return formatBytes(bytesPerSecond, decimals) + '/s';
|
||||
}
|
||||
|
||||
function formatSpeedWithStyling(bytesPerSecond, decimals = 1) {
|
||||
if (bytesPerSecond === null || bytesPerSecond === undefined) return 'N/A';
|
||||
|
||||
let formattedSpeed;
|
||||
if (bytesPerSecond < 1) {
|
||||
formattedSpeed = '0 B/s';
|
||||
} else {
|
||||
formattedSpeed = formatBytes(bytesPerSecond, decimals) + '/s';
|
||||
}
|
||||
|
||||
// Use same absolute thresholds as chart logic
|
||||
const mbps = bytesPerSecond / (1024 * 1024);
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
|
||||
let textClass = '';
|
||||
if (mbps < 1) {
|
||||
// Not noteworthy - use theme-adaptive dim gray
|
||||
textClass = isDarkMode ? 'text-gray-400' : 'text-gray-300';
|
||||
} else {
|
||||
// Noteworthy - use normal text color
|
||||
textClass = 'text-gray-800 dark:text-gray-200';
|
||||
}
|
||||
|
||||
return `<span class="${textClass}">${formattedSpeed}</span>`;
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
if (seconds === null || seconds === undefined || seconds < 0) return 'N/A';
|
||||
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
||||
@@ -197,6 +242,7 @@ PulseApp.utils = (() => {
|
||||
createProgressTextBarHTML,
|
||||
formatBytes,
|
||||
formatSpeed,
|
||||
formatSpeedWithStyling,
|
||||
formatUptime,
|
||||
formatDuration,
|
||||
formatPbsTimestamp,
|
||||
|
||||
Reference in New Issue
Block a user