mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
fix: simplify PBS connected status text
This commit is contained in:
+72
-37
@@ -1,48 +1,83 @@
|
||||
# Proxmox VE Connection Details for Pulse Monitoring
|
||||
# Pulse Environment Variables Example
|
||||
# Copy this file to .env in the same directory and fill in your details.
|
||||
# DO NOT commit your actual .env file to version control.
|
||||
|
||||
# --- Primary Proxmox Endpoint (Required) ---
|
||||
# URL of your Proxmox server or a node within the cluster
|
||||
PROXMOX_HOST=https://your-proxmox-ip-or-hostname:8006
|
||||
|
||||
# Proxmox API Token ID (e.g., user@pam!tokenid) - Recommended & Required
|
||||
# --- Primary Proxmox VE Endpoint ---
|
||||
# Required connection details for your main Proxmox VE host or cluster.
|
||||
PROXMOX_HOST=your-proxmox-ip-or-hostname
|
||||
PROXMOX_TOKEN_ID=your-api-token-id@pam!your-token-name
|
||||
|
||||
# Proxmox API Token Secret (UUID format) - Recommended & Required
|
||||
PROXMOX_TOKEN_SECRET=your-api-token-secret-uuid
|
||||
|
||||
# --- Optional Primary Endpoint Settings ---
|
||||
# Display name for this endpoint (defaults to PROXMOX_HOST if unset)
|
||||
# PROXMOX_NODE_NAME=MyPrimaryCluster
|
||||
# Optional: Node name for display (defaults to PROXMOX_HOST if not set)
|
||||
# PROXMOX_NODE_NAME=MyPrimaryNode
|
||||
|
||||
# Allow connections to servers with self-signed SSL certificates (true/false)
|
||||
# Defaults to false if unset. Set to true only if needed and you understand the risks.
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS=false
|
||||
|
||||
# Proxmox API port (defaults to 8006 if unset)
|
||||
# Optional: Proxmox API port (defaults to 8006 if not set)
|
||||
# PROXMOX_PORT=8006
|
||||
|
||||
# --- Additional Proxmox Endpoints (Optional) ---
|
||||
# To monitor more than one separate Proxmox environment (cluster or standalone node),
|
||||
# add blocks of variables starting with index 2 (_2, _3, etc.)
|
||||
# Optional: Set to 'false' to disable certificate validation (useful for self-signed certs)
|
||||
# Defaults to 'true' (validation enabled) if not set.
|
||||
PROXMOX_ALLOW_SELF_SIGNED_CERTS=true
|
||||
|
||||
# Example for a second endpoint:
|
||||
# PROXMOX_HOST_2=https://second-proxmox-ip:8006
|
||||
# PROXMOX_TOKEN_ID_2=user@pam!second-token # Required for additional endpoints
|
||||
# PROXMOX_TOKEN_SECRET_2=another-api-token-secret-uuid # Required for additional endpoints
|
||||
# Optional settings for the second endpoint:
|
||||
# PROXMOX_NODE_NAME_2=MySecondCluster
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS_2=false
|
||||
# Optional: Fallback username/password (use Token Auth primarily)
|
||||
# PROXMOX_USERNAME=your_username
|
||||
# PROXMOX_PASSWORD=your_password
|
||||
# PROXMOX_REALM=pam # or pve, etc.
|
||||
|
||||
# Optional: Set to 'false' to disable this endpoint (defaults to true)
|
||||
# PROXMOX_ENABLED=true
|
||||
|
||||
# --- Additional Proxmox VE Endpoints ---
|
||||
# Use suffixes _2, _3, etc., for more endpoints. HOST, TOKEN_ID, and TOKEN_SECRET are required for each.
|
||||
# PROXMOX_HOST_2=second-proxmox-ip
|
||||
# PROXMOX_TOKEN_ID_2=user@realm!token_name
|
||||
# PROXMOX_TOKEN_SECRET_2=secret-uuid
|
||||
# PROXMOX_NODE_NAME_2=MySecondNode
|
||||
# PROXMOX_PORT_2=8006
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS_2=true
|
||||
# PROXMOX_ENABLED_2=true
|
||||
|
||||
# Example for a third endpoint:
|
||||
# PROXMOX_HOST_3=https://third-proxmox-ip:8006
|
||||
# PROXMOX_TOKEN_ID_3=user@pve!third-token # Required for additional endpoints
|
||||
# PROXMOX_TOKEN_SECRET_3=yet-another-api-token-secret-uuid # Required for additional endpoints
|
||||
# Optional settings for the third endpoint:
|
||||
# PROXMOX_NODE_NAME_3=StandaloneNode
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS_3=true
|
||||
# PROXMOX_HOST_3=third-proxmox-ip
|
||||
# PROXMOX_TOKEN_ID_3=user@realm!token_name
|
||||
# PROXMOX_TOKEN_SECRET_3=secret-uuid
|
||||
# ... and so on
|
||||
|
||||
# --- Pulse Server Settings ---
|
||||
# Port for the Pulse monitoring server itself to listen on
|
||||
# Defaults to 7655 if unset
|
||||
# PORT=7655
|
||||
# --- Primary Proxmox Backup Server (PBS) Endpoint ---
|
||||
# Required: Set PBS_HOST and EITHER (PBS_USER + PBS_PASSWORD) OR (PBS_TOKEN_ID + PBS_TOKEN_SECRET)
|
||||
PBS_HOST=your-pbs-ip-or-hostname
|
||||
|
||||
# Option 1: User/Password Authentication
|
||||
# PBS_USER=your_pbs_user@pbs # e.g., root@pam or backupuser@pbs
|
||||
# PBS_PASSWORD=your_pbs_password
|
||||
# PBS_REALM=pbs # Optional: defaults to 'pbs' if using user/pass
|
||||
|
||||
# Option 2: API Token Authentication (Recommended)
|
||||
PBS_TOKEN_ID=your_pbs_api_token_id@pbs!your_token_name # e.g., backup@pbs!pulse-token
|
||||
PBS_TOKEN_SECRET=your_pbs_api_token_secret_uuid
|
||||
|
||||
# Optional: PBS instance name for display (defaults to PBS_HOST if not set)
|
||||
# PBS_NODE_NAME=MyPrimaryPBS
|
||||
|
||||
# Optional: PBS API port (defaults to 8007 if not set)
|
||||
# PBS_PORT=8007
|
||||
|
||||
# Optional: Set to 'false' to disable certificate validation for PBS
|
||||
# Defaults to 'true' (validation enabled) if not set.
|
||||
PBS_ALLOW_SELF_SIGNED_CERTS=true
|
||||
|
||||
# --- Additional Proxmox Backup Server (PBS) Endpoints ---
|
||||
# Use suffixes _2, _3, etc., for more PBS instances. PBS_HOST_n and authentication details are required.
|
||||
# PBS_HOST_2=second-pbs-ip
|
||||
# Option 1: User/Password
|
||||
# PBS_USER_2=user@realm
|
||||
# PBS_PASSWORD_2=password
|
||||
# PBS_REALM_2=pbs
|
||||
# Option 2: Token
|
||||
# PBS_TOKEN_ID_2=token_id@realm!token_name
|
||||
# PBS_TOKEN_SECRET_2=secret-uuid
|
||||
# PBS_NODE_NAME_2=MySecondPBS
|
||||
# PBS_PORT_2=8007
|
||||
# PBS_ALLOW_SELF_SIGNED_CERTS_2=true
|
||||
|
||||
# --- Development Settings ---
|
||||
# Set to 'development' to enable features like hot-reloading (requires chokidar dev dependency)
|
||||
# NODE_ENV=development
|
||||
+267
-222
@@ -124,71 +124,108 @@ if (process.env.NODE_ENV === 'development') {
|
||||
|
||||
// --- Create API Clients for Each Endpoint ---
|
||||
const apiClients = {}; // Use an object to store clients, keyed by endpoint.id
|
||||
let pbsApiClient = null; // Initialize PBS client as null
|
||||
let pbsConfig = null; // Initialize PBS config as null
|
||||
const pbsConfigs = []; // Array to hold all parsed PBS configurations
|
||||
const pbsApiClients = {}; // Object to hold initialized clients, keyed by pbsConfig.id
|
||||
|
||||
// --- Load PBS Configuration (if provided) ---
|
||||
// Check for User/Password first
|
||||
if (process.env.PBS_HOST && process.env.PBS_USER && process.env.PBS_PASSWORD) {
|
||||
const pbsHost = process.env.PBS_HOST;
|
||||
const pbsUser = process.env.PBS_USER;
|
||||
const pbsPassword = process.env.PBS_PASSWORD;
|
||||
const pbsRealm = process.env.PBS_REALM || 'pbs'; // Default realm 'pbs'
|
||||
function loadPbsConfig(index = null) {
|
||||
const suffix = index ? `_${index}` : '';
|
||||
const hostVar = `PBS_HOST${suffix}`;
|
||||
const userVar = `PBS_USER${suffix}`;
|
||||
const passVar = `PBS_PASSWORD${suffix}`;
|
||||
const realmVar = `PBS_REALM${suffix}`;
|
||||
const tokenIdVar = `PBS_TOKEN_ID${suffix}`;
|
||||
const tokenSecretVar = `PBS_TOKEN_SECRET${suffix}`;
|
||||
const nodeNameVar = `PBS_NODE_NAME${suffix}`;
|
||||
const portVar = `PBS_PORT${suffix}`;
|
||||
const selfSignedVar = `PBS_ALLOW_SELF_SIGNED_CERTS${suffix}`;
|
||||
|
||||
// Basic placeholder check for PBS user/pass vars
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHost.includes(p) || pbsUser.includes(p) || pbsPassword.includes(p)
|
||||
);
|
||||
|
||||
if (pbsPlaceholders.length > 0) {
|
||||
console.warn(`WARN: Skipping PBS configuration. The following variables seem to contain placeholder values: ${pbsPlaceholders.join(', ')}`);
|
||||
} else {
|
||||
pbsConfig = {
|
||||
id: 'pbs_primary_userpass',
|
||||
authMethod: 'userpass', // Indicate auth method
|
||||
name: process.env.PBS_NODE_NAME || pbsHost,
|
||||
host: pbsHost,
|
||||
port: process.env.PBS_PORT || '8007',
|
||||
user: pbsUser,
|
||||
password: pbsPassword,
|
||||
realm: pbsRealm,
|
||||
nodeName: process.env.PBS_NODE_NAME,
|
||||
allowSelfSignedCerts: process.env.PBS_ALLOW_SELF_SIGNED_CERTS !== 'false',
|
||||
enabled: true
|
||||
};
|
||||
console.log(`INFO: Found PBS configuration (User/Password): ${pbsConfig.name} (${pbsConfig.host})`);
|
||||
const pbsHost = process.env[hostVar];
|
||||
if (!pbsHost) {
|
||||
// No more PBS configs if PBS_HOST is missing
|
||||
return false; // Indicate no more configs found
|
||||
}
|
||||
// Check for Token second (fallback)
|
||||
} else if (process.env.PBS_HOST && process.env.PBS_TOKEN_ID && process.env.PBS_TOKEN_SECRET) {
|
||||
const pbsHost = process.env.PBS_HOST;
|
||||
const pbsTokenId = process.env.PBS_TOKEN_ID;
|
||||
const pbsTokenSecret = process.env.PBS_TOKEN_SECRET;
|
||||
|
||||
// Basic placeholder check for PBS token vars
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHost.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p)
|
||||
);
|
||||
const pbsUser = process.env[userVar];
|
||||
const pbsPassword = process.env[passVar];
|
||||
const pbsTokenId = process.env[tokenIdVar];
|
||||
const pbsTokenSecret = process.env[tokenSecretVar];
|
||||
|
||||
if (pbsPlaceholders.length > 0) {
|
||||
console.warn(`WARN: Skipping PBS configuration (Token). The following variables seem to contain placeholder values: ${pbsPlaceholders.join(', ')}`);
|
||||
} else {
|
||||
pbsConfig = {
|
||||
id: 'pbs_primary_token',
|
||||
authMethod: 'token', // Indicate auth method
|
||||
name: process.env.PBS_NODE_NAME || pbsHost,
|
||||
host: pbsHost,
|
||||
port: process.env.PBS_PORT || '8007',
|
||||
tokenId: pbsTokenId,
|
||||
tokenSecret: pbsTokenSecret,
|
||||
nodeName: process.env.PBS_NODE_NAME,
|
||||
allowSelfSignedCerts: process.env.PBS_ALLOW_SELF_SIGNED_CERTS !== 'false',
|
||||
enabled: true
|
||||
};
|
||||
console.log(`INFO: Found PBS configuration (API Token): ${pbsConfig.name} (${pbsConfig.host})`);
|
||||
let config = null;
|
||||
let idPrefix = index ? `pbs_endpoint_${index}` : 'pbs_primary';
|
||||
|
||||
// Check User/Password first
|
||||
if (pbsUser && pbsPassword) {
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHost.includes(p) || pbsUser.includes(p) || pbsPassword.includes(p)
|
||||
);
|
||||
if (pbsPlaceholders.length > 0) {
|
||||
console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (User/Pass). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`);
|
||||
} else {
|
||||
config = {
|
||||
id: `${idPrefix}_userpass`,
|
||||
authMethod: 'userpass',
|
||||
name: process.env[nodeNameVar] || pbsHost, // User-defined name or host
|
||||
host: pbsHost,
|
||||
port: process.env[portVar] || '8007',
|
||||
user: pbsUser,
|
||||
password: pbsPassword,
|
||||
realm: process.env[realmVar] || 'pbs',
|
||||
nodeName: process.env[nodeNameVar], // Store explicitly set node name
|
||||
allowSelfSignedCerts: process.env[selfSignedVar] !== 'false',
|
||||
enabled: true // Assuming enabled if configured
|
||||
};
|
||||
console.log(`INFO: Found PBS configuration ${index || 'primary'} (User/Password): ${config.name} (${config.host})`);
|
||||
}
|
||||
}
|
||||
// Check Token second
|
||||
else if (pbsTokenId && pbsTokenSecret) {
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHost.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p)
|
||||
);
|
||||
if (pbsPlaceholders.length > 0) {
|
||||
console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (Token). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`);
|
||||
} else {
|
||||
config = {
|
||||
id: `${idPrefix}_token`,
|
||||
authMethod: 'token',
|
||||
name: process.env[nodeNameVar] || pbsHost,
|
||||
host: pbsHost,
|
||||
port: process.env[portVar] || '8007',
|
||||
tokenId: pbsTokenId,
|
||||
tokenSecret: pbsTokenSecret,
|
||||
nodeName: process.env[nodeNameVar],
|
||||
allowSelfSignedCerts: process.env[selfSignedVar] !== 'false',
|
||||
enabled: true
|
||||
};
|
||||
console.log(`INFO: Found PBS configuration ${index || 'primary'} (API Token): ${config.name} (${config.host})`);
|
||||
}
|
||||
}
|
||||
// Warn if host is set but auth is incomplete
|
||||
else {
|
||||
console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set either (${userVar} + ${passVar}) or (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`);
|
||||
}
|
||||
} else if (process.env.PBS_HOST || process.env.PBS_TOKEN_ID || process.env.PBS_TOKEN_SECRET || process.env.PBS_USER || process.env.PBS_PASSWORD) {
|
||||
// Warn if some but not all required PBS vars are set
|
||||
console.warn("WARN: Partial PBS configuration found. Please set PBS_HOST and either (PBS_TOKEN_ID + PBS_TOKEN_SECRET) or (PBS_USER + PBS_PASSWORD) to enable PBS monitoring.");
|
||||
|
||||
if (config) {
|
||||
pbsConfigs.push(config);
|
||||
return true; // Indicate a config was found and added
|
||||
}
|
||||
return true; // Indicate we should check the next index even if this one was partial/invalid
|
||||
}
|
||||
|
||||
// Load primary PBS config (index=null)
|
||||
loadPbsConfig();
|
||||
|
||||
// Load additional PBS configs (index=2, 3, ...)
|
||||
let pbsIndex = 2;
|
||||
while (loadPbsConfig(pbsIndex)) {
|
||||
pbsIndex++;
|
||||
}
|
||||
|
||||
if (pbsConfigs.length > 0) {
|
||||
console.log(`INFO: Loaded configuration for ${pbsConfigs.length} PBS instances.`);
|
||||
} else {
|
||||
console.log("INFO: No PBS instances configured.");
|
||||
}
|
||||
// --- End PBS Configuration Loading ---
|
||||
|
||||
@@ -321,54 +358,69 @@ async function getPbsAuthTicketAndSetupClient(config) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initializePbsClient() {
|
||||
if (pbsConfig) {
|
||||
if (pbsConfig.authMethod === 'userpass') {
|
||||
pbsApiClient = await getPbsAuthTicketAndSetupClient(pbsConfig);
|
||||
if (pbsApiClient) {
|
||||
console.log(`INFO: Initialized API client for PBS (User/Password Auth): ${pbsConfig.name} (${pbsConfig.host})`);
|
||||
} else {
|
||||
console.error("ERROR: Failed to initialize PBS client using User/Password.");
|
||||
}
|
||||
} else if (pbsConfig.authMethod === 'token') {
|
||||
// Original Token Auth Logic
|
||||
const pbsBaseURL = pbsConfig.host.includes('://')
|
||||
? `${pbsConfig.host}/api2/json`
|
||||
: `https://${pbsConfig.host}:${pbsConfig.port}/api2/json`;
|
||||
async function initializeAllPbsClients() {
|
||||
if (pbsConfigs.length === 0) return;
|
||||
|
||||
const pbsAxiosInstance = axios.create({
|
||||
baseURL: pbsBaseURL,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: !pbsConfig.allowSelfSignedCerts
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
console.log(`INFO: Initializing API clients for ${pbsConfigs.length} PBS instances...`);
|
||||
const initPromises = pbsConfigs.map(async (config) => {
|
||||
let clientData = null;
|
||||
try {
|
||||
if (config.authMethod === 'userpass') {
|
||||
clientData = await getPbsAuthTicketAndSetupClient(config);
|
||||
if (clientData) {
|
||||
console.log(`INFO: Initialized API client for PBS (User/Password Auth): ${config.name} (${config.host})`);
|
||||
} else {
|
||||
console.error(`ERROR: Failed to initialize PBS client (User/Password) for: ${config.name} (${config.host})`);
|
||||
}
|
||||
});
|
||||
} else if (config.authMethod === 'token') {
|
||||
// Token Auth Logic (adapted from old initializePbsClient)
|
||||
const pbsBaseURL = config.host.includes('://')
|
||||
? `${config.host}/api2/json`
|
||||
: `https://${config.host}:${config.port}/api2/json`;
|
||||
|
||||
pbsAxiosInstance.interceptors.request.use(config => {
|
||||
config.headers.Authorization = `PBSAPIToken ${pbsConfig.tokenId}:${pbsConfig.tokenSecret}`;
|
||||
return config;
|
||||
});
|
||||
const pbsAxiosInstance = axios.create({
|
||||
baseURL: pbsBaseURL,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: !config.allowSelfSignedCerts
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
axiosRetry(pbsAxiosInstance, {
|
||||
retries: 3,
|
||||
retryDelay: (retryCount, error) => {
|
||||
console.warn(`Retrying PBS API request (Token Auth - attempt ${retryCount}) due to error: ${error.message}`);
|
||||
return axiosRetry.exponentialDelay(retryCount);
|
||||
},
|
||||
retryCondition: (error) => {
|
||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
||||
},
|
||||
});
|
||||
pbsAxiosInstance.interceptors.request.use(reqConfig => {
|
||||
reqConfig.headers.Authorization = `PBSAPIToken ${config.tokenId}:${config.tokenSecret}`;
|
||||
return reqConfig;
|
||||
});
|
||||
|
||||
pbsApiClient = { client: pbsAxiosInstance, config: pbsConfig };
|
||||
console.log(`INFO: Initialized API client for PBS (Token Auth): ${pbsConfig.name} (${pbsConfig.host})`);
|
||||
axiosRetry(pbsAxiosInstance, {
|
||||
retries: 3,
|
||||
retryDelay: (retryCount, error) => {
|
||||
console.warn(`Retrying PBS API request for ${config.name} (Token Auth - attempt ${retryCount}) due to error: ${error.message}`);
|
||||
return axiosRetry.exponentialDelay(retryCount);
|
||||
},
|
||||
retryCondition: (error) => {
|
||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
||||
},
|
||||
});
|
||||
|
||||
clientData = { client: pbsAxiosInstance, config: config };
|
||||
console.log(`INFO: Initialized API client for PBS (Token Auth): ${config.name} (${config.host})`);
|
||||
} else {
|
||||
console.error(`ERROR: Unknown authMethod '${config.authMethod}' for PBS config: ${config.name}`);
|
||||
}
|
||||
|
||||
if (clientData) {
|
||||
pbsApiClients[config.id] = clientData; // Store successful client keyed by config ID
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`ERROR: Unhandled exception during PBS client initialization for ${config.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(initPromises);
|
||||
console.log(`INFO: Finished initializing PBS API clients. ${Object.keys(pbsApiClients).length} clients successfully initialized.`);
|
||||
}
|
||||
|
||||
if (Object.keys(apiClients).length === 0 && !pbsApiClient) {
|
||||
if (Object.keys(apiClients).length === 0 && pbsConfigs.length === 0) {
|
||||
console.error("\n--- Configuration Error ---");
|
||||
console.error("No enabled Proxmox VE or PBS endpoints could be configured. Please check your .env file and environment variables.");
|
||||
process.exit(1);
|
||||
@@ -562,7 +614,7 @@ let currentNodes = [];
|
||||
let currentVms = [];
|
||||
let currentContainers = [];
|
||||
let currentMetrics = [];
|
||||
let pbsData = { status: 'initializing' }; // Initial status
|
||||
let pbsDataArray = []; // Array to hold data for each PBS instance
|
||||
let isDiscoveryRunning = false; // Prevent concurrent discovery runs
|
||||
let isMetricsRunning = false; // Prevent concurrent metric runs
|
||||
let discoveryTimeoutId = null;
|
||||
@@ -705,8 +757,7 @@ async function fetchDataForNode(apiClient, endpointId, nodeName) {
|
||||
*/
|
||||
async function fetchDiscoveryData() {
|
||||
console.log("[Discovery Cycle] Starting fetch across all endpoints...");
|
||||
// Initialize with the *current* pbsData state to preserve status between fetches if a fetch fails
|
||||
let aggregatedResult = { nodes: [], vms: [], containers: [], pbs: pbsData || { status: 'initializing' } };
|
||||
let aggregatedResult = { nodes: [], vms: [], containers: [], pbs: [] }; // pbs is now an array
|
||||
|
||||
// --- PVE Data Fetching ---
|
||||
const pveEndpointIds = Object.keys(apiClients).filter(id => !apiClients[id].isPbs); // Exclude PBS client if present
|
||||
@@ -818,111 +869,113 @@ async function fetchDiscoveryData() {
|
||||
}
|
||||
|
||||
// --- Fetch PBS Data (if configured) ---
|
||||
let pbsFetchStatus = 'unconfigured'; // Default status if not configured
|
||||
let fetchedPbsData = null; // Store successfully fetched data
|
||||
const pbsClientIds = Object.keys(pbsApiClients);
|
||||
const pbsDataResults = []; // Array to hold results for each PBS instance
|
||||
|
||||
if (pbsApiClient) {
|
||||
console.log("INFO: Fetching PBS discovery data...");
|
||||
let pbsNodeName = pbsApiClient.config.nodeName; // Use configured name if available
|
||||
if (pbsClientIds.length > 0) {
|
||||
console.log(`INFO: Fetching discovery data for ${pbsClientIds.length} PBS instances...`);
|
||||
const pbsPromises = pbsClientIds.map(async (pbsClientId) => {
|
||||
const { client: pbsClientInstance, config: pbsInstanceConfig } = pbsApiClients[pbsClientId];
|
||||
const instanceName = pbsInstanceConfig.name; // Use the configured name
|
||||
let instanceData = {
|
||||
pbsEndpointId: pbsClientId, // Identifier for this PBS instance
|
||||
pbsInstanceName: instanceName, // Human-readable name
|
||||
status: 'error', // Default to error
|
||||
nodeName: pbsInstanceConfig.nodeName, // Start with configured node name
|
||||
backupTasks: { recentTasks: [], summary: {} },
|
||||
datastores: [],
|
||||
verificationTasks: { summary: {} },
|
||||
syncTasks: { summary: {} },
|
||||
pruneTasks: { summary: {} }
|
||||
};
|
||||
|
||||
if (!pbsApiClient.client) {
|
||||
console.error("ERROR: pbsApiClient.client is not initialized!");
|
||||
pbsFetchStatus = 'error'; // Set status to error if client isn't ready
|
||||
} else {
|
||||
if (!pbsNodeName) {
|
||||
pbsNodeName = await fetchPbsNodeName(pbsApiClient);
|
||||
if (pbsNodeName && pbsNodeName !== 'localhost') {
|
||||
pbsApiClient.config.nodeName = pbsNodeName;
|
||||
try {
|
||||
// Ensure client is valid (redundant check, should be caught in init)
|
||||
if (!pbsClientInstance) {
|
||||
throw new Error(`Client not initialized for PBS instance: ${instanceName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only proceed if we have a node name (or fallback 'localhost')
|
||||
if (pbsNodeName) {
|
||||
try {
|
||||
// Fetch all data concurrently
|
||||
// Determine node name if not pre-configured
|
||||
if (!instanceData.nodeName) {
|
||||
instanceData.nodeName = await fetchPbsNodeName({ client: pbsClientInstance, config: pbsInstanceConfig }); // Pass the object
|
||||
// Store detected name back in config for future use (within this run cycle)
|
||||
if (instanceData.nodeName && instanceData.nodeName !== 'localhost') {
|
||||
pbsInstanceConfig.nodeName = instanceData.nodeName; // Update the config object directly
|
||||
}
|
||||
}
|
||||
|
||||
// Only proceed if we have a node name
|
||||
if (instanceData.nodeName) {
|
||||
// Fetch all data concurrently for this instance
|
||||
const [
|
||||
backupTasks, // Now includes detailed recent tasks
|
||||
datastores,
|
||||
verificationTasks,
|
||||
syncTasks,
|
||||
pruneTasks
|
||||
backupTasksResult,
|
||||
datastoresResult,
|
||||
verificationTasksResult,
|
||||
syncTasksResult,
|
||||
pruneTasksResult
|
||||
] = await Promise.all([
|
||||
fetchPbsTaskData(pbsApiClient, pbsNodeName), // Enhanced function
|
||||
fetchPbsDatastoreData(pbsApiClient),
|
||||
fetchPbsTaskSummaryByType(pbsApiClient, pbsNodeName, ['verify']), // Fetch verification tasks
|
||||
fetchPbsTaskSummaryByType(pbsApiClient, pbsNodeName, ['sync']), // Fetch sync tasks
|
||||
fetchPbsTaskSummaryByType(pbsApiClient, pbsNodeName, ['garbage_collection', 'prune']) // Fetch GC/Prune tasks
|
||||
fetchPbsTaskData({ client: pbsClientInstance, config: pbsInstanceConfig }, instanceData.nodeName),
|
||||
fetchPbsDatastoreData({ client: pbsClientInstance, config: pbsInstanceConfig }),
|
||||
fetchPbsTaskSummaryByType({ client: pbsClientInstance, config: pbsInstanceConfig }, instanceData.nodeName, ['verify']),
|
||||
fetchPbsTaskSummaryByType({ client: pbsClientInstance, config: pbsInstanceConfig }, instanceData.nodeName, ['sync']),
|
||||
fetchPbsTaskSummaryByType({ client: pbsClientInstance, config: pbsInstanceConfig }, instanceData.nodeName, ['garbage_collection', 'prune'])
|
||||
]);
|
||||
|
||||
// Combine fetched data *only on success*
|
||||
fetchedPbsData = {
|
||||
backupTasks: backupTasks, // Contains { recentTasks: [], summary: {...} }
|
||||
datastores: datastores, // Contains GC status per datastore
|
||||
verificationTasks: verificationTasks, // Contains summary: {...}
|
||||
syncTasks: syncTasks, // Contains summary: {...}
|
||||
pruneTasks: pruneTasks, // Contains summary: {...} for GC/Prune
|
||||
nodeName: pbsNodeName,
|
||||
status: 'ok' // Overall status is OK if all fetches succeed
|
||||
};
|
||||
pbsFetchStatus = 'ok'; // Update status on successful fetch
|
||||
console.log("INFO: Finished fetching PBS discovery data successfully.");
|
||||
|
||||
} catch (pbsError) {
|
||||
console.error(`ERROR: Failed fetching PBS data during discovery cycle: ${pbsError.message}`);
|
||||
pbsFetchStatus = 'error'; // Set status to error on fetch failure
|
||||
if (pbsError.response?.status === 401) {
|
||||
console.error("ERROR: PBS API Authentication Expired or Invalid (401). Re-login needed.");
|
||||
}
|
||||
// Keep fetchedPbsData as null
|
||||
// Assign results to the instanceData object
|
||||
instanceData.backupTasks = backupTasksResult;
|
||||
instanceData.datastores = datastoresResult;
|
||||
instanceData.verificationTasks = verificationTasksResult;
|
||||
instanceData.syncTasks = syncTasksResult;
|
||||
instanceData.pruneTasks = pruneTasksResult;
|
||||
instanceData.status = 'ok'; // Mark as OK if all fetches succeeded
|
||||
console.log(`INFO: Finished fetching PBS discovery data successfully for: ${instanceName}`);
|
||||
} else {
|
||||
console.error(`ERROR: Could not determine node name for PBS instance ${instanceName}, cannot fetch task data.`);
|
||||
instanceData.status = 'error'; // Keep status as error
|
||||
}
|
||||
} else {
|
||||
console.error("ERROR: Could not determine PBS node name, cannot fetch PBS task data.");
|
||||
pbsFetchStatus = 'error'; // Error if node name is missing
|
||||
|
||||
} catch (pbsError) {
|
||||
console.error(`ERROR: Failed fetching PBS data for instance ${instanceName}: ${pbsError.message}`);
|
||||
instanceData.status = 'error'; // Ensure status is error on any failure
|
||||
if (pbsError.response?.status === 401) {
|
||||
console.error(`ERROR: PBS API Authentication Expired/Invalid (401) for ${instanceName}.`);
|
||||
}
|
||||
// instanceData already defaults to error state with empty data structures
|
||||
}
|
||||
}
|
||||
return instanceData; // Return the data object for this instance (ok or error state)
|
||||
}); // End map over pbsClientIds
|
||||
|
||||
const settledPbsResults = await Promise.allSettled(pbsPromises);
|
||||
settledPbsResults.forEach(result => {
|
||||
if (result.status === 'fulfilled') {
|
||||
pbsDataResults.push(result.value); // Add the instance data (ok or error)
|
||||
} else {
|
||||
// This should ideally not happen if errors are caught within the promise
|
||||
console.error(`ERROR: Unhandled rejection fetching PBS data: ${result.reason}`);
|
||||
// Could push a generic error object here if needed
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log("[Discovery Cycle] No PBS client configured.");
|
||||
pbsFetchStatus = 'unconfigured';
|
||||
// Ensure a default structure for unconfigured state
|
||||
fetchedPbsData = {
|
||||
backupTasks: { recentTasks: [], summary: { ok: 0, failed: 0, total: 0, lastOk: null, lastFailed: null } },
|
||||
datastores: [],
|
||||
verificationTasks: { summary: { ok: 0, failed: 0, total: 0, lastOk: null, lastFailed: null } },
|
||||
syncTasks: { summary: { ok: 0, failed: 0, total: 0, lastOk: null, lastFailed: null } },
|
||||
pruneTasks: { summary: { ok: 0, failed: 0, total: 0, lastOk: null, lastFailed: null } },
|
||||
nodeName: null,
|
||||
status: 'unconfigured'
|
||||
};
|
||||
console.log("[Discovery Cycle] No PBS instances configured or initialized.");
|
||||
// pbsDataResults remains empty
|
||||
}
|
||||
// --- End Fetch PBS Data ---
|
||||
|
||||
// --- Update Global State ---
|
||||
// Update pbsData only if the fetch was successful OR if it's the first time and status is unconfigured
|
||||
if (fetchedPbsData && (pbsFetchStatus === 'ok' || (pbsFetchStatus === 'unconfigured' && pbsData.status === 'initializing'))) {
|
||||
pbsData = fetchedPbsData;
|
||||
} else if (pbsFetchStatus === 'error') {
|
||||
// If fetch failed, update only the status field in the global pbsData
|
||||
// Preserve the last known good data for other fields
|
||||
pbsData = {
|
||||
...(pbsData || {}), // Keep existing data
|
||||
status: 'error', // Update status to error
|
||||
nodeName: pbsApiClient?.config?.nodeName || pbsData?.nodeName || null // Preserve node name if possible
|
||||
};
|
||||
// Ensure essential structures exist if pbsData was previously null/empty
|
||||
pbsData.backupTasks = pbsData.backupTasks || { recentTasks: [], summary: {} };
|
||||
pbsData.datastores = pbsData.datastores || [];
|
||||
pbsData.verificationTasks = pbsData.verificationTasks || { summary: {} };
|
||||
pbsData.syncTasks = pbsData.syncTasks || { summary: {} };
|
||||
pbsData.pruneTasks = pbsData.pruneTasks || { summary: {} };
|
||||
}
|
||||
// ---> CHANGE: Update pbsDataArray (the global state)
|
||||
pbsDataArray = pbsDataResults; // Replace the global state with the newly fetched array
|
||||
// <--- END CHANGE
|
||||
|
||||
// Always update PVE data
|
||||
aggregatedResult.nodes = tempNodes;
|
||||
aggregatedResult.vms = tempVms;
|
||||
aggregatedResult.containers = tempContainers;
|
||||
|
||||
// Add the final, potentially updated pbsData state to the result
|
||||
aggregatedResult.pbs = pbsData;
|
||||
// ---> CHANGE: Add the final pbsDataArray state to the result
|
||||
// aggregatedResult.pbs = pbsData;
|
||||
aggregatedResult.pbs = pbsDataArray;
|
||||
// <--- END CHANGE
|
||||
|
||||
if (DEBUG_METRICS) {
|
||||
console.log(`[Discovery Cycle] Aggregated PVE results. Total: ${aggregatedResult.nodes.length} nodes, ${aggregatedResult.vms.length} VMs, ${aggregatedResult.containers.length} containers across ${pveEndpointIds.length} configured PVE endpoints.`);
|
||||
@@ -1035,34 +1088,32 @@ async function fetchMetricsData(runningVms, runningContainers) {
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[socket] Client connected. Total clients: ${io.engine.clientsCount}`);
|
||||
|
||||
// ---> NEW: Send initial PBS status immediately <---
|
||||
let initialPbsStatus = 'unconfigured';
|
||||
if (pbsConfig) {
|
||||
initialPbsStatus = 'configured'; // Assume configured if ENV vars are set
|
||||
// We don't know the *actual* connection status yet,
|
||||
// discovery cycle will provide 'ok' or 'error' later.
|
||||
// ---> CHANGE: Send initial status based on pbsConfigs array
|
||||
let initialPbsStatuses = pbsConfigs.map(conf => ({
|
||||
pbsEndpointId: conf.id,
|
||||
pbsInstanceName: conf.name,
|
||||
status: 'configured' // Assume configured initially
|
||||
}));
|
||||
if (initialPbsStatuses.length === 0) {
|
||||
initialPbsStatuses.push({ pbsEndpointId: 'none', pbsInstanceName: 'None', status: 'unconfigured' });
|
||||
}
|
||||
console.log(`[socket] Sending initial PBS status to new client: ${initialPbsStatus}`);
|
||||
socket.emit('pbsInitialStatus', { status: initialPbsStatus });
|
||||
// ---> END NEW SECTION <---
|
||||
console.log(`[socket] Sending initial PBS status array to new client:`, initialPbsStatuses);
|
||||
socket.emit('pbsInitialStatus', initialPbsStatuses); // Send the array
|
||||
// ---> END CHANGE <---
|
||||
|
||||
// Send initial PVE/Metric data immediately if available
|
||||
// ---> CHANGE: Send initial data using pbsDataArray
|
||||
if (currentNodes.length > 0 || currentVms.length > 0 || currentContainers.length > 0) {
|
||||
console.log('[socket] Sending existing PVE/Metric data to new client.');
|
||||
// Send existing data but ensure pbs status reflects the initial one sent above,
|
||||
// as the global pbsData might not be initialized or might be stale if discovery hasn't run.
|
||||
let pbsToSend = pbsData || { tasks: {}, datastores: [], nodeName: null, status: initialPbsStatus };
|
||||
// Make sure the status aligns with what we just sent in pbsInitialStatus if global state is uninit
|
||||
if (pbsToSend.status === 'initializing') {
|
||||
pbsToSend.status = initialPbsStatus;
|
||||
}
|
||||
|
||||
// If pbsDataArray is empty but configs exist, send the initial configured statuses
|
||||
let pbsToSend = (pbsDataArray && pbsDataArray.length > 0) ? pbsDataArray : initialPbsStatuses.map(s => ({ ...s })); // Use copy
|
||||
|
||||
socket.emit('rawData', {
|
||||
nodes: currentNodes,
|
||||
vms: currentVms,
|
||||
containers: currentContainers,
|
||||
metrics: currentMetrics,
|
||||
pbs: pbsToSend // Send current global PBS state or initial status
|
||||
pbs: pbsToSend // Send the array of PBS data/statuses
|
||||
});
|
||||
} else {
|
||||
// If no data yet, trigger a discovery cycle (if not already running)
|
||||
@@ -1092,21 +1143,17 @@ async function runDiscoveryCycle() {
|
||||
isDiscoveryRunning = true;
|
||||
|
||||
try {
|
||||
const discoveryData = await fetchDiscoveryData(); // This now returns {nodes, vms, containers, pbs}
|
||||
const discoveryData = await fetchDiscoveryData(); // Returns {nodes, vms, containers, pbs: pbsDataArray}
|
||||
|
||||
// Update global state variables from the returned data
|
||||
currentNodes = discoveryData.nodes || []; // Use fallback for safety
|
||||
// Update global state variables
|
||||
currentNodes = discoveryData.nodes || [];
|
||||
currentVms = discoveryData.vms || [];
|
||||
currentContainers = discoveryData.containers || [];
|
||||
// Only update pbsData if it's present in the result (handles initial state)
|
||||
if (discoveryData.pbs) {
|
||||
pbsData = discoveryData.pbs;
|
||||
} else {
|
||||
// If fetchDiscoveryData somehow didn't return pbs, keep old state or default
|
||||
pbsData = pbsData || { tasks: {}, datastores: [], nodeName: null, status: 'error' };
|
||||
}
|
||||
// ---> CHANGE: Update pbsDataArray
|
||||
pbsDataArray = discoveryData.pbs || []; // Update the global array
|
||||
// <--- END CHANGE
|
||||
|
||||
// Emit combined data using the updated global variables
|
||||
// Emit combined data
|
||||
if (io.engine.clientsCount > 0) {
|
||||
if (DEBUG_METRICS) {
|
||||
console.log('[Discovery Cycle] Emitting updated structural data including PBS.');
|
||||
@@ -1115,7 +1162,7 @@ async function runDiscoveryCycle() {
|
||||
nodes: currentNodes,
|
||||
vms: currentVms,
|
||||
containers: currentContainers,
|
||||
pbs: pbsData // Use the updated global pbs state
|
||||
pbs: pbsDataArray
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1168,11 +1215,11 @@ async function runMetricCycle() {
|
||||
|
||||
// Emit combined data (always emit, even if metrics weren't updated this cycle)
|
||||
io.emit('rawData', {
|
||||
nodes: currentNodes, // Send current nodes/vms/cts state
|
||||
nodes: currentNodes,
|
||||
vms: currentVms,
|
||||
containers: currentContainers,
|
||||
pbs: pbsData, // ADDED: Include current PBS state
|
||||
metrics: currentMetrics // Send the newly fetched metrics
|
||||
pbs: pbsDataArray,
|
||||
metrics: currentMetrics
|
||||
});
|
||||
} else {
|
||||
// console.log('[Metrics Cycle] No running guests found, skipping metric fetch.');
|
||||
@@ -1181,7 +1228,7 @@ async function runMetricCycle() {
|
||||
io.emit('rawData', {
|
||||
nodes: currentNodes, vms: currentVms,
|
||||
containers: currentContainers, metrics: currentMetrics,
|
||||
pbs: pbsData // ADDED: Include current PBS state
|
||||
pbs: pbsDataArray
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1211,8 +1258,6 @@ runDiscoveryCycle(); // Run discovery first
|
||||
// Metrics will be triggered after discovery or by its own timer if clients connect later
|
||||
scheduleNextMetric(); // Start scheduling metrics right away
|
||||
|
||||
// --- End New Update Cycle Logic ---
|
||||
|
||||
// --- PBS Data Fetching Functions ---
|
||||
|
||||
async function fetchPbsNodeName(pbsClient) {
|
||||
@@ -1446,7 +1491,7 @@ async function fetchPbsDatastoreData(pbsClient) {
|
||||
|
||||
// Start the server
|
||||
async function startServer() {
|
||||
await initializePbsClient(); // Initialize PBS client (might fail)
|
||||
await initializeAllPbsClients();
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server listening on port ${PORT}`);
|
||||
|
||||
+259
-166
@@ -121,7 +121,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let containersData = [];
|
||||
let metricsData = [];
|
||||
let dashboardData = [];
|
||||
let pbsData = {}; // Add state for PBS data
|
||||
// ---> CHANGE: pbsData becomes pbsDataArray
|
||||
// let pbsData = {}; // Add state for PBS data
|
||||
let pbsDataArray = []; // Holds data for multiple PBS instances
|
||||
// <--- END CHANGE
|
||||
// Load saved sort state from localStorage or use defaults
|
||||
const savedSortState = JSON.parse(localStorage.getItem('pulseSortState')) || {};
|
||||
const sortState = {
|
||||
@@ -137,7 +140,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let filterStatus = 'all'; // New state variable for status filter
|
||||
let initialDataReceived = false; // Flag to control initial rendering
|
||||
let storageData = {}; // Add state for storage data
|
||||
let pbsConfigured = false; // Flag to track if PBS is configured
|
||||
// ---> REMOVE: pbsConfigured flag is less relevant now
|
||||
// let pbsConfigured = false; // Flag to track if PBS is configured
|
||||
// <--- END REMOVE
|
||||
|
||||
// --- Global Helper for Text Progress Bar ---
|
||||
const createProgressTextBarHTML = (percent, text, colorClass) => {
|
||||
@@ -1151,26 +1156,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
// ---> END MODIFICATION <---
|
||||
|
||||
let pbsStatusReceived = false; // Track if we got PBS status in *this* update
|
||||
// Only update pbsData if it exists in the incoming data
|
||||
// ---> CHANGE: Update pbsDataArray
|
||||
if (data.hasOwnProperty('pbs')) {
|
||||
pbsData = data.pbs;
|
||||
pbsStatusReceived = true;
|
||||
// Determine configuration status based on the *first* received status
|
||||
if (!initialDataReceived) {
|
||||
pbsConfigured = pbsData.status !== 'unconfigured';
|
||||
}
|
||||
// Expecting an array now
|
||||
pbsDataArray = Array.isArray(data.pbs) ? data.pbs : [];
|
||||
console.log(`[socket.on("rawData")] Updated pbsDataArray with ${pbsDataArray.length} instance(s).`);
|
||||
} else {
|
||||
// If pbs field is missing, KEEP the existing pbsData state.
|
||||
// If this is the *very first* data load and pbs is missing, assume unconfigured
|
||||
if (!initialDataReceived && !pbsStatusReceived) {
|
||||
pbsConfigured = false;
|
||||
pbsData = { status: 'unconfigured' }; // Set a default state
|
||||
}
|
||||
// We could optionally default it only if pbsData is currently null/undefined,
|
||||
// but preserving the last known state is safer.
|
||||
// pbsData = pbsData || { status: 'unconfigured' };
|
||||
// If pbs key is missing, preserve the existing array
|
||||
console.log('[socket.on("rawData")] PBS key missing in rawData, preserving existing pbsDataArray.');
|
||||
}
|
||||
// <--- END CHANGE
|
||||
|
||||
console.log('[socket.on("rawData")] Parsed data and updated stores');
|
||||
|
||||
// Set flag after first successful data parse
|
||||
@@ -1194,18 +1190,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// ---> NEW: Listener for initial PBS status <---
|
||||
socket.on('pbsInitialStatus', (data) => {
|
||||
console.log('[socket] Received pbsInitialStatus:', data);
|
||||
if (data && data.status) {
|
||||
// Update the UI immediately with the basic configuration status
|
||||
// We pass a minimal object because full data isn't available yet
|
||||
updatePbsInfo({ status: data.status });
|
||||
// Optionally update pbsConfigured flag if still needed elsewhere
|
||||
pbsConfigured = data.status !== 'unconfigured';
|
||||
// ---> CHANGE: Handle initial PBS status array
|
||||
socket.on('pbsInitialStatus', (pbsStatusArray) => {
|
||||
console.log('[socket] Received pbsInitialStatus array:', pbsStatusArray);
|
||||
if (Array.isArray(pbsStatusArray)) {
|
||||
// Update the global pbsDataArray with these initial statuses
|
||||
// This ensures the UI shows something before the first full discovery
|
||||
pbsDataArray = pbsStatusArray.map(statusInfo => ({
|
||||
...statusInfo, // includes pbsEndpointId, pbsInstanceName, status
|
||||
// Add default empty structures for other fields expected by updatePbsInfo
|
||||
backupTasks: { recentTasks: [], summary: {} },
|
||||
datastores: [],
|
||||
verificationTasks: { summary: {} },
|
||||
syncTasks: { summary: {} },
|
||||
pruneTasks: { summary: {} },
|
||||
nodeName: null // Node name isn't known yet
|
||||
}));
|
||||
// Trigger an immediate partial UI update for PBS status
|
||||
updatePbsInfo(pbsDataArray);
|
||||
} else {
|
||||
console.warn('[socket] Received non-array data for pbsInitialStatus:', pbsStatusArray);
|
||||
}
|
||||
});
|
||||
// ---> END NEW LISTENER <---
|
||||
// ---> END CHANGE
|
||||
|
||||
function requestFullData() {
|
||||
console.log("Requesting full data...");
|
||||
@@ -1292,11 +1299,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function updateAllUITables() {
|
||||
// Update UI tables using the currently stored data
|
||||
updateNodesTable(nodesData);
|
||||
updateVmsTable(vmsData);
|
||||
updateContainersTable(containersData);
|
||||
// updateVmsTable(vmsData); // No separate VM table
|
||||
// updateContainersTable(containersData); // No separate CT table
|
||||
refreshDashboardData(); // Process and update the main dashboard
|
||||
updateStorageInfo(storageData); // Update storage info tab
|
||||
updatePbsInfo(pbsData); // Update PBS info
|
||||
// ---> CHANGE: Pass pbsDataArray
|
||||
// updatePbsInfo(pbsData);
|
||||
updatePbsInfo(pbsDataArray);
|
||||
// <--- END CHANGE
|
||||
}
|
||||
|
||||
// Add a separate fetch for storage data, maybe less frequent?
|
||||
@@ -1438,7 +1448,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return `<span class="text-xs ${colorClass}">${gcStatus}</span>`;
|
||||
};
|
||||
|
||||
|
||||
// --- Function to Update Specific Task Summary Card ---
|
||||
function updatePbsTaskSummaryCard(prefix, summaryData) {
|
||||
const okEl = document.getElementById(`pbs-${prefix}-ok`);
|
||||
@@ -1475,152 +1484,236 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// --- Updated Function: Update PBS Info Section ---
|
||||
function updatePbsInfo(pbs) {
|
||||
// Add logging to inspect received data structure
|
||||
console.log('[updatePbsInfo] Received PBS data:', JSON.stringify(pbs, null, 2)); // Log the received object
|
||||
|
||||
// Get references to all elements
|
||||
const statusElement = document.getElementById('pbs-connection-status');
|
||||
const dsSection = document.getElementById('pbs-datastores-section');
|
||||
const dsTableBody = document.getElementById('pbs-datastores-table-body');
|
||||
const summariesSection = document.getElementById('pbs-tasks-summaries-section');
|
||||
const recentTasksSection = document.getElementById('pbs-recent-tasks-section');
|
||||
const recentTasksTableBody = document.getElementById('pbs-recent-tasks-table-body');
|
||||
// Old tasks section element (to ensure it remains hidden or remove if safe)
|
||||
const oldTasksSection = document.getElementById('pbs-tasks-section');
|
||||
|
||||
// Basic check for essential elements
|
||||
if (!statusElement || !dsSection || !dsTableBody || !summariesSection || !recentTasksSection || !recentTasksTableBody) {
|
||||
console.warn("One or more PBS UI elements not found, cannot update fully.");
|
||||
function updatePbsInfo(pbsArray) {
|
||||
const container = document.getElementById('pbs-instances-container');
|
||||
if (!container) {
|
||||
console.error("PBS container element #pbs-instances-container not found!");
|
||||
return;
|
||||
}
|
||||
// Hide old task section if it exists
|
||||
if (oldTasksSection) oldTasksSection.classList.add('hidden');
|
||||
container.innerHTML = ''; // Clear previous content
|
||||
|
||||
// ---> Ensure all detail sections are hidden initially <---
|
||||
dsSection.classList.add('hidden');
|
||||
summariesSection.classList.add('hidden');
|
||||
recentTasksSection.classList.add('hidden');
|
||||
// ---> End initial hide <---
|
||||
// Log the array being processed
|
||||
console.log('[updatePbsInfo] Processing PBS array:', pbsArray);
|
||||
|
||||
// Update Connection Status
|
||||
let statusText = 'Loading...';
|
||||
let showDetails = false;
|
||||
statusElement.className = 'mb-3 text-sm'; // Reset classes
|
||||
|
||||
switch (pbs.status) {
|
||||
case 'configured':
|
||||
statusText = `PBS Configured (${pbs.nodeName || '...'}), attempting connection...`;
|
||||
statusElement.classList.add('text-gray-600', 'dark:text-gray-400');
|
||||
showDetails = false;
|
||||
break;
|
||||
case 'ok':
|
||||
statusText = `Connected to PBS: ${pbs.nodeName || 'Unknown Node'}`;
|
||||
statusElement.classList.add('text-green-600', 'dark:text-green-400');
|
||||
showDetails = true;
|
||||
break;
|
||||
case 'error':
|
||||
statusText = `Error connecting to PBS: ${pbs.nodeName || 'Configured Host'}. Check Pulse logs.`;
|
||||
statusElement.classList.add('text-red-600', 'dark:text-red-400');
|
||||
showDetails = false;
|
||||
break;
|
||||
case 'unconfigured':
|
||||
statusText = 'PBS monitoring is not configured.';
|
||||
statusElement.classList.add('text-gray-600', 'dark:text-gray-400');
|
||||
showDetails = false;
|
||||
break;
|
||||
default:
|
||||
statusText = `PBS status: ${pbs.status || 'Unknown'}`;
|
||||
statusElement.classList.add('text-gray-600', 'dark:text-gray-400');
|
||||
showDetails = false;
|
||||
break;
|
||||
if (!Array.isArray(pbsArray) || pbsArray.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 dark:text-gray-400">No PBS instances configured or data available.</p>';
|
||||
return;
|
||||
}
|
||||
statusElement.textContent = statusText;
|
||||
|
||||
// Show/Hide Detail Sections based on connection status
|
||||
// Use toggle with the 'force' parameter based on showDetails
|
||||
dsSection.classList.toggle('hidden', !showDetails);
|
||||
summariesSection.classList.toggle('hidden', !showDetails);
|
||||
recentTasksSection.classList.toggle('hidden', !showDetails);
|
||||
// Create content for each PBS instance
|
||||
pbsArray.forEach((pbsInstance, index) => {
|
||||
const instanceId = pbsInstance.pbsEndpointId || `instance-${index}`; // Use ID from backend or generate one
|
||||
const instanceName = pbsInstance.pbsInstanceName || `PBS Instance ${index + 1}`;
|
||||
|
||||
// Update Datastores Table (only if showing details and data exists)
|
||||
if (showDetails && pbs.datastores) {
|
||||
dsTableBody.innerHTML = ''; // Clear previous rows
|
||||
if (pbs.datastores.length === 0) {
|
||||
dsTableBody.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-sm text-gray-400 text-center">No PBS datastores found or accessible.</td></tr>`;
|
||||
} else {
|
||||
pbs.datastores.forEach(ds => {
|
||||
const totalBytes = ds.total || 0;
|
||||
const usedBytes = ds.used || 0;
|
||||
// Calculate available, defaulting to 0 if total is null/0
|
||||
const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0);
|
||||
const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0;
|
||||
const usageColor = getUsageColor(usagePercent);
|
||||
const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A';
|
||||
const gcStatusHtml = getPbsGcStatusText(ds.gcStatus);
|
||||
// --- Create Wrapper Div for this instance ---
|
||||
const instanceWrapper = document.createElement('div');
|
||||
instanceWrapper.className = 'pbs-instance-section border border-gray-200 dark:border-gray-700 rounded p-4 bg-gray-50/30 dark:bg-gray-800/30';
|
||||
instanceWrapper.id = `pbs-instance-${instanceId}`;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50';
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 whitespace-nowrap">${ds.name || 'N/A'}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${ds.path || 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${formatBytes(usedBytes)}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${formatBytes(availableBytes)}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-center min-w-[150px]">${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'}</td>
|
||||
<td class="px-4 py-2 text-center whitespace-nowrap">${gcStatusHtml}</td>
|
||||
`;
|
||||
dsTableBody.appendChild(row);
|
||||
});
|
||||
// --- Instance Header (Name and Status) ---
|
||||
const headerDiv = document.createElement('div');
|
||||
headerDiv.className = 'flex justify-between items-center mb-3';
|
||||
|
||||
const instanceTitle = document.createElement('h3');
|
||||
instanceTitle.className = 'text-lg font-semibold text-gray-800 dark:text-gray-200';
|
||||
instanceTitle.textContent = instanceName;
|
||||
|
||||
const statusElement = document.createElement('div');
|
||||
statusElement.className = 'text-sm'; // Base class
|
||||
statusElement.id = `pbs-status-${instanceId}`;
|
||||
|
||||
headerDiv.appendChild(instanceTitle);
|
||||
headerDiv.appendChild(statusElement);
|
||||
instanceWrapper.appendChild(headerDiv);
|
||||
|
||||
// --- Determine Status Text and Detail Visibility ---
|
||||
let statusText = 'Loading...';
|
||||
let showDetails = false;
|
||||
let statusColorClass = 'text-gray-600 dark:text-gray-400';
|
||||
|
||||
switch (pbsInstance.status) {
|
||||
case 'configured':
|
||||
statusText = `Configured (${pbsInstance.nodeName || '...'}), attempting connection...`;
|
||||
statusColorClass = 'text-gray-600 dark:text-gray-400';
|
||||
showDetails = false;
|
||||
break;
|
||||
case 'ok':
|
||||
// ---> CHANGE: Simplify the 'ok' status text
|
||||
// statusText = `Connected: ${pbsInstance.nodeName || 'Unknown Node'}`;
|
||||
statusText = `Status: OK (${pbsInstance.nodeName || 'Unknown Node'})`; // Show Node Name in status
|
||||
// <--- END CHANGE
|
||||
statusColorClass = 'text-green-600 dark:text-green-400';
|
||||
showDetails = true;
|
||||
break;
|
||||
case 'error':
|
||||
statusText = `Error connecting. Check Pulse logs.`;
|
||||
statusColorClass = 'text-red-600 dark:text-red-400';
|
||||
showDetails = false;
|
||||
break;
|
||||
case 'unconfigured':
|
||||
statusText = 'Not configured.';
|
||||
statusColorClass = 'text-gray-600 dark:text-gray-400';
|
||||
showDetails = false;
|
||||
break;
|
||||
default:
|
||||
statusText = `Status: ${pbsInstance.status || 'Unknown'}`;
|
||||
statusColorClass = 'text-gray-600 dark:text-gray-400';
|
||||
showDetails = false;
|
||||
break;
|
||||
}
|
||||
} else if (!showDetails) {
|
||||
// Clear table if not showing details
|
||||
dsTableBody.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-sm text-gray-400 text-center">${statusText}</td></tr>`;
|
||||
}
|
||||
statusElement.textContent = statusText;
|
||||
statusElement.classList.add(...statusColorClass.split(' '));
|
||||
|
||||
// Update Task Summary Cards (only if showing details)
|
||||
if (showDetails) {
|
||||
// Access the nested summary objects correctly
|
||||
updatePbsTaskSummaryCard('tasks', pbs.backupTasks); // Pass the whole backupTasks object which contains .summary
|
||||
updatePbsTaskSummaryCard('verify', pbs.verificationTasks); // Pass the whole object
|
||||
updatePbsTaskSummaryCard('sync', pbs.syncTasks); // Pass the whole object
|
||||
updatePbsTaskSummaryCard('prune', pbs.pruneTasks); // Pass the whole object
|
||||
} else {
|
||||
// Clear all summary cards if not showing details
|
||||
updatePbsTaskSummaryCard('tasks', null);
|
||||
updatePbsTaskSummaryCard('verify', null);
|
||||
updatePbsTaskSummaryCard('sync', null);
|
||||
updatePbsTaskSummaryCard('prune', null);
|
||||
}
|
||||
// --- Create Containers for Details (Datastores, Summaries, Tasks) ---
|
||||
const detailsContainer = document.createElement('div');
|
||||
detailsContainer.className = `pbs-instance-details space-y-4 ${showDetails ? '' : 'hidden'}`;
|
||||
detailsContainer.id = `pbs-details-${instanceId}`;
|
||||
|
||||
// Update Recent Backup Tasks Table (only if showing details and data exists)
|
||||
// Access the nested recentTasks array correctly
|
||||
if (showDetails && pbs.backupTasks && pbs.backupTasks.recentTasks) {
|
||||
recentTasksTableBody.innerHTML = ''; // Clear previous rows
|
||||
const recentTasks = pbs.backupTasks.recentTasks; // Use the correct path
|
||||
|
||||
if (recentTasks.length === 0) {
|
||||
recentTasksTableBody.innerHTML = `<tr><td colspan="5" class="px-4 py-4 text-sm text-gray-400 text-center">No recent backup tasks found (last 7 days).</td></tr>`;
|
||||
} else {
|
||||
recentTasks.forEach(task => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50';
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${task.id || 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-center">${getPbsStatusIcon(task.status)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${formatPbsTimestamp(task.startTime)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${formatDuration(task.duration)}</td>
|
||||
<!-- <td class="px-4 py-2 text-right whitespace-nowrap">${task.size ? formatBytes(task.size) : 'N/A'}</td> -->
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500 font-mono truncate" title="${task.upid || 'N/A'}">${task.upid || 'N/A'}</td>
|
||||
`;
|
||||
recentTasksTableBody.appendChild(row);
|
||||
});
|
||||
// --- Datastores Section ---
|
||||
const dsSection = document.createElement('div');
|
||||
dsSection.id = `pbs-ds-section-${instanceId}`;
|
||||
dsSection.innerHTML = `
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Datastores</h4>
|
||||
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded">
|
||||
<table id="pbs-ds-table-${instanceId}" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Name</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Path</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Used</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Available</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Total</th>
|
||||
<th scope="col" class="px-4 py-2 text-center font-semibold">Usage</th>
|
||||
<th scope="col" class="px-4 py-2 text-center font-semibold">GC Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pbs-ds-tbody-${instanceId}" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- JS fills this -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
detailsContainer.appendChild(dsSection);
|
||||
|
||||
// Populate Datastore Table Body
|
||||
const dsTableBody = dsSection.querySelector(`#pbs-ds-tbody-${instanceId}`);
|
||||
if (showDetails && pbsInstance.datastores) {
|
||||
if (pbsInstance.datastores.length === 0) {
|
||||
dsTableBody.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-sm text-gray-400 text-center">No PBS datastores found or accessible.</td></tr>`;
|
||||
} else {
|
||||
pbsInstance.datastores.forEach(ds => {
|
||||
const totalBytes = ds.total || 0;
|
||||
const usedBytes = ds.used || 0;
|
||||
const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0);
|
||||
const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0;
|
||||
const usageColor = getUsageColor(usagePercent);
|
||||
const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A';
|
||||
const gcStatusHtml = getPbsGcStatusText(ds.gcStatus);
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50';
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 whitespace-nowrap">${ds.name || 'N/A'}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${ds.path || 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${formatBytes(usedBytes)}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${formatBytes(availableBytes)}</td>
|
||||
<td class="px-4 py-2 text-right whitespace-nowrap">${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-center min-w-[150px]">${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'}</td>
|
||||
<td class="px-4 py-2 text-center whitespace-nowrap">${gcStatusHtml}</td>
|
||||
`;
|
||||
dsTableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
} else if (!showDetails) {
|
||||
dsTableBody.innerHTML = `<tr><td colspan="7" class="px-4 py-4 text-sm text-gray-400 text-center">${statusText}</td></tr>`;
|
||||
}
|
||||
} else if (!showDetails) {
|
||||
// Clear table if not showing details
|
||||
recentTasksTableBody.innerHTML = `<tr><td colspan="5" class="px-4 py-4 text-sm text-gray-400 text-center">${statusText}</td></tr>`;
|
||||
}
|
||||
|
||||
// --- Task Summaries Section ---
|
||||
const summariesSection = document.createElement('div');
|
||||
summariesSection.id = `pbs-summaries-section-${instanceId}`;
|
||||
summariesSection.className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4';
|
||||
|
||||
// Helper to create a summary card
|
||||
const createSummaryCard = (type, title, summaryData) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-100/50 dark:bg-gray-700/50';
|
||||
const summary = summaryData?.summary || {}; // Default to empty if data missing
|
||||
const ok = summary.ok ?? '-';
|
||||
const failed = summary.failed ?? '-';
|
||||
const total = summary.total ?? '-';
|
||||
const lastOk = formatPbsTimestamp(summary.lastOk);
|
||||
const lastFailed = formatPbsTimestamp(summary.lastFailed);
|
||||
const failedStyle = (failed > 0) ? 'font-bold text-red-600 dark:text-red-400' : 'text-red-600 dark:text-red-400 font-semibold';
|
||||
|
||||
card.innerHTML = `
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">${title} (7d)</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">OK:</span> <span class="ml-1 text-green-600 dark:text-green-400 font-semibold">${ok}</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Failed:</span> <span class="ml-1 ${failedStyle}">${failed}</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Total:</span> <span class="ml-1 text-gray-700 dark:text-gray-300 font-semibold">${total}</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last OK:</span> <span class="ml-1 text-gray-600 dark:text-gray-400 text-xs">${lastOk}</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last Fail:</span> <span class="ml-1 text-gray-600 dark:text-gray-400 text-xs">${lastFailed}</span></div>
|
||||
</div>`;
|
||||
return card;
|
||||
};
|
||||
|
||||
summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks));
|
||||
summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks));
|
||||
summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks));
|
||||
summariesSection.appendChild(createSummaryCard('prune', 'Prune/GC', pbsInstance.pruneTasks));
|
||||
detailsContainer.appendChild(summariesSection);
|
||||
|
||||
// --- Recent Backup Tasks Section ---
|
||||
const recentTasksSection = document.createElement('div');
|
||||
recentTasksSection.id = `pbs-recent-tasks-section-${instanceId}`;
|
||||
recentTasksSection.innerHTML = `
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Recent Backup Tasks</h4>
|
||||
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded max-h-96 overflow-y-auto">
|
||||
<table id="pbs-recent-tasks-table-${instanceId}" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-100 dark:bg-gray-700/50 sticky top-0">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Guest</th>
|
||||
<th scope="col" class="px-4 py-2 text-center font-semibold">Status</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Start Time</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Duration</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">UPID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pbs-recent-tasks-tbody-${instanceId}" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- JS fills this -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
detailsContainer.appendChild(recentTasksSection);
|
||||
|
||||
// Populate Recent Tasks Table Body
|
||||
const recentTasksTbody = recentTasksSection.querySelector(`#pbs-recent-tasks-tbody-${instanceId}`);
|
||||
if (showDetails && pbsInstance.backupTasks && pbsInstance.backupTasks.recentTasks) {
|
||||
const recentTasks = pbsInstance.backupTasks.recentTasks;
|
||||
if (recentTasks.length === 0) {
|
||||
recentTasksTbody.innerHTML = `<tr><td colspan="5" class="px-4 py-4 text-sm text-gray-400 text-center">No recent backup tasks found (last 7 days).</td></tr>`;
|
||||
} else {
|
||||
recentTasks.forEach(task => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50';
|
||||
row.innerHTML = `
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-600 dark:text-gray-300">${task.id || 'N/A'}</td>
|
||||
<td class="px-4 py-2 text-center">${getPbsStatusIcon(task.status)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${formatPbsTimestamp(task.startTime)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-gray-500 dark:text-gray-400">${formatDuration(task.duration)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs text-gray-400 dark:text-gray-500 font-mono truncate" title="${task.upid || 'N/A'}">${task.upid || 'N/A'}</td>
|
||||
`;
|
||||
recentTasksTbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
} else if (!showDetails) {
|
||||
recentTasksTbody.innerHTML = `<tr><td colspan="5" class="px-4 py-4 text-sm text-gray-400 text-center">${statusText}</td></tr>`;
|
||||
}
|
||||
|
||||
// Append details container (conditionally hidden) to the main wrapper
|
||||
instanceWrapper.appendChild(detailsContainer);
|
||||
|
||||
// Append the wrapper for this instance to the main container
|
||||
container.appendChild(instanceWrapper);
|
||||
}); // End forEach pbsInstance
|
||||
}
|
||||
// --- End Update PBS Info Function ---
|
||||
|
||||
|
||||
+5
-94
@@ -218,100 +218,11 @@
|
||||
<!-- Add PBS Tab Content -->
|
||||
<div id="pbs" class="tab-content hidden bg-white dark:bg-gray-800 rounded-b rounded-tr shadow p-3 mb-2">
|
||||
<h2 class="text-lg font-medium mb-2 text-gray-800 dark:text-gray-200">Proxmox Backup Server Status</h2>
|
||||
<div id="pbs-connection-status" class="mb-3 text-sm text-gray-600 dark:text-gray-400">Loading PBS status...</div>
|
||||
|
||||
<!-- Datastore Summary -->
|
||||
<div id="pbs-datastores-section" class="mb-4 hidden">
|
||||
<h3 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Datastores</h3>
|
||||
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded">
|
||||
<table id="pbs-datastores-table" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Name</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Path</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Used</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Available</th>
|
||||
<th scope="col" class="px-4 py-2 text-right font-semibold">Total</th>
|
||||
<th scope="col" class="px-4 py-2 text-center font-semibold">Usage</th>
|
||||
<th scope="col" class="px-4 py-2 text-center font-semibold">GC Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pbs-datastores-table-body" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Datastore rows will be inserted here -->
|
||||
<tr><td colspan="7" class="px-4 py-4 text-sm text-gray-400 text-center">Loading datastore data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Summaries (Grid Layout) -->
|
||||
<div id="pbs-tasks-summaries-section" class="mb-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 hidden">
|
||||
<!-- Backup Task Summary -->
|
||||
<div id="pbs-backup-summary-card" class="border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Backups (7d)</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">OK:</span> <span id="pbs-tasks-ok" class="ml-1 text-green-600 dark:text-green-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Failed:</span> <span id="pbs-tasks-failed" class="ml-1 text-red-600 dark:text-red-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Total:</span> <span id="pbs-tasks-total" class="ml-1 text-gray-700 dark:text-gray-300 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last OK:</span> <span id="pbs-tasks-last-ok" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last Fail:</span> <span id="pbs-tasks-last-failed" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Verification Task Summary -->
|
||||
<div id="pbs-verify-summary-card" class="border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Verification (7d)</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">OK:</span> <span id="pbs-verify-ok" class="ml-1 text-green-600 dark:text-green-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Failed:</span> <span id="pbs-verify-failed" class="ml-1 text-red-600 dark:text-red-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Total:</span> <span id="pbs-verify-total" class="ml-1 text-gray-700 dark:text-gray-300 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last OK:</span> <span id="pbs-verify-last-ok" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last Fail:</span> <span id="pbs-verify-last-failed" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sync Task Summary -->
|
||||
<div id="pbs-sync-summary-card" class="border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Sync (7d)</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">OK:</span> <span id="pbs-sync-ok" class="ml-1 text-green-600 dark:text-green-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Failed:</span> <span id="pbs-sync-failed" class="ml-1 text-red-600 dark:text-red-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Total:</span> <span id="pbs-sync-total" class="ml-1 text-gray-700 dark:text-gray-300 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last OK:</span> <span id="pbs-sync-last-ok" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last Fail:</span> <span id="pbs-sync-last-failed" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Prune/GC Task Summary -->
|
||||
<div id="pbs-prune-summary-card" class="border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-50/50 dark:bg-gray-700/30">
|
||||
<h4 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Prune/GC (7d)</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">OK:</span> <span id="pbs-prune-ok" class="ml-1 text-green-600 dark:text-green-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Failed:</span> <span id="pbs-prune-failed" class="ml-1 text-red-600 dark:text-red-400 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Total:</span> <span id="pbs-prune-total" class="ml-1 text-gray-700 dark:text-gray-300 font-semibold">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last OK:</span> <span id="pbs-prune-last-ok" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
<div><span class="font-medium text-gray-800 dark:text-gray-200">Last Fail:</span> <span id="pbs-prune-last-failed" class="ml-1 text-gray-600 dark:text-gray-400 text-xs">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Recent Backup Task List -->
|
||||
<div id="pbs-recent-tasks-section" class="mb-4 hidden">
|
||||
<h3 class="text-md font-semibold mb-2 text-gray-700 dark:text-gray-300">Recent Backup Tasks</h3>
|
||||
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700 rounded max-h-96 overflow-y-auto">
|
||||
<table id="pbs-recent-tasks-table" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 sticky top-0">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Guest</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Status</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Start Time</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">Duration</th>
|
||||
<th scope="col" class="px-4 py-2 text-left font-semibold">UPID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pbs-recent-tasks-table-body" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Recent task rows will be inserted here -->
|
||||
<tr><td colspan="5" class="px-4 py-4 text-sm text-gray-400 text-center">Loading recent task data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Container for dynamically generated PBS instance data -->
|
||||
<div id="pbs-instances-container" class="space-y-6">
|
||||
<!-- Loading message while JS populates -->
|
||||
<p class="text-gray-500 dark:text-gray-400">Loading PBS data...</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+87
-63
@@ -554,35 +554,59 @@ video {
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
.\!container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.\!container {
|
||||
max-width: 640px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.\!container {
|
||||
max-width: 768px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.\!container {
|
||||
max-width: 1024px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1024px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.\!container {
|
||||
max-width: 1280px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.\!container {
|
||||
max-width: 1536px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1536px;
|
||||
}
|
||||
@@ -649,10 +673,6 @@ video {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
@@ -705,6 +725,10 @@ video {
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
@@ -721,18 +745,14 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
.max-h-96 {
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
.max-h-\[80vh\] {
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.max-h-96 {
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
.w-1\/12 {
|
||||
width: 8.333333%;
|
||||
}
|
||||
@@ -749,6 +769,10 @@ video {
|
||||
width: 16.666667%;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-3\/12 {
|
||||
width: 25%;
|
||||
}
|
||||
@@ -777,18 +801,14 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
.min-w-\[150px\] {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.min-w-full {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.min-w-\[150px\] {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.max-w-\[95\%\] {
|
||||
max-width: 95%;
|
||||
}
|
||||
@@ -881,6 +901,10 @@ video {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-1\.5 {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -893,16 +917,24 @@ video {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-1\.5 {
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -979,18 +1011,23 @@ video {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.border-l {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.border-b-2 {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-l {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.border-t-2 {
|
||||
border-top-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||
@@ -1005,11 +1042,6 @@ video {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
@@ -1025,6 +1057,10 @@ video {
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-100\/50 {
|
||||
background-color: rgb(243 244 246 / 0.5);
|
||||
}
|
||||
|
||||
.bg-gray-200 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||
@@ -1040,6 +1076,10 @@ video {
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-50\/30 {
|
||||
background-color: rgb(249 250 251 / 0.3);
|
||||
}
|
||||
|
||||
.bg-gray-900\/90 {
|
||||
background-color: rgb(17 24 39 / 0.9);
|
||||
}
|
||||
@@ -1094,10 +1134,6 @@ video {
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-50\/50 {
|
||||
background-color: rgb(249 250 251 / 0.5);
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
@@ -1203,14 +1239,14 @@ video {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-sans {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
@@ -1295,6 +1331,11 @@ video {
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity));
|
||||
@@ -1335,6 +1376,11 @@ video {
|
||||
color: rgb(252 165 165 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity));
|
||||
@@ -1360,16 +1406,6 @@ video {
|
||||
color: rgb(202 138 4 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -1651,6 +1687,10 @@ video {
|
||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800\/30:is(.dark *) {
|
||||
background-color: rgb(31 41 55 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||
@@ -1698,10 +1738,6 @@ video {
|
||||
background-color: rgb(19 78 74 / 0.5);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-700\/30:is(.dark *) {
|
||||
background-color: rgb(55 65 81 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:text-blue-300:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(147 197 253 / var(--tw-text-opacity));
|
||||
@@ -1883,18 +1919,6 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
.md\:col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.md\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user