feat(storage): show SMART health in dashboard#1485
Conversation
Add latest Storage SMART snapshot aggregation for the dashboard and display per-drive health status inside the existing Storage card. Include frontend summary utilities, status icons, translations, and tests for dashboard storage health rendering.
📝 WalkthroughWalkthroughThis PR implements end-to-end storage health SMART snapshot monitoring. The core database exposes a new ChangesStorage Health Dashboard Feature
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Pull request overview
Adds support for showing per-drive Storage SMART health on the dashboard by exposing a “latest SMART snapshot per device” backend query and rendering a concise health/status list in the existing Storage dashboard card.
Changes:
- Backend: add a DB query to fetch the latest SMART snapshot per storage device, expose it via a new Tauri command, and add the corresponding (Specta) models/types.
- Frontend: add summary-building utilities + tests, and render per-drive health status icons/labels in the Storage dashboard card.
- i18n: add dashboard storage-health status translations (en/ja/ru) and fill a missing ru dashboard GPU selector label.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/rspc/bindings.ts | Adds the new command/type exports for SMART dashboard snapshots (but this file is generated). |
| src/lang/en.json | Adds storage health status strings for the dashboard. |
| src/lang/ja.json | Adds storage health status strings for the dashboard. |
| src/lang/ru.json | Adds storage health status strings and a missing dashboard GPU selector label. |
| src/features/hardware/dashboard/utils/storageHealthSummary.ts | New utility to aggregate latest SMART snapshots into a dashboard-friendly view model. |
| src/features/hardware/dashboard/utils/storageHealthSummary.test.ts | Unit tests for the storage health summary builder. |
| src/features/hardware/dashboard/hooks/useSortableDashboard.ts | Normalizes stored dashboard item lists and introduces a default item list. |
| src/features/hardware/dashboard/components/StorageHealthStatusIcon.tsx | New status icon component with accessible labeling and i18n integration. |
| src/features/hardware/dashboard/components/DashboardItems.tsx | Fetches latest SMART snapshots and renders a per-drive health overview in the Storage card. |
| src-tauri/src/models/hardware.rs | Adds app-side models/enums for SMART dashboard snapshots and conversion from core records. |
| src-tauri/src/lib.rs | Registers the new Tauri command. |
| src-tauri/src/commands/hardware.rs | Adds the new get_storage_smart_latest_snapshots command. |
| core/src/models/hardware.rs | Adds a core model for a snapshot “record” shape returned by the DB layer. |
| core/src/infrastructure/database/storage_smart.rs | Adds latest_snapshot_records() query + helpers and tests. |
| inUseBytes: string | null, | ||
| allocBytes: string | null, | ||
| } | null, string>(__TAURI_INVOKE("get_gpu_memory_usage")), | ||
| // ## Get latest Storage SMART snapshots for the dashboard | ||
| getStorageSmartLatestSnapshots: () => typedError<StorageSmartDashboardSnapshot[], string>(__TAURI_INVOKE("get_storage_smart_latest_snapshots")), | ||
| getSettings: () => typedError<ClientSettings_Serialize, string>(__TAURI_INVOKE("get_settings")), | ||
| setLanguage: (newLanguage: string) => typedError<null, string>(__TAURI_INVOKE("set_language", { newLanguage })), | ||
| setTheme: (newTheme: Theme) => typedError<null, string>(__TAURI_INVOKE("set_theme", { newTheme })), |
| const daysBetweenDateKeys = (currentDate: string, snapshotDate: string) => { | ||
| const current = Date.parse(`${currentDate}T00:00:00`); | ||
| const snapshot = Date.parse(`${snapshotDate}T00:00:00`); | ||
| if (!Number.isFinite(current) || !Number.isFinite(snapshot)) { |
| /// | ||
| /// ## Get latest Storage SMART snapshots for the dashboard | ||
| /// | ||
| #[command] | ||
| #[specta::specta] | ||
| pub async fn get_storage_smart_latest_snapshots() | ||
| -> Result<Vec<models::hardware::StorageSmartDashboardSnapshot>, String> { | ||
| hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records() | ||
| .await | ||
| .map(|records| records.into_iter().map(Into::into).collect()) | ||
| .map_err(|e| format!("Failed to fetch latest storage SMART snapshots: {e}")) | ||
| } |
| const DEFAULT_DASHBOARD_ITEMS: DashboardItemType[] = [ | ||
| "cpu", | ||
| "gpu", | ||
| "memory", | ||
| "storage", | ||
| "network", | ||
| "process", | ||
| "motherboard", | ||
| ]; |
| useEffect(() => { | ||
| let isMounted = true; | ||
|
|
||
| const loadStorageHealthDevices = async () => { | ||
| const result = await commands.getStorageSmartLatestSnapshots(); | ||
| if (!isMounted) return; | ||
|
|
||
| if (isError(result)) { | ||
| console.error( | ||
| "Failed to fetch storage SMART dashboard snapshots", | ||
| result.error, | ||
| ); | ||
| setStorageHealthDevices([]); | ||
| return; | ||
| } | ||
|
|
||
| setStorageHealthDevices(buildStorageHealthSummary(result.data).devices); | ||
| }; | ||
|
|
||
| loadStorageHealthDevices(); | ||
| const intervalId = window.setInterval(loadStorageHealthDevices, 60_000); | ||
|
|
||
| return () => { | ||
| isMounted = false; | ||
| window.clearInterval(intervalId); | ||
| }; | ||
| }, []); |
Rust Tauri Coverage ReportCoverage Details |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/src/commands/hardware.rs`:
- Around line 189-195: The command get_storage_smart_latest_snapshots currently
calls hardviz_core::infrastructure::database directly; move that DB call into a
new service function (e.g.,
services::hardware_service::get_storage_smart_latest_snapshots) which simply
delegates to
hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records()
and returns
Result<Vec<hardviz_core::models::hardware::StorageHealthSnapshotRecord>,
sqlx::Error>; then update the command get_storage_smart_latest_snapshots to call
services::hardware_service::get_storage_smart_latest_snapshots().await, map the
returned core records into the Tauri API type
(models::hardware::StorageSmartDashboardSnapshot via Into) and map errors into
the existing string error message, so the commands layer only handles boundary
formatting and the services layer owns DB access.
In `@src/features/hardware/dashboard/components/DashboardItems.tsx`:
- Around line 424-431: The error branch currently only console.errors when
isError(result) in the SMART fetch code; modify it to also show a user-facing
error via useTauriDialog().error(...) so failures are visible in the UI:
import/use the dialog hook where this code lives, call the dialog's error method
with a descriptive message and the error details (e.g., include result.error)
before or after calling setStorageHealthDevices([]), and keep the existing
console.error for debugging; update the block handling isError(result) around
the storage SMART fetch to call useTauriDialog().error(...) with the constructed
message.
In `@src/features/hardware/dashboard/utils/storageHealthSummary.ts`:
- Around line 185-191: daysBetweenDateKeys is DST-sensitive because it parses
local-midnight timestamps; switch to UTC-based parsing so midnight is canonical
and not offset by DST: replace the Date.parse(`${...}T00:00:00`) approach in
daysBetweenDateKeys with a UTC construction (e.g., parse the YYYY-MM-DD parts
and use Date.UTC(year, month-1, day) or ensure the string ends with Z) to get
UTC milliseconds for both current and snapshot, keep the same MS_PER_DAY divisor
(86_400_000) and Math.floor logic, and preserve the Number.isFinite check and
POSITIVE_INFINITY fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: d7ab6d03-5b25-4b37-a243-8ae91e6648d8
📒 Files selected for processing (14)
core/src/infrastructure/database/storage_smart.rscore/src/models/hardware.rssrc-tauri/src/commands/hardware.rssrc-tauri/src/lib.rssrc-tauri/src/models/hardware.rssrc/features/hardware/dashboard/components/DashboardItems.tsxsrc/features/hardware/dashboard/components/StorageHealthStatusIcon.tsxsrc/features/hardware/dashboard/hooks/useSortableDashboard.tssrc/features/hardware/dashboard/utils/storageHealthSummary.test.tssrc/features/hardware/dashboard/utils/storageHealthSummary.tssrc/lang/en.jsonsrc/lang/ja.jsonsrc/lang/ru.jsonsrc/rspc/bindings.ts
| pub async fn get_storage_smart_latest_snapshots() | ||
| -> Result<Vec<models::hardware::StorageSmartDashboardSnapshot>, String> { | ||
| hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records() | ||
| .await | ||
| .map(|records| records.into_iter().map(Into::into).collect()) | ||
| .map_err(|e| format!("Failed to fetch latest storage SMART snapshots: {e}")) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Route this command through services instead of calling Core DB directly.
Line 191 currently reaches into hardviz_core::infrastructure::database from the commands layer. Please move that call behind a service function and let the command only handle boundary formatting.
Proposed direction
#[command]
#[specta::specta]
pub async fn get_storage_smart_latest_snapshots()
-> Result<Vec<models::hardware::StorageSmartDashboardSnapshot>, String> {
- hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records()
+ crate::services::hardware_service::get_storage_smart_latest_snapshots()
.await
.map(|records| records.into_iter().map(Into::into).collect())
.map_err(|e| format!("Failed to fetch latest storage SMART snapshots: {e}"))
}// src-tauri/src/services/hardware_service.rs
pub async fn get_storage_smart_latest_snapshots(
) -> Result<Vec<hardviz_core::models::hardware::StorageHealthSnapshotRecord>, sqlx::Error> {
hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records().await
}As per coding guidelines, src-tauri/src/{commands,services,platform}/**/*.rs: "Add new backend commands in ... layered architecture", and src-tauri/src/commands/**/*.rs: "Commands layer should ... delegate business logic to the services layer".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src-tauri/src/commands/hardware.rs` around lines 189 - 195, The command
get_storage_smart_latest_snapshots currently calls
hardviz_core::infrastructure::database directly; move that DB call into a new
service function (e.g.,
services::hardware_service::get_storage_smart_latest_snapshots) which simply
delegates to
hardviz_core::infrastructure::database::storage_smart::latest_snapshot_records()
and returns
Result<Vec<hardviz_core::models::hardware::StorageHealthSnapshotRecord>,
sqlx::Error>; then update the command get_storage_smart_latest_snapshots to call
services::hardware_service::get_storage_smart_latest_snapshots().await, map the
returned core records into the Tauri API type
(models::hardware::StorageSmartDashboardSnapshot via Into) and map errors into
the existing string error message, so the commands layer only handles boundary
formatting and the services layer owns DB access.
| if (isError(result)) { | ||
| console.error( | ||
| "Failed to fetch storage SMART dashboard snapshots", | ||
| result.error, | ||
| ); | ||
| setStorageHealthDevices([]); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Surface SMART fetch failures to users, not only console logs.
Line 425-428 logs the error but doesn’t notify users. The failure becomes silent in the dashboard UI.
💡 Suggested fix
+import { useTauriDialog } from "@/hooks/useTauriDialog";
...
export const StorageDataInfo = () => {
const { t } = useTranslation();
+ const { error: showError } = useTauriDialog();
...
if (isError(result)) {
console.error(
"Failed to fetch storage SMART dashboard snapshots",
result.error,
);
+ showError(
+ t("error.title.error"),
+ "Failed to fetch storage health data.",
+ );
setStorageHealthDevices([]);
return;
}As per coding guidelines, "Check Result from backend commands with isError(...) helper and notify failures via useTauriDialog().error(...)".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (isError(result)) { | |
| console.error( | |
| "Failed to fetch storage SMART dashboard snapshots", | |
| result.error, | |
| ); | |
| setStorageHealthDevices([]); | |
| return; | |
| } | |
| if (isError(result)) { | |
| console.error( | |
| "Failed to fetch storage SMART dashboard snapshots", | |
| result.error, | |
| ); | |
| showError( | |
| t("error.title.error"), | |
| "Failed to fetch storage health data.", | |
| ); | |
| setStorageHealthDevices([]); | |
| return; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/hardware/dashboard/components/DashboardItems.tsx` around lines
424 - 431, The error branch currently only console.errors when isError(result)
in the SMART fetch code; modify it to also show a user-facing error via
useTauriDialog().error(...) so failures are visible in the UI: import/use the
dialog hook where this code lives, call the dialog's error method with a
descriptive message and the error details (e.g., include result.error) before or
after calling setStorageHealthDevices([]), and keep the existing console.error
for debugging; update the block handling isError(result) around the storage
SMART fetch to call useTauriDialog().error(...) with the constructed message.
| const daysBetweenDateKeys = (currentDate: string, snapshotDate: string) => { | ||
| const current = Date.parse(`${currentDate}T00:00:00`); | ||
| const snapshot = Date.parse(`${snapshotDate}T00:00:00`); | ||
| if (!Number.isFinite(current) || !Number.isFinite(snapshot)) { | ||
| return Number.POSITIVE_INFINITY; | ||
| } | ||
| return Math.floor((current - snapshot) / 86_400_000); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the file
find . -type f -name "storageHealthSummary.ts" | head -20Repository: shm11C3/HardwareVisualizer
Length of output: 134
🏁 Script executed:
# If file exists, show the context around lines 185-191
if [ -f "src/features/hardware/dashboard/utils/storageHealthSummary.ts" ]; then
cat -n "src/features/hardware/dashboard/utils/storageHealthSummary.ts" | sed -n '180,200p'
fiRepository: shm11C3/HardwareVisualizer
Length of output: 676
🏁 Script executed:
# Search for usages of daysBetweenDateKeys to understand the impact
rg "daysBetweenDateKeys" --context 5Repository: shm11C3/HardwareVisualizer
Length of output: 2178
🏁 Script executed:
# Search for isStale usage to understand how the result is used
rg "isStale" --context 3Repository: shm11C3/HardwareVisualizer
Length of output: 4922
🏁 Script executed:
# Find STORAGE_HEALTH_STALE_AFTER_DAYS constant
rg "STORAGE_HEALTH_STALE_AFTER_DAYS" -A 2 -B 2Repository: shm11C3/HardwareVisualizer
Length of output: 937
🏁 Script executed:
# Check the test file for DST-related cases and understand the threshold
cat -n "src/features/hardware/dashboard/utils/storageHealthSummary.test.ts" | head -100Repository: shm11C3/HardwareVisualizer
Length of output: 3752
🏁 Script executed:
# Verify dateKey format more thoroughly
rg "toLocalDateKey" -A 5 -B 2Repository: shm11C3/HardwareVisualizer
Length of output: 1476
🏁 Script executed:
# Check for DST-related test cases in the test file
cat -n "src/features/hardware/dashboard/utils/storageHealthSummary.test.ts" | grep -i "dst\|stale\|day" -A 3 -B 1Repository: shm11C3/HardwareVisualizer
Length of output: 2359
🏁 Script executed:
# Get the full test file to see all test cases
wc -l "src/features/hardware/dashboard/utils/storageHealthSummary.test.ts"Repository: shm11C3/HardwareVisualizer
Length of output: 141
🏁 Script executed:
# View the stale-related test cases
cat -n "src/features/hardware/dashboard/utils/storageHealthSummary.test.ts" | sed -n '110,180p'Repository: shm11C3/HardwareVisualizer
Length of output: 2435
🏁 Script executed:
# Let me create a test to demonstrate the DST issue
python3 << 'EOF'
# Simulate the current daysBetweenDateKeys behavior during DST transition
import time
from datetime import datetime, timezone
# Spring forward DST transition: March 9, 2024 at 2 AM EST -> 3 AM EDT
# Testing March 8 vs March 10
# Simulate what Date.parse would do with local timezone
# In browsers/JS, Date.parse with no timezone uses local time
# Let's calculate the millisecond difference
# March 8, 2024 00:00:00 EST (UTC-5) = 1709898000 seconds UTC
# March 10, 2024 00:00:00 EDT (UTC-4) = 1710070800 seconds UTC
march_8_utc = 1709898000
march_10_utc = 1710070800
diff_seconds = march_10_utc - march_8_utc
diff_ms = diff_seconds * 1000
hours_per_24 = 86_400_000
days_computed = diff_ms // hours_per_24
print(f"March 8 EST midnight (UTC): {march_8_utc}")
print(f"March 10 EDT midnight (UTC): {march_10_utc}")
print(f"Difference in seconds: {diff_seconds}")
print(f"Difference in hours: {diff_seconds / 3600}")
print(f"Difference in milliseconds: {diff_ms}")
print(f"Days computed (floor({diff_ms} / 86400000)): {days_computed}")
print(f"Calendar days difference: 2")
print(f"OFF BY: {2 - days_computed} day(s)")
EOFRepository: shm11C3/HardwareVisualizer
Length of output: 335
🏁 Script executed:
# Also check for any existing DST test coverage
grep -r "DST\|daylight\|transition" "src/features/hardware/dashboard/utils/" || echo "No DST tests found"Repository: shm11C3/HardwareVisualizer
Length of output: 89
🏁 Script executed:
# Verify the dateKey format by checking if it's used elsewhere with expected patterns
rg "date.*:.*\"[0-9]{4}-[0-9]{2}-[0-9]{2}\"" | head -20Repository: shm11C3/HardwareVisualizer
Length of output: 268
🏁 Script executed:
# Check if there's any actual DST test or boundary case test
cat -n "src/features/hardware/dashboard/utils/storageHealthSummary.test.ts" | tail -50Repository: shm11C3/HardwareVisualizer
Length of output: 1743
🏁 Script executed:
# Let me simulate a more accurate DST boundary case (adjacent calendar days across DST)
python3 << 'EOF'
# The key issue: March 9 EST -> March 10 EDT (same midnight times but different UTC offsets)
# Spring forward: At 2 AM EST, clocks jump to 3 AM EDT
# March 9, 2024 00:00:00 EST (UTC-5:00)
march_9_est_seconds = 1709898000 # March 9 05:00 UTC
# March 10, 2024 00:00:00 EDT (UTC-4:00) - one hour shifted
march_10_edt_seconds = 1710010800 # March 10 04:00 UTC
diff_seconds = march_10_edt_seconds - march_9_est_seconds
diff_ms = diff_seconds * 1000
hours_per_24 = 86_400_000
days_computed = diff_ms // hours_per_24
print("=== DST Boundary Case: March 9 EST to March 10 EDT ===")
print(f"March 9, 2024 00:00:00 EST (local) = {march_9_est_seconds} seconds UTC")
print(f"March 10, 2024 00:00:00 EDT (local) = {march_10_edt_seconds} seconds UTC")
print(f"Actual difference: {diff_seconds} seconds = {diff_seconds/3600} hours")
print(f"Milliseconds: {diff_ms}")
print(f"Days computed via division: floor({diff_ms} / {hours_per_24}) = {days_computed}")
print(f"Calendar days differ by: 1")
print(f"RESULT: Off by {1 - days_computed} day(s) - Returns 0 instead of 1!")
# Another case: March 10 EDT to March 12 EDT (3 calendar days spanning forward transition)
march_12_edt_seconds = 1710184800 # March 12 04:00 UTC
diff2_seconds = march_12_edt_seconds - march_9_est_seconds
diff2_ms = diff2_seconds * 1000
days2_computed = diff2_ms // hours_per_24
print("\n=== Spanning DST: March 9 EST to March 12 EDT ===")
print(f"Actual difference: {diff2_seconds} seconds = {diff2_seconds/3600} hours")
print(f"Days computed: {days2_computed}")
print(f"Calendar days differ by: 3")
print(f"RESULT: Off by {3 - days2_computed} day(s)")
EOFRepository: shm11C3/HardwareVisualizer
Length of output: 647
🏁 Script executed:
# Verify what Date.UTC would return for comparison
python3 << 'EOF'
from datetime import datetime, timezone
# Date.UTC always interprets as UTC midnight
march_9_utc = datetime(2024, 3, 9, 0, 0, 0, tzinfo=timezone.utc).timestamp()
march_10_utc = datetime(2024, 3, 10, 0, 0, 0, tzinfo=timezone.utc).timestamp()
march_12_utc = datetime(2024, 3, 12, 0, 0, 0, tzinfo=timezone.utc).timestamp()
diff1 = (march_10_utc - march_9_utc) / 86400
diff2 = (march_12_utc - march_9_utc) / 86400
print("=== Using UTC dates (proposed fix) ===")
print(f"March 9 UTC midnight to March 10 UTC midnight: {diff1} days (floor = {int(diff1)})")
print(f"March 9 UTC midnight to March 12 UTC midnight: {diff2} days (floor = {int(diff2)})")
print("Always correct regardless of DST!")
EOFRepository: shm11C3/HardwareVisualizer
Length of output: 279
🏁 Script executed:
# Test the actual direction: newer date minus older date across DST
python3 << 'EOF'
# The function is called as: daysBetweenDateKeys(toLocalDateKey(now), latestDate)
# So: (current - snapshot) where current is "now" and snapshot is "latestDate"
# Case 1: One calendar day across spring forward DST (Mar 9 EST -> Mar 10 EDT)
# now = March 10, latestDate = March 9
march_10_edt_utc = 1710010800 # March 10 00:00 EDT = March 10 04:00 UTC
march_9_est_utc = 1709898000 # March 9 00:00 EST = March 9 05:00 UTC
diff1 = march_10_edt_utc - march_9_est_utc
days1 = diff1 // 86400
remainder1 = diff1 % 86400
print("=== 1 Calendar Day Across Spring Forward (current=Mar10 EDT, snapshot=Mar9 EST) ===")
print(f"March 10 00:00 EDT (local) = {march_10_edt_utc} UTC seconds = March 10 04:00 UTC")
print(f"March 9 00:00 EST (local) = {march_9_est_utc} UTC seconds = March 9 05:00 UTC")
print(f"Difference: {diff1} seconds = {diff1/3600} hours")
print(f"Days computed: floor({diff1} / 86400) = {days1}")
print(f"Calendar day difference: 1")
print(f"BUG: Returns {days1} instead of 1! ❌\n")
# Case 2: Three calendar days spanning DST
march_12_edt_utc = 1710184800 # March 12 00:00 EDT = March 12 04:00 UTC
diff2 = march_12_edt_utc - march_9_est_utc
days2 = diff2 // 86400
print("=== 3 Calendar Days Spanning Spring Forward (current=Mar12 EDT, snapshot=Mar9 EST) ===")
print(f"Difference: {diff2} seconds = {diff2/3600} hours")
print(f"Days computed: floor({diff2} / 86400) = {days2}")
print(f"Calendar day difference: 3")
print(f"BUG: Returns {days2} instead of 3! ❌")
EOFRepository: shm11C3/HardwareVisualizer
Length of output: 691
🏁 Script executed:
# Let me recalculate more precisely using actual timezone-aware datetime
python3 << 'EOF'
from datetime import datetime, timezone, timedelta, tzinfo
# Define EST and EDT timezones
class EST(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=-5)
def tzname(self, dt):
return "EST"
def dst(self, dt):
return timedelta(0)
class EDT(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=-4)
def tzname(self, dt):
return "EDT"
def dst(self, dt):
return timedelta(0)
# Create dates with appropriate timezones for DST transition
march_9_est = datetime(2024, 3, 9, 0, 0, 0, tzinfo=EST())
march_10_edt = datetime(2024, 3, 10, 0, 0, 0, tzinfo=EDT())
# Convert to UTC to see the actual time difference
march_9_utc = march_9_est.astimezone(timezone.utc)
march_10_utc = march_10_edt.astimezone(timezone.utc)
print(f"March 9 00:00 EST = {march_9_utc} UTC")
print(f"March 10 00:00 EDT = {march_10_utc} UTC")
diff_seconds = (march_10_utc - march_9_utc).total_seconds()
diff_hours = diff_seconds / 3600
days_floored = int(diff_seconds // 86400)
print(f"\nTime difference: {diff_seconds} seconds = {diff_hours} hours")
print(f"Days (floored): {days_floored}")
print(f"Calendar days apart: 1")
print(f"\nBUG CONFIRMED: Returns {days_floored} days instead of 1 day!")
print("\nThis means data 1 calendar day old reports as 0 days old.")
print("With STORAGE_HEALTH_STALE_AFTER_DAYS = 2, it won't be marked stale until 3+ days pass.")
EOFRepository: shm11C3/HardwareVisualizer
Length of output: 454
Fix DST-sensitive date comparison that causes staleness misclassification.
The daysBetweenDateKeys function uses Date.parse() with local timezone, which introduces a 1-hour offset at DST transitions. During spring forward (e.g., March 9 EST to March 10 EDT), the midnight times are only 23 hours apart; dividing by 86,400ms (24 hours) returns 0 days instead of 1. This causes data 1 calendar day old to incorrectly report as 0 days old. With STORAGE_HEALTH_STALE_AFTER_DAYS = 2, storage data up to 3 calendar days old may not be marked stale.
Use UTC date parsing to eliminate timezone-dependent calculations:
Suggested fix
+const parseDateKeyUtc = (dateKey: string) => {
+ const [year, month, day] = dateKey.split("-").map(Number);
+ if (!year || !month || !day) return Number.NaN;
+ return Date.UTC(year, month - 1, day);
+};
+
const daysBetweenDateKeys = (currentDate: string, snapshotDate: string) => {
- const current = Date.parse(`${currentDate}T00:00:00`);
- const snapshot = Date.parse(`${snapshotDate}T00:00:00`);
+ const current = parseDateKeyUtc(currentDate);
+ const snapshot = parseDateKeyUtc(snapshotDate);
if (!Number.isFinite(current) || !Number.isFinite(snapshot)) {
return Number.POSITIVE_INFINITY;
}
return Math.floor((current - snapshot) / 86_400_000);
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/hardware/dashboard/utils/storageHealthSummary.ts` around lines
185 - 191, daysBetweenDateKeys is DST-sensitive because it parses local-midnight
timestamps; switch to UTC-based parsing so midnight is canonical and not offset
by DST: replace the Date.parse(`${...}T00:00:00`) approach in
daysBetweenDateKeys with a UTC construction (e.g., parse the YYYY-MM-DD parts
and use Date.UTC(year, month-1, day) or ensure the string ends with Z) to get
UTC milliseconds for both current and snapshot, keep the same MS_PER_DAY divisor
(86_400_000) and Math.floor logic, and preserve the Number.isFinite check and
POSITIVE_INFINITY fallback.
Add latest Storage SMART snapshot aggregation for the dashboard and display per-drive health status inside the existing Storage card.
Include frontend summary utilities, status icons, translations, and tests for dashboard storage health rendering.
Summary
Related Issues
Type of Change
fix/branch)feat/branch)refactor/branch)docs/branch)chore/branch)Screenshots / Videos
Test Plan
Checklist
npm run lint && npm run format/cargo tauri-lint && cargo tauri-fmt)npm test/cargo tauri-test)Summary by CodeRabbit
New Features
Localization