diff --git a/CHANGELOG_TEMP.md b/CHANGELOG_TEMP.md deleted file mode 100644 index 80aee0782..000000000 --- a/CHANGELOG_TEMP.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog for v3.22.1 - -## ✨ Features -- [fddc9c2](https://github.com/rcourtman/pulse/commit/fddc9c2) feat: implement sudoless update system with polkit integration - -## 🔄 All Changes -- [fddc9c2](https://github.com/rcourtman/pulse/commit/fddc9c2) feat: implement sudoless update system with polkit integration - -### Key Improvements -- **Sudoless Updates**: No more manual sudo commands required for updates -- **Polkit Integration**: Secure privilege escalation for service restarts -- **Enhanced Update Process**: Multi-strategy restart approach with better reliability -- **Comprehensive Testing**: New test suite covering update workflows -- **Improved UI**: Better progress tracking and user feedback during updates -- **Automatic Setup**: Install script now automatically configures polkit rules - -### Technical Details -- Added polkit rule for service management without sudo -- Fixed npm dependency conflicts during update extraction -- Implemented graceful fallback strategies for service restart -- Enhanced frontend with new 'restarting' phase indicator -- Resolved race conditions in update completion flow - -This release significantly improves the update experience by eliminating the need for manual intervention during the update process. \ No newline at end of file diff --git a/ProxmoxVE b/ProxmoxVE new file mode 160000 index 000000000..1e8c5036e --- /dev/null +++ b/ProxmoxVE @@ -0,0 +1 @@ +Subproject commit 1e8c5036e85f0a7ed6e97b7f643a3971e2de5836 diff --git a/commits.txt b/commits.txt deleted file mode 100644 index 0d66c6c57..000000000 --- a/commits.txt +++ /dev/null @@ -1 +0,0 @@ -fddc9c2 feat: implement sudoless update system with polkit integration diff --git a/server/configLoader.js b/server/configLoader.js index 634d15c0a..2a6dca116 100644 --- a/server/configLoader.js +++ b/server/configLoader.js @@ -34,9 +34,28 @@ class ConfigurationError extends Error { // Function to get update channel preference function getUpdateChannelPreference() { - const updateChannel = process.env.UPDATE_CHANNEL || 'stable'; - const validChannels = ['stable', 'rc']; + const fs = require('fs'); + const path = require('path'); + // Try to read from config/.env file first, then fallback to default + const configDir = path.join(__dirname, '../config'); + const configEnvPath = path.join(configDir, '.env'); + + let updateChannel = 'stable'; // Default value + + if (fs.existsSync(configEnvPath)) { + try { + const configContent = fs.readFileSync(configEnvPath, 'utf8'); + const updateChannelMatch = configContent.match(/^UPDATE_CHANNEL=(.+)$/m); + if (updateChannelMatch) { + updateChannel = updateChannelMatch[1].trim(); + } + } catch (error) { + console.warn('WARN: Could not read UPDATE_CHANNEL from config file. Using default "stable".'); + } + } + + const validChannels = ['stable', 'rc']; if (!validChannels.includes(updateChannel)) { console.warn(`WARN: Invalid UPDATE_CHANNEL value "${updateChannel}". Using default "stable".`); return 'stable'; diff --git a/server/tests/integration.test.js b/server/tests/integration.test.js index d152730af..85d29a29d 100644 --- a/server/tests/integration.test.js +++ b/server/tests/integration.test.js @@ -502,130 +502,224 @@ describe('Pulse Integration Tests', () => { }); }); - describe('Configuration-Based Workflow', () => { - test('should load configuration and initialize complete monitoring stack', async () => { - // === STEP 1: Mock configuration loading === - const mockConfig = { - endpoints: [{ - id: 'production-pve', - name: 'Production Cluster', - host: 'pve-prod.company.com', - port: '8006', - tokenId: 'monitor@pve!readonly', - tokenSecret: 'secret-token', - enabled: true, - allowSelfSignedCerts: false - }], - pbsConfigs: [{ - id: 'production-pbs', - name: 'Production Backup', - host: 'pbs-prod.company.com', - port: '8007', - tokenId: 'monitor@pbs!readonly', - tokenSecret: 'secret-pbs-token', - authMethod: 'token', - allowSelfSignedCerts: false - }] - }; - - // Mock the configuration loader - jest.doMock('../configLoader', () => ({ - loadConfiguration: jest.fn().mockReturnValue(mockConfig) - })); - - // === STEP 2: Mock API client initialization === - jest.doMock('../apiClients', () => ({ - initializeApiClients: jest.fn().mockResolvedValue({ - apiClients: mockApiClients, - pbsApiClients: mockPbsApiClients - }) - })); - - // === STEP 3: Simulate full initialization === - const { loadConfiguration: mockedLoadConfig } = require('../configLoader'); - const { initializeApiClients: mockedInitClients } = require('../apiClients'); - - const config = mockedLoadConfig(); - const { apiClients, pbsApiClients } = await mockedInitClients( - config.endpoints, - config.pbsConfigs - ); - - // === STEP 4: Verify configuration-driven setup === - expect(config.endpoints).toHaveLength(1); - expect(config.pbsConfigs).toHaveLength(1); - expect(config.endpoints[0].id).toBe('production-pve'); - expect(config.pbsConfigs[0].id).toBe('production-pbs'); - - expect(apiClients).toBeDefined(); - expect(pbsApiClients).toBeDefined(); - - // === STEP 5: Test monitoring with configured endpoints === - mockApiClients['pve-main'].client.get.mockResolvedValue({ - data: { data: [{ node: 'prod-node', status: 'online' }] } - }); - - const discoveryData = await fetchDiscoveryData(apiClients, pbsApiClients); - expect(discoveryData.nodes).toHaveLength(1); - expect(discoveryData.nodes[0].node).toBe('prod-node'); - }); - }); - - describe('Performance and Stress Scenarios', () => { - test('should handle large cluster with many guests efficiently', async () => { - const nodeCount = 5; - const guestsPerNode = 20; - const totalGuests = nodeCount * guestsPerNode; - - // === STEP 1: Mock large cluster === + describe('Real Production Workflow: Multi-Tenant Environment', () => { + test('should handle admin investigating cross-tenant resource conflicts', async () => { + // REAL SCENARIO: Admin gets reports of VMs interfering with each other's performance + // Multiple departments sharing the same cluster with different SLA requirements + + // Mock multi-tenant cluster data mockApiClients['pve-main'].client.get.mockImplementation((path) => { if (path === '/nodes') { - const nodes = Array.from({ length: nodeCount }, (_, i) => ({ - node: `node${i + 1}`, - status: 'online' - })); - return Promise.resolve({ data: { data: nodes } }); + return Promise.resolve({ + data: { + data: [ + { node: 'cluster1-node1', status: 'online' }, + { node: 'cluster1-node2', status: 'online' } + ] + } + }); } - - // Generate guests for each node - for (let nodeIndex = 1; nodeIndex <= nodeCount; nodeIndex++) { - if (path.includes(`/nodes/node${nodeIndex}/qemu`)) { - const vms = Array.from({ length: guestsPerNode / 2 }, (_, i) => ({ - vmid: nodeIndex * 1000 + i, - name: `vm-${nodeIndex}-${i}`, - status: 'running' - })); - return Promise.resolve({ data: { data: vms } }); + if (path.includes('/qemu')) { + if (path.includes('cluster1-node1')) { + return Promise.resolve({ + data: { + data: [ + { vmid: 1000, name: 'finance-db', status: 'running', tags: 'finance;critical' }, + { vmid: 1001, name: 'hr-app', status: 'running', tags: 'hr;standard' }, + { vmid: 1002, name: 'dev-test', status: 'running', tags: 'development;low' } + ] + } + }); } - if (path.includes(`/nodes/node${nodeIndex}/lxc`)) { - const containers = Array.from({ length: guestsPerNode / 2 }, (_, i) => ({ - vmid: nodeIndex * 1000 + 500 + i, - name: `ct-${nodeIndex}-${i}`, - status: 'running' - })); - return Promise.resolve({ data: { data: containers } }); + if (path.includes('cluster1-node2')) { + return Promise.resolve({ + data: { + data: [ + { vmid: 2000, name: 'marketing-web', status: 'running', tags: 'marketing;standard' }, + { vmid: 2001, name: 'analytics-worker', status: 'running', tags: 'analytics;high' } + ] + } + }); } } - + if (path.includes('/lxc')) { + return Promise.resolve({ data: { data: [] } }); + } return Promise.resolve({ data: { data: [] } }); }); - // === STEP 2: Measure discovery performance === - const startTime = Date.now(); - const discoveryData = await fetchDiscoveryData(mockApiClients, {}); - const discoveryTime = Date.now() - startTime; + const discoveryData = await fetchDiscoveryData(mockApiClients, mockPbsApiClients); - // === STEP 3: Verify scale handling === - expect(discoveryData.nodes).toHaveLength(nodeCount); - expect(discoveryData.vms).toHaveLength(nodeCount * (guestsPerNode / 2)); - expect(discoveryData.containers).toHaveLength(nodeCount * (guestsPerNode / 2)); + // ANALYZE: Resource distribution across departments + const departmentMapping = { + finance: discoveryData.vms.filter(vm => vm.tags?.includes('finance')), + hr: discoveryData.vms.filter(vm => vm.tags?.includes('hr')), + development: discoveryData.vms.filter(vm => vm.tags?.includes('development')), + marketing: discoveryData.vms.filter(vm => vm.tags?.includes('marketing')), + analytics: discoveryData.vms.filter(vm => vm.tags?.includes('analytics')) + }; - const totalDiscoveredGuests = discoveryData.vms.length + discoveryData.containers.length; - expect(totalDiscoveredGuests).toBe(totalGuests); + // VALIDATE: Multi-tenant separation + expect(departmentMapping.finance).toHaveLength(1); + expect(departmentMapping.analytics).toHaveLength(1); + + // DETECT: Potential resource conflicts + const criticalVMs = discoveryData.vms.filter(vm => vm.tags?.includes('critical')); + const nodeDistribution = {}; + discoveryData.vms.forEach(vm => { + if (!nodeDistribution[vm.node]) nodeDistribution[vm.node] = []; + nodeDistribution[vm.node].push(vm); + }); - // === STEP 4: Performance assertions === - expect(discoveryTime).toBeLessThan(5000); // Should complete within 5 seconds - console.log(`Integration test: Discovered ${totalGuests} guests across ${nodeCount} nodes in ${discoveryTime}ms`); + // VALIDATE: Critical VMs should not be overloaded on same node + const criticalNode = criticalVMs[0]?.node; + const vmsOnCriticalNode = nodeDistribution[criticalNode] || []; + + if (vmsOnCriticalNode.length > 2) { + console.warn(`RESOURCE CONFLICT: ${vmsOnCriticalNode.length} VMs on node with critical workload`); + } + + console.log(`Multi-tenant analysis: ${Object.keys(departmentMapping).length} departments across ${discoveryData.nodes.length} nodes`); + }); + }); + + describe('Real Operations: Disaster Recovery Testing', () => { + test('should help admin validate backup recovery process for critical VMs', async () => { + // REAL SCENARIO: Monthly DR test - admin needs to verify which VMs can be recovered + + // Mock PBS with realistic backup scenario + mockPbsApiClients['pbs-main'].client.get.mockImplementation((path) => { + if (path === '/nodes') { + return Promise.resolve({ data: { data: [{ node: 'pbs-dr' }] } }); + } + if (path === '/config/datastore') { + return Promise.resolve({ data: { data: [{ name: 'dr-backups' }] } }); + } + if (path.includes('/admin/datastore/dr-backups/snapshots')) { + const now = Math.floor(Date.now() / 1000); + return Promise.resolve({ + data: { + data: [ + // Critical systems with recent backups + { 'backup-id': '100', 'backup-type': 'vm', 'backup-time': now - 3600, size: 10737418240, protected: true }, + { 'backup-id': '101', 'backup-type': 'vm', 'backup-time': now - 3600, size: 5368709120, protected: true }, + // Development VM with older backup (acceptable) + { 'backup-id': '200', 'backup-type': 'vm', 'backup-time': now - 86400, size: 2147483648, protected: false }, + // Critical container with very recent backup + { 'backup-id': '300', 'backup-type': 'ct', 'backup-time': now - 1800, size: 1073741824, protected: true }, + // Test VM with gap in backups (concerning!) + { 'backup-id': '400', 'backup-type': 'vm', 'backup-time': now - 259200, size: 8589934592, protected: false } + ] + } + }); + } + return Promise.resolve({ data: { data: [] } }); + }); + + // Mock PVE discovery to correlate with backups + mockApiClients['pve-main'].client.get.mockImplementation((path) => { + if (path === '/nodes') { + return Promise.resolve({ data: { data: [{ node: 'production', status: 'online' }] } }); + } + if (path.includes('/qemu')) { + return Promise.resolve({ + data: { + data: [ + { vmid: 100, name: 'finance-app', status: 'running', tags: 'critical;finance' }, + { vmid: 101, name: 'customer-db', status: 'running', tags: 'critical;database' }, + { vmid: 200, name: 'dev-staging', status: 'running', tags: 'development' }, + { vmid: 400, name: 'legacy-system', status: 'running', tags: 'legacy;important' } + ] + } + }); + } + if (path.includes('/lxc')) { + return Promise.resolve({ + data: { data: [{ vmid: 300, name: 'web-proxy', status: 'running', tags: 'critical;web' }] } + }); + } + return Promise.resolve({ data: { data: [] } }); + }); + + const [discoveryData, pbsData] = await Promise.all([ + fetchDiscoveryData(mockApiClients, {}), + fetchPbsData(mockPbsApiClients) + ]); + + // ANALYZE: DR readiness for each system + const drAnalysis = { + criticalSystems: [], + warningItems: [], + gapDetected: [] + }; + + const allGuests = [...discoveryData.vms, ...discoveryData.containers]; + const allBackups = pbsData[0].datastores[0].snapshots; + + allGuests.forEach(guest => { + const backups = allBackups.filter(backup => + backup['backup-id'] === guest.vmid.toString() + ); + + if (backups.length === 0) { + drAnalysis.gapDetected.push({ + guest: guest.name, + vmid: guest.vmid, + issue: 'No backups found' + }); + return; + } + + const latestBackup = backups[0]; + const backupAge = (Date.now() / 1000) - latestBackup['backup-time']; + const ageInHours = backupAge / 3600; + + const isCritical = guest.tags?.includes('critical'); + + if (isCritical) { + drAnalysis.criticalSystems.push({ + guest: guest.name, + vmid: guest.vmid, + lastBackupAge: ageInHours, + protected: latestBackup.protected, + size: latestBackup.size + }); + + if (ageInHours > 6) { // Critical systems should be backed up within 6 hours + drAnalysis.warningItems.push({ + guest: guest.name, + vmid: guest.vmid, + issue: `Critical system backup ${Math.round(ageInHours)} hours old` + }); + } + } else if (ageInHours > 48) { // Non-critical can be up to 48 hours + drAnalysis.warningItems.push({ + guest: guest.name, + vmid: guest.vmid, + issue: `Backup ${Math.round(ageInHours)} hours old` + }); + } + }); + + // VALIDATE: DR test criteria + expect(drAnalysis.criticalSystems.length).toBeGreaterThan(0); + expect(drAnalysis.gapDetected.length).toBe(0); // No critical systems should lack backups + + // REPORT: DR readiness status + console.log(`DR Test Summary:`); + console.log(`- Critical systems monitored: ${drAnalysis.criticalSystems.length}`); + console.log(`- Warning items: ${drAnalysis.warningItems.length}`); + console.log(`- Backup gaps: ${drAnalysis.gapDetected.length}`); + + if (drAnalysis.warningItems.length > 0) { + console.log(`DR Warnings:`); + drAnalysis.warningItems.forEach(item => { + console.log(` - ${item.guest} (${item.vmid}): ${item.issue}`); + }); + } + + // This test would help identify DR readiness issues before they become problems + expect(drAnalysis.criticalSystems.every(sys => sys.lastBackupAge < 24)).toBe(true); }); test('should handle concurrent operations without race conditions', async () => { diff --git a/server/tests/userWorkflow.test.js b/server/tests/userWorkflow.test.js index 1387df20b..919e1cfd7 100644 --- a/server/tests/userWorkflow.test.js +++ b/server/tests/userWorkflow.test.js @@ -339,6 +339,227 @@ describe('Real User Workflows - Production Scenarios', () => { console.log(`Memory increase: ${Math.round(memoryIncrease / 1024 / 1024)}MB`); }); }); + + describe('Scenario 6: Admin Debugs "Slow Dashboard Loading"', () => { + test('should identify performance bottlenecks in data fetching', async () => { + // REAL SCENARIO: Dashboard taking 30+ seconds to load, admin needs to find why + + const mockApiClients = createRealisticMockClients(realApiData.pveCluster); + const performanceMetrics = { + discoveryStart: Date.now(), + nodeCallTimes: [], + totalApiCalls: 0 + }; + + // Monitor API call performance + const originalGet = mockApiClients.primary.client.get; + mockApiClients.primary.client.get = jest.fn().mockImplementation(async (path) => { + const callStart = Date.now(); + performanceMetrics.totalApiCalls++; + + // Simulate realistic response times for different endpoints + let delay = 100; // Default delay + if (path.includes('/qemu') || path.includes('/lxc')) { + delay = 500; // Guest endpoints are slower + } + if (path.includes('node3')) { + delay = 2000; // One node is slow (network issue) + } + + await new Promise(resolve => setTimeout(resolve, delay)); + const result = await originalGet.call(this, path); + + const callTime = Date.now() - callStart; + performanceMetrics.nodeCallTimes.push({ path, time: callTime }); + + return result; + }); + + const discoveryData = await fetchDiscoveryData(mockApiClients, {}); + const totalTime = Date.now() - performanceMetrics.discoveryStart; + + // ANALYZE: Performance bottlenecks + const slowCalls = performanceMetrics.nodeCallTimes.filter(call => call.time > 1000); + const avgCallTime = performanceMetrics.nodeCallTimes.reduce((sum, call) => sum + call.time, 0) / performanceMetrics.nodeCallTimes.length; + + // VALIDATE: Should identify the slow node + expect(slowCalls.length).toBeGreaterThan(0); + expect(slowCalls.some(call => call.path.includes('node3'))).toBe(true); + + // DETECT: Performance recommendations + if (avgCallTime > 500) { + console.log(`PERFORMANCE ISSUE: Average API call time ${Math.round(avgCallTime)}ms`); + } + if (totalTime > 5000) { + console.log(`PERFORMANCE ISSUE: Total discovery time ${totalTime}ms`); + } + + console.log(`Performance analysis: ${performanceMetrics.totalApiCalls} API calls, ${slowCalls.length} slow calls`); + slowCalls.forEach(call => { + console.log(` SLOW: ${call.path} took ${call.time}ms`); + }); + }); + }); + + describe('Scenario 7: Admin Investigates "Missing Backup Alerts"', () => { + test('should detect when backup monitoring is not working correctly', async () => { + // REAL SCENARIO: VM 102 hasn't been backed up in 3 days but no alerts fired + + const mockPbsClients = createRealisticPbsClients(realApiData.pbsBackups); + const pbsData = await fetchPbsData(mockPbsClients); + + // ANALYZE: Backup monitoring effectiveness + const allBackups = pbsData[0].datastores[0].snapshots; + const vm102Backups = allBackups.filter(snap => + snap['backup-id'] === '102' && snap['backup-type'] === 'vm' + ); + + expect(vm102Backups).toHaveLength(1); + + const vm102LastBackup = vm102Backups[0]; + const backupAge = (Date.now() / 1000) - vm102LastBackup['backup-time']; + const ageInDays = backupAge / (24 * 3600); + + // VALIDATE: Should detect old backup + expect(ageInDays).toBeGreaterThan(2); // More than 2 days old + + // SIMULATE: Alert system check + const mockAlertThreshold = 24 * 3600; // 24 hours + const shouldHaveAlerted = backupAge > mockAlertThreshold; + + // DETECT: Alert system gap + if (shouldHaveAlerted) { + console.log(`MONITORING GAP: VM 102 backup is ${Math.round(ageInDays * 10) / 10} days old, should have triggered alert`); + console.log(`Backup age: ${Math.round(backupAge / 3600)} hours (threshold: ${mockAlertThreshold / 3600} hours)`); + } + + // VALIDATE: This test helps identify why backup alerts aren't working + expect(shouldHaveAlerted).toBe(true); + + // RECOMMEND: Compare with other VMs to see pattern + const recentBackups = allBackups.filter(snap => { + const snapAge = (Date.now() / 1000) - snap['backup-time']; + return snapAge < (24 * 3600); // Less than 24 hours old + }); + + console.log(`Found ${recentBackups.length} recent backups vs ${allBackups.length} total`); + }); + }); + + describe('Scenario 8: Data Integrity Validation', () => { + test('should validate that all running VMs have corresponding metrics', async () => { + // REAL SCENARIO: Admin notices some VMs missing from metrics dashboard + + const mockApiClients = createRealisticMockClients(realApiData.pveCluster); + const discoveryData = await fetchDiscoveryData(mockApiClients, {}); + + const runningGuests = [ + ...discoveryData.vms.filter(vm => vm.status === 'running'), + ...discoveryData.containers.filter(ct => ct.status === 'running') + ]; + + // Mock metrics that might miss some guests + const mockMetricsApiClients = createRealisticMockClientsWithMetrics(realApiData.currentMetrics); + const metricsData = await fetchMetricsData( + discoveryData.vms.filter(vm => vm.status === 'running'), + discoveryData.containers.filter(ct => ct.status === 'running'), + mockMetricsApiClients + ); + + // DATA INTEGRITY CHECK: Every running guest should have metrics + const runningGuestIds = runningGuests.map(g => g.vmid); + const metricsGuestIds = metricsData.map(m => m.id); + + const missingMetrics = runningGuestIds.filter(id => !metricsGuestIds.includes(id)); + const extraMetrics = metricsGuestIds.filter(id => !runningGuestIds.includes(id)); + + // VALIDATE: Data consistency + expect(missingMetrics).toHaveLength(0); // No running guests should be missing metrics + expect(extraMetrics).toHaveLength(0); // No metrics for non-existent guests + + if (missingMetrics.length > 0) { + console.error(`DATA INTEGRITY ISSUE: ${missingMetrics.length} running guests missing metrics:`, missingMetrics); + } + if (extraMetrics.length > 0) { + console.error(`DATA INTEGRITY ISSUE: ${extraMetrics.length} metrics for non-running guests:`, extraMetrics); + } + + // VALIDATE: Metrics data quality + metricsData.forEach(metrics => { + expect(metrics.current).toBeDefined(); + expect(typeof metrics.current.cpu).toBe('number'); + expect(metrics.current.cpu).toBeGreaterThanOrEqual(0); + expect(metrics.current.cpu).toBeLessThanOrEqual(1); // Assuming decimal format + }); + + console.log(`Data integrity check: ${runningGuests.length} running guests, ${metricsData.length} metrics records`); + }); + }); + + describe('Scenario 9: Admin Responds to "Disk Space Critical" Alert', () => { + test('should help admin prioritize disk cleanup actions', async () => { + // REAL SCENARIO: Multiple disk space alerts, admin needs to know where to focus cleanup + + // Mock guests with varying disk usage + const diskPressureGuests = { + 106: { cpu: 0.12, memory: 536870912, disk: 0.92 }, // Pulse - 92% full + 200: { cpu: 0.15, memory: 2147483648, disk: 0.88 }, // UnraidServer - 88% full + 107: { cpu: 0.08, memory: 268435456, disk: 0.95 }, // Jellyfin - 95% full (critical!) + 108: { cpu: 0.22, memory: 1073741824, disk: 0.85 } // Frigate - 85% full + }; + + const mockApiClients = createRealisticMockClientsWithMetrics(diskPressureGuests); + const metricsData = await fetchMetricsData([], [ + { vmid: 106, name: 'pulse', status: 'running', endpointId: 'primary', node: 'minipc', type: 'lxc' }, + { vmid: 200, name: 'UnraidServer', status: 'running', endpointId: 'primary', node: 'desktop', type: 'qemu' }, + { vmid: 107, name: 'jellyfin', status: 'running', endpointId: 'primary', node: 'minipc', type: 'lxc' }, + { vmid: 108, name: 'frigate', status: 'running', endpointId: 'primary', node: 'delly', type: 'lxc' } + ], mockApiClients); + + // ANALYZE: Disk usage patterns + const diskMetrics = metricsData.map(m => ({ + id: m.id, + name: m.guestName, + diskUsage: m.current.disk * 100, + type: m.type + })).sort((a, b) => b.diskUsage - a.diskUsage); + + // PRIORITIZE: Critical vs warning levels + const criticalDisk = diskMetrics.filter(g => g.diskUsage > 90); // >90% + const warningDisk = diskMetrics.filter(g => g.diskUsage > 85 && g.diskUsage <= 90); // 85-90% + + // VALIDATE: Should identify jellyfin as highest priority + expect(criticalDisk).toHaveLength(2); // Jellyfin (95%) and Pulse (92%) + expect(criticalDisk[0].name).toBe('jellyfin'); + expect(criticalDisk[0].diskUsage).toBe(95); + + // RECOMMEND: Actions based on service type + const mediaServices = criticalDisk.filter(g => + ['jellyfin', 'plex', 'frigate'].includes(g.name.toLowerCase()) + ); + const systemServices = criticalDisk.filter(g => + ['pulse', 'pihole', 'homeassistant'].includes(g.name.toLowerCase()) + ); + + console.log('DISK CLEANUP PRIORITIES:'); + console.log(`CRITICAL (>90%): ${criticalDisk.length} services`); + criticalDisk.forEach(g => { + console.log(` - ${g.name}: ${g.diskUsage}% full`); + }); + + console.log(`WARNING (85-90%): ${warningDisk.length} services`); + + // GUIDANCE: Specific cleanup recommendations + if (mediaServices.length > 0) { + console.log('RECOMMENDATION: Check media files for cleanup (jellyfin, frigate)'); + } + if (systemServices.length > 0) { + console.log('RECOMMENDATION: Check logs and temporary files (pulse, system services)'); + } + + expect(criticalDisk.length).toBeGreaterThan(0); + }); + }); }); // Helper functions for realistic test data