Skip to content

Commit 28e4d9d

Browse files
committed
Release v1.9.1: Fix battery history time filter issues and update version numbers
1 parent 24c5306 commit 28e4d9d

File tree

12 files changed

+247
-47
lines changed

12 files changed

+247
-47
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to the PowerPulse project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.9.1] - 2025-03-01
9+
10+
### Fixed
11+
- Fixed battery history time filter issues:
12+
- Changed time filter options from 7 days to 3 days for better data representation
13+
- Improved client-side filtering to ensure only data within the requested time range is displayed
14+
- Added data sampling for multi-day views to get a more representative sample across the full time range
15+
- Increased record limit for multi-day views to provide better data coverage
16+
- Enhanced logging for better troubleshooting of time filter issues
17+
818
## [1.9.0] - 2025-03-01
919

1020
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PowerPulse
22

33
![PowerPulse Logo](https://img.shields.io/badge/PowerPulse-UPS%20Monitoring-blue)
4-
![Version](https://img.shields.io/badge/version-1.9.0-green)
4+
![Version](https://img.shields.io/badge/version-1.9.1-green)
55
![License](https://img.shields.io/badge/license-MIT-lightgrey)
66

77
PowerPulse is a modern UPS (Uninterruptible Power Supply) monitoring dashboard integrated with Network UPS Tools (NUT). It provides a clean, responsive interface for monitoring and managing your UPS systems.

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "powerpulse-client",
33
"private": true,
4-
"version": "1.9.0",
4+
"version": "1.9.1",
55
"author": "blink-zero",
66
"repository": {
77
"type": "git",

client/src/hooks/useBatteryHistory.js

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,35 @@ import { fetchBatteryHistory, recordBatteryCharge } from '../services/batteryHis
44
/**
55
* Custom hook for fetching and managing battery history data
66
* @param {number|null} upsId - The ID of the UPS system to fetch history for
7+
* @param {number} timeFilter - Number of days to fetch history for (default: 7)
78
* @returns {Object} - Battery history data and loading state
89
*/
9-
export const useBatteryHistory = (upsId) => {
10+
export const useBatteryHistory = (upsId, timeFilter = 7) => {
1011
const [batteryHistory, setBatteryHistory] = useState({
1112
labels: [],
1213
datasets: [],
1314
});
1415
const [loading, setLoading] = useState(false);
1516
const [error, setError] = useState(null);
17+
const [currentTimeFilter, setCurrentTimeFilter] = useState(timeFilter);
18+
19+
// Update the time filter when it changes
20+
useEffect(() => {
21+
setCurrentTimeFilter(timeFilter);
22+
}, [timeFilter]);
1623

1724
// Function to fetch and process battery history data
18-
const fetchAndProcessData = async () => {
25+
const fetchAndProcessData = async (days = currentTimeFilter) => {
1926
try {
2027
setLoading(true);
2128
setError(null);
2229

30+
console.log(`Fetching battery history for UPS ${upsId} with time filter: ${days} days`);
31+
2332
// Use our service to fetch the data
24-
const data = await fetchBatteryHistory(upsId);
33+
const data = await fetchBatteryHistory(upsId, days);
34+
35+
console.log(`Received ${data?.length || 0} data points for the last ${days} days`);
2536

2637
if (!data || data.length === 0) {
2738
console.log('No battery history data available');
@@ -36,17 +47,81 @@ export const useBatteryHistory = (upsId) => {
3647

3748
// Process the data for the chart
3849
// The API returns an array of objects with charge_percent and timestamp
50+
console.log('Raw data from API:', data);
51+
52+
// Sort the data by timestamp
3953
const sortedData = [...data].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
4054

55+
console.log(`Sorted data: ${sortedData.length} records`);
56+
57+
// Even though the server should filter the data, we'll also filter it on the client side
58+
// to ensure we only show data for the requested time period
59+
const now = new Date();
60+
const oldestAllowedDate = new Date(now);
61+
oldestAllowedDate.setDate(oldestAllowedDate.getDate() - days);
62+
console.log(`Client-side filtering: Only including records after ${oldestAllowedDate.toLocaleString()}`);
63+
64+
// Filter the data to only include records within the requested time period
65+
const filteredData = sortedData.filter(item => {
66+
const timestamp = new Date(item.timestamp);
67+
return timestamp >= oldestAllowedDate;
68+
});
69+
70+
console.log(`After client-side filtering: ${filteredData.length} records (was ${sortedData.length})`);
71+
72+
// For multi-day views, ensure we get a good representation of the data across the full time range
73+
let finalData = filteredData;
74+
75+
if (days > 1) {
76+
console.log(`Ensuring good data representation for ${days}-day view`);
77+
78+
// Calculate the time range we want to cover (from now to N days ago)
79+
const now = new Date();
80+
const oldestAllowedDate = new Date(now);
81+
oldestAllowedDate.setDate(oldestAllowedDate.getDate() - days);
82+
83+
// If we don't have data going back the full requested time range,
84+
// we'll work with what we have and distribute it evenly
85+
86+
// Calculate how many data points we want per day (aim for about 12)
87+
const totalDesiredPoints = days * 12;
88+
89+
if (filteredData.length > totalDesiredPoints) {
90+
// If we have more points than desired, sample them
91+
const samplingInterval = Math.max(1, Math.floor(filteredData.length / totalDesiredPoints));
92+
93+
console.log(`Sampling interval: ${samplingInterval} (keeping 1 out of every ${samplingInterval} points)`);
94+
95+
// Sample the data at regular intervals
96+
finalData = filteredData.filter((_, index) => index % samplingInterval === 0);
97+
98+
// Always include the first and last points to ensure we cover the full time range
99+
if (finalData[0] !== filteredData[0]) {
100+
finalData.unshift(filteredData[0]);
101+
}
102+
if (finalData[finalData.length - 1] !== filteredData[filteredData.length - 1]) {
103+
finalData.push(filteredData[filteredData.length - 1]);
104+
}
105+
106+
console.log(`After sampling: ${finalData.length} records (was ${filteredData.length})`);
107+
} else {
108+
// If we have fewer points than desired, use all of them
109+
console.log(`Using all ${filteredData.length} records (fewer than desired ${totalDesiredPoints})`);
110+
111+
// If we have very few points, we might want to interpolate additional points
112+
// to get a smoother chart, but for now we'll just use what we have
113+
}
114+
}
115+
41116
// Check if the data has the expected properties
42117
let timestamps, charges;
43118

44-
if (sortedData.length > 0 && !sortedData[0].charge_percent) {
119+
if (finalData.length > 0 && !finalData[0].charge_percent) {
45120
console.warn('Data format issue: charge_percent not found in data');
46-
console.log('Data sample:', sortedData[0]);
121+
console.log('Data sample:', finalData[0]);
47122

48123
// Try to detect the correct property names
49-
const sampleItem = sortedData[0];
124+
const sampleItem = finalData[0];
50125
const chargeKey = Object.keys(sampleItem).find(key =>
51126
key.includes('charge') || key.includes('battery') ||
52127
(typeof sampleItem[key] === 'number' && sampleItem[key] >= 0 && sampleItem[key] <= 100)
@@ -59,8 +134,8 @@ export const useBatteryHistory = (upsId) => {
59134

60135
if (chargeKey && timeKey) {
61136
console.log(`Using detected keys: ${chargeKey} for charge and ${timeKey} for timestamp`);
62-
timestamps = sortedData.map(item => item[timeKey]);
63-
charges = sortedData.map(item => item[chargeKey]);
137+
timestamps = finalData.map(item => item[timeKey]);
138+
charges = finalData.map(item => item[chargeKey]);
64139
} else {
65140
console.error('Could not detect appropriate data keys');
66141
setBatteryHistory({
@@ -70,15 +145,47 @@ export const useBatteryHistory = (upsId) => {
70145
return;
71146
}
72147
} else {
73-
timestamps = sortedData.map(item => item.timestamp);
74-
charges = sortedData.map(item => item.charge_percent);
148+
timestamps = finalData.map(item => item.timestamp);
149+
charges = finalData.map(item => item.charge_percent);
75150
}
76151

77-
// Format timestamps for better display in a 7-day view
78-
const formattedLabels = timestamps.map(ts => {
79-
const date = new Date(ts);
80-
return date.toISOString(); // Return ISO string for proper date handling in the chart
152+
// Store the original Date objects for logging
153+
const dateObjects = timestamps.map(ts => new Date(ts));
154+
155+
// Format timestamps for better display
156+
const formattedLabels = dateObjects.map(date => {
157+
return date.toLocaleString([], {
158+
month: 'short',
159+
day: 'numeric',
160+
hour: '2-digit',
161+
minute: '2-digit'
162+
});
81163
});
164+
165+
// Log the date range of the data
166+
if (dateObjects.length > 0) {
167+
const oldestDate = dateObjects[0];
168+
const newestDate = dateObjects[dateObjects.length - 1];
169+
console.log(`Data date range: ${oldestDate.toLocaleString()} to ${newestDate.toLocaleString()}`);
170+
171+
// Calculate how many hours of data we have
172+
const hoursDiff = (newestDate - oldestDate) / (1000 * 60 * 60);
173+
console.log(`Data spans approximately ${hoursDiff.toFixed(1)} hours (${(hoursDiff / 24).toFixed(1)} days)`);
174+
175+
// Check if the data matches the requested time filter
176+
const now = new Date();
177+
const requestedOldestDate = new Date(now);
178+
requestedOldestDate.setDate(requestedOldestDate.getDate() - days);
179+
console.log(`Requested date range: ${requestedOldestDate.toLocaleString()} to ${now.toLocaleString()}`);
180+
181+
// Calculate the difference between the oldest date in the data and the requested oldest date
182+
const diffHours = (oldestDate - requestedOldestDate) / (1000 * 60 * 60);
183+
console.log(`Difference between requested and actual oldest date: ${diffHours.toFixed(1)} hours`);
184+
185+
if (diffHours > 24) {
186+
console.warn(`Warning: Data does not go back as far as requested. Missing approximately ${diffHours.toFixed(1)} hours of data.`);
187+
}
188+
}
82189

83190
setBatteryHistory({
84191
labels: formattedLabels,
@@ -128,14 +235,16 @@ export const useBatteryHistory = (upsId) => {
128235

129236
useEffect(() => {
130237
if (!upsId) return;
131-
fetchAndProcessData();
132-
}, [upsId]);
238+
fetchAndProcessData(currentTimeFilter);
239+
}, [upsId, currentTimeFilter]);
133240

134241
return {
135242
batteryHistory,
136243
loading,
137244
error,
138245
recordCurrentCharge,
139-
refreshData: fetchAndProcessData
246+
refreshData: fetchAndProcessData,
247+
currentTimeFilter,
248+
setTimeFilter: setCurrentTimeFilter
140249
};
141250
};

client/src/pages/Dashboard.jsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from 'react';
22
import axios from 'axios';
3-
import { FiBattery, FiAlertCircle, FiClock, FiThermometer, FiPercent, FiZap, FiActivity, FiRefreshCw, FiCalendar } from 'react-icons/fi';
3+
import { FiBattery, FiAlertCircle, FiClock, FiThermometer, FiPercent, FiZap, FiActivity, FiRefreshCw, FiCalendar, FiFilter } from 'react-icons/fi';
44
import { useSettings } from '../context/SettingsContext';
55
import { useBatteryHistory } from '../hooks/useBatteryHistory';
66
import { Line } from 'react-chartjs-2';
@@ -12,7 +12,8 @@ import {
1212
LineElement,
1313
Title,
1414
Tooltip,
15-
Legend
15+
Legend,
16+
Filler
1617
} from 'chart.js';
1718

1819
// Register Chart.js components
@@ -23,7 +24,8 @@ ChartJS.register(
2324
LineElement,
2425
Title,
2526
Tooltip,
26-
Legend
27+
Legend,
28+
Filler // Register the Filler plugin for the 'fill' option
2729
);
2830

2931
const Dashboard = () => {
@@ -32,13 +34,15 @@ const Dashboard = () => {
3234
const [loading, setLoading] = useState(true);
3335
const [error, setError] = useState(null);
3436
const [selectedUps, setSelectedUps] = useState(null);
37+
const [timeFilter, setTimeFilter] = useState(3); // Default to 3 days
3538
const {
3639
batteryHistory,
3740
loading: historyLoading,
3841
error: historyError,
3942
recordCurrentCharge,
40-
refreshData: refreshBatteryHistory
41-
} = useBatteryHistory(selectedUps?.id);
43+
refreshData: refreshBatteryHistory,
44+
currentTimeFilter
45+
} = useBatteryHistory(selectedUps?.id, timeFilter);
4246
const [lastUpdated, setLastUpdated] = useState(new Date());
4347

4448
useEffect(() => {
@@ -381,10 +385,33 @@ const Dashboard = () => {
381385
</h3>
382386
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 flex items-center">
383387
<FiCalendar className="mr-1" />
384-
Last 7 days
388+
Last {currentTimeFilter} days
385389
</span>
386390
</div>
387391
<div className="flex items-center">
392+
{/* Time filter dropdown */}
393+
<div className="mr-4 relative">
394+
<div className="flex items-center">
395+
<FiFilter className="h-4 w-4 text-gray-500 dark:text-gray-400 mr-1" />
396+
<select
397+
value={timeFilter}
398+
onChange={(e) => {
399+
const newTimeFilter = Number(e.target.value);
400+
console.log(`Changing time filter from ${timeFilter} to ${newTimeFilter} days`);
401+
setTimeFilter(newTimeFilter);
402+
// Explicitly refresh the data when the filter changes
403+
if (selectedUps) {
404+
setTimeout(() => refreshBatteryHistory(newTimeFilter), 100);
405+
}
406+
}}
407+
className="text-sm bg-transparent border-none text-gray-500 dark:text-gray-400 focus:ring-0 focus:outline-none cursor-pointer pr-8"
408+
>
409+
<option value={1}>Last 24 hours</option>
410+
<option value={3}>Last 3 days</option>
411+
</select>
412+
</div>
413+
</div>
414+
388415
{/* Only show error if we're not showing the placeholder chart */}
389416
{historyError && batteryHistory.labels.length > 0 && (
390417
<span className="text-xs text-red-500 mr-2">{historyError}</span>
@@ -441,11 +468,11 @@ const Dashboard = () => {
441468
maxRotation: 45,
442469
color: 'rgba(107, 114, 128, 0.8)',
443470
autoSkip: true,
444-
maxTicksLimit: 12,
471+
maxTicksLimit: currentTimeFilter === 1 ? 24 : 14, // Show more ticks for better visibility
445472
},
446473
title: {
447474
display: true,
448-
text: 'Time (Last 7 Days)',
475+
text: `Time (Last ${currentTimeFilter} ${currentTimeFilter === 1 ? 'Day' : 'Days'})`,
449476
color: 'rgba(107, 114, 128, 0.8)',
450477
}
451478
},
@@ -538,7 +565,7 @@ const Dashboard = () => {
538565
},
539566
title: {
540567
display: true,
541-
text: 'Time (Last 7 Days)',
568+
text: `Time (Last ${currentTimeFilter} ${currentTimeFilter === 1 ? 'Day' : 'Days'})`,
542569
color: 'rgba(107, 114, 128, 0.8)',
543570
}
544571
},

client/src/services/batteryHistoryService.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import axios from 'axios';
33
/**
44
* Fetches battery history data for a specific UPS system
55
* @param {number} upsId - The ID of the UPS system
6+
* @param {number} days - Number of days of history to fetch (default: 7)
67
* @returns {Promise<Array>} - Array of battery history records
78
*/
8-
export const fetchBatteryHistory = async (upsId) => {
9+
export const fetchBatteryHistory = async (upsId, days = 7) => {
910
try {
1011
// Try the standard endpoint first
1112
try {
@@ -23,9 +24,9 @@ export const fetchBatteryHistory = async (upsId) => {
2324
} catch (altErr) {
2425
console.warn('Alternative endpoint failed:', altErr.message);
2526

26-
// Try a direct database query using our debug endpoint
27-
try {
28-
const response = await axios.get(`/api/debug/battery-history/${upsId}?days=7`);
27+
// Try a direct database query using our debug endpoint
28+
try {
29+
const response = await axios.get(`/api/debug/battery-history/${upsId}?days=${days}`);
2930
console.log('Battery history from debug endpoint:', response.data);
3031

3132
// If we got data, return it
@@ -71,7 +72,7 @@ export const fetchBatteryHistory = async (upsId) => {
7172
console.log('Manual recording response:', recordResponse.data);
7273

7374
// Try to fetch the data again
74-
const retryResponse = await axios.get(`/api/debug/battery-history/${upsId}?days=7`);
75+
const retryResponse = await axios.get(`/api/debug/battery-history/${upsId}?days=${days}`);
7576
console.log('Battery history after manual recording:', retryResponse.data);
7677
return retryResponse.data;
7778
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "powerpulse",
3-
"version": "1.9.0",
3+
"version": "1.9.1",
44
"description": "Modern UPS monitoring dashboard integrated with Network UPS Tools (NUT)",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)